Skip to content
Snippets Groups Projects
Verified Commit a2ff1112 authored by Simon Knox's avatar Simon Knox Committed by GitLab
Browse files

Merge branch '477534-development-widget-view-branches-2' into 'master'

View branches in development widget

See merge request !173384



Merged-by: Simon Knox's avatarSimon Knox <simon@gitlab.com>
Approved-by: Nick Leonard's avatarNick Leonard <nleonard@gitlab.com>
Approved-by: default avatarFernanda Toledo <ftoledo@gitlab.com>
Approved-by: Simon Knox's avatarSimon Knox <simon@gitlab.com>
Reviewed-by: Simon Knox's avatarSimon Knox <simon@gitlab.com>
Reviewed-by: default avatarFernanda Toledo <ftoledo@gitlab.com>
Co-authored-by: Deepika Guliani's avatarDeepika Guliani <dguliani@gitlab.com>
parents 63656bb5 9c9f04ac
No related branches found
No related tags found
1 merge request!173384View branches in development widget
Pipeline #1560379844 passed with warnings
Pipeline: E2E Omnibus GitLab EE

#1560411239

    Pipeline: E2E CNG

    #1560411232

      Pipeline: E2E GDK

      #1560382937

        +30
        Showing
        with 234 additions and 62 deletions
        ......@@ -88,6 +88,9 @@ export default {
        featureFlags() {
        return this.workItemDevelopment?.featureFlags?.nodes || [];
        },
        relatedBranches() {
        return this.workItemDevelopment?.relatedBranches?.nodes || [];
        },
        shouldShowEmptyState() {
        return this.isRelatedDevelopmentListEmpty ? this.workItemsAlphaEnabled : true;
        },
        ......@@ -95,7 +98,12 @@ export default {
        return this.workItemDevelopment && this.shouldShowEmptyState;
        },
        isRelatedDevelopmentListEmpty() {
        return !this.error && this.linkedMergeRequests.length === 0 && this.featureFlags.length === 0;
        return (
        !this.error &&
        this.linkedMergeRequests.length === 0 &&
        this.featureFlags.length === 0 &&
        this.relatedBranches.length === 0
        );
        },
        showAutoCloseInformation() {
        return (
        ......
        <script>
        import { GlIcon, GlTooltip, GlLink } from '@gitlab/ui';
        export default {
        components: {
        GlIcon,
        GlTooltip,
        GlLink,
        },
        props: {
        itemContent: {
        type: Object,
        required: true,
        },
        },
        };
        </script>
        <template>
        <div
        ref="branchInfo"
        class="gl-grid-cols-[auto, 1fr] gl-grid gl-w-fit gl-gap-2 gl-gap-5 gl-p-2 gl-pl-0 gl-pr-3"
        >
        <gl-link
        :href="itemContent.comparePath"
        class="gl-truncate gl-text-primary hover:gl-text-primary hover:gl-underline"
        >
        <gl-icon name="branch" class="gl-text-gray-500" />
        {{ itemContent.name }}
        </gl-link>
        <gl-tooltip :target="() => $refs.branchInfo" placement="top">
        <div class="gl-font-bold">{{ __('Branch') }}</div>
        <div>{{ itemContent.name }}</div>
        </gl-tooltip>
        </div>
        </template>
        <script>
        import { GlButton } from '@gitlab/ui';
        import { uniqueId } from 'lodash';
        import { sprintf, __ } from '~/locale';
        import { renderGFM } from '~/behaviors/markdown/render_gfm';
        import { TYPENAME_FEATURE_FLAG } from '~/graphql_shared/constants';
        import WorkItemDevelopmentMrItem from './work_item_development_mr_item.vue';
        import WorkItemDevelopmentBranchItem from './work_item_development_branch_item.vue';
        const DEFAULT_RENDER_COUNT = 3;
        ......@@ -30,8 +32,10 @@ export default {
        },
        computed: {
        list() {
        // keeping as a separate prop, will be appending with branches
        return [...this.sortedFeatureFlags, ...this.mergeRequests];
        return [...this.sortedFeatureFlags, ...this.mergeRequests, ...this.relatedBranches];
        },
        relatedBranches() {
        return this.workItemDevWidget.relatedBranches?.nodes || [];
        },
        mergeRequests() {
        return this.workItemDevWidget.closingMergeRequests?.nodes || [];
        ......@@ -74,6 +78,8 @@ export default {
        component = WorkItemDevelopmentMrItem;
        } else if (this.isFeatureFlag(item)) {
        component = 'work-item-development-ff-item';
        } else if (this.isBranch(item)) {
        component = WorkItemDevelopmentBranchItem;
        } else {
        component = 'li';
        }
        ......@@ -86,13 +92,17 @@ export default {
        // eslint-disable-next-line no-underscore-dangle
        return item.__typename === TYPENAME_FEATURE_FLAG;
        },
        isBranch(item) {
        // eslint-disable-next-line no-underscore-dangle
        return item.__typename === 'WorkItemRelatedBranch';
        },
        async toggleShowLess() {
        this.showLess = !this.showLess;
        await this.$nextTick();
        renderGFM(this.$refs['list-body']);
        },
        itemId(item) {
        return item.id || item.mergeRequest.id;
        return item?.id || item?.mergeRequest?.id || uniqueId('branch-id-');
        },
        itemObject(item) {
        return this.isMergeRequest(item) ? item.mergeRequest : item;
        ......
        ......@@ -96,6 +96,12 @@ fragment WorkItemWidgets on WorkItemWidget {
        ... on WorkItemWidgetDevelopment {
        type
        willAutoCloseByMergeRequest
        relatedBranches {
        nodes {
        name
        comparePath
        }
        }
        closingMergeRequests {
        nodes {
        fromMrDescription
        ......
        ......@@ -138,6 +138,12 @@ fragment WorkItemWidgets on WorkItemWidget {
        ... on WorkItemWidgetDevelopment {
        type
        willAutoCloseByMergeRequest
        relatedBranches {
        nodes {
        name
        comparePath
        }
        }
        featureFlags {
        nodes {
        id
        ......
        ......@@ -11,6 +11,7 @@ import {
        workItemDevelopmentFragmentResponse,
        workItemDevelopmentMRNodes,
        workItemDevelopmentFeatureFlagNodes,
        workItemRelatedBranchNodes,
        } from 'jest/work_items/mock_data';
        import WorkItemDevelopment from '~/work_items/components/work_item_development/work_item_development.vue';
        ......@@ -28,32 +29,55 @@ describe('WorkItemDevelopment EE', () => {
        const workItemWithMRListOnly = workItemResponseFactory({
        developmentWidgetPresent: true,
        developmentItems: workItemDevelopmentFragmentResponse(workItemDevelopmentMRNodes, true, []),
        developmentItems: workItemDevelopmentFragmentResponse({
        mrNodes: workItemDevelopmentMRNodes,
        willAutoCloseByMergeRequest: true,
        featureFlagNodes: [],
        branchNodes: [],
        }),
        });
        const workItemWithFlagListOnly = workItemResponseFactory({
        developmentWidgetPresent: true,
        developmentItems: workItemDevelopmentFragmentResponse(
        [],
        false,
        workItemDevelopmentFeatureFlagNodes,
        ),
        developmentItems: workItemDevelopmentFragmentResponse({
        mrNodes: [],
        willAutoCloseByMergeRequest: false,
        featureFlagNodes: workItemDevelopmentFeatureFlagNodes,
        branchNodes: [],
        }),
        canUpdate: true,
        });
        const workItemWithNoMRsOrFlags = workItemResponseFactory({
        const workItemWithBranchListOnly = workItemResponseFactory({
        developmentWidgetPresent: true,
        developmentItems: workItemDevelopmentFragmentResponse([], false, []),
        developmentItems: workItemDevelopmentFragmentResponse({
        mrNodes: [],
        willAutoCloseByMergeRequest: false,
        featureFlagNodes: [],
        branchNodes: workItemRelatedBranchNodes,
        }),
        canUpdate: true,
        });
        const workItemWithMRsAndFlagList = workItemResponseFactory({
        const workItemWithNoDevItems = workItemResponseFactory({
        developmentWidgetPresent: true,
        developmentItems: workItemDevelopmentFragmentResponse(
        workItemDevelopmentMRNodes,
        true,
        workItemDevelopmentFeatureFlagNodes,
        ),
        developmentItems: workItemDevelopmentFragmentResponse({
        mrNodes: [],
        willAutoCloseByMergeRequest: false,
        featureFlagNodes: [],
        branchNodes: [],
        }),
        canUpdate: true,
        });
        const workItemWithAllDevItems = workItemResponseFactory({
        developmentWidgetPresent: true,
        developmentItems: workItemDevelopmentFragmentResponse({
        mrNodes: workItemDevelopmentMRNodes,
        willAutoCloseByMergeRequest: true,
        featureFlagNodes: workItemDevelopmentFeatureFlagNodes,
        branchNodes: workItemRelatedBranchNodes,
        }),
        });
        const successQueryHandler = jest.fn().mockResolvedValue({
        ......@@ -66,7 +90,7 @@ describe('WorkItemDevelopment EE', () => {
        },
        });
        const successQueryHandlerWithEmptyFlagListAndHasMRList = jest.fn().mockResolvedValue({
        const successQueryHandlerWithOnlyMRList = jest.fn().mockResolvedValue({
        data: {
        workspace: {
        __typename: 'Project',
        ......@@ -76,22 +100,32 @@ describe('WorkItemDevelopment EE', () => {
        },
        });
        const successQueryHandlerWithEmptyMRAndFlagList = jest.fn().mockResolvedValue({
        const successQueryHandlerWithOnlyBranchList = jest.fn().mockResolvedValue({
        data: {
        workspace: {
        __typename: 'Project',
        id: 'gid://gitlab/Project/1',
        workItem: workItemWithBranchListOnly.data.workItem,
        },
        },
        });
        const successQueryHandlerWithNoDevItem = jest.fn().mockResolvedValue({
        data: {
        workspace: {
        __typename: 'Project',
        id: 'gid://gitlab/Project/1',
        workItem: workItemWithNoMRsOrFlags.data.workItem,
        workItem: workItemWithNoDevItems.data.workItem,
        },
        },
        });
        const successQueryHandlerWithMRAndFlagList = jest.fn().mockResolvedValue({
        const successQueryHandlerWithAllDevItemsList = jest.fn().mockResolvedValue({
        data: {
        workspace: {
        __typename: 'Project',
        id: 'gid://gitlab/Project/1',
        workItem: workItemWithMRsAndFlagList.data.workItem,
        workItem: workItemWithAllDevItems.data.workItem,
        },
        },
        });
        ......@@ -155,7 +189,7 @@ describe('WorkItemDevelopment EE', () => {
        describe('when the list of Feature Flag is empty but there is a MR list', () => {
        it(`hides 'Create MR' and 'Create branch' buttons when flag enabled`, async () => {
        createComponent({
        workItemQueryHandler: successQueryHandlerWithEmptyFlagListAndHasMRList,
        workItemQueryHandler: successQueryHandlerWithOnlyMRList,
        workItemsAlphaEnabled: true,
        });
        await waitForPromises();
        ......@@ -166,7 +200,7 @@ describe('WorkItemDevelopment EE', () => {
        it(`hides 'Create MR' and 'Create branch' buttons when flag disabled`, async () => {
        createComponent({
        workItemQueryHandler: successQueryHandlerWithEmptyFlagListAndHasMRList,
        workItemQueryHandler: successQueryHandlerWithOnlyMRList,
        workItemsAlphaEnabled: false,
        });
        await waitForPromises();
        ......@@ -179,7 +213,7 @@ describe('WorkItemDevelopment EE', () => {
        describe('when both the list of Feature flags and MRs are empty', () => {
        it(`shows 'Create MR' and 'Create branch' buttons when flag enabled`, async () => {
        createComponent({
        workItemQueryHandler: successQueryHandlerWithEmptyMRAndFlagList,
        workItemQueryHandler: successQueryHandlerWithNoDevItem,
        workItemsAlphaEnabled: true,
        });
        await waitForPromises();
        ......@@ -190,7 +224,7 @@ describe('WorkItemDevelopment EE', () => {
        it(`hides 'Create MR' and 'Create branch' buttons when flag disabled`, async () => {
        createComponent({
        workItemQueryHandler: successQueryHandlerWithEmptyMRAndFlagList,
        workItemQueryHandler: successQueryHandlerWithNoDevItem,
        workItemsAlphaEnabled: false,
        });
        await waitForPromises();
        ......@@ -203,7 +237,7 @@ describe('WorkItemDevelopment EE', () => {
        describe('when both the list of Feature flags and MRs exist', () => {
        it(`hides 'Create MR' and 'Create branch' buttons when flag enabled`, async () => {
        createComponent({
        workItemQueryHandler: successQueryHandlerWithMRAndFlagList,
        workItemQueryHandler: successQueryHandlerWithAllDevItemsList,
        workItemsAlphaEnabled: true,
        });
        await waitForPromises();
        ......@@ -214,7 +248,7 @@ describe('WorkItemDevelopment EE', () => {
        it(`hides 'Create MR' and 'Create branch' buttons when flag disabled`, async () => {
        createComponent({
        workItemQueryHandler: successQueryHandlerWithMRAndFlagList,
        workItemQueryHandler: successQueryHandlerWithAllDevItemsList,
        workItemsAlphaEnabled: false,
        });
        await waitForPromises();
        ......@@ -224,27 +258,20 @@ describe('WorkItemDevelopment EE', () => {
        });
        });
        describe('when there is only a list of feature flags', () => {
        beforeEach(async () => {
        createComponent();
        await waitForPromises();
        });
        it('should show the relationship list', () => {
        expect(findRelationshipList().exists()).toBe(true);
        });
        });
        describe('when there is only a list of MR`s', () => {
        beforeEach(async () => {
        it.each`
        description | successQueryResolveHandler
        ${'feature flags'} | ${successQueryHandler}
        ${'MRs'} | ${successQueryHandlerWithOnlyMRList}
        ${'branches'} | ${successQueryHandlerWithOnlyBranchList}
        `(
        'should show the relationship list when there is only a list of $description',
        async ({ successQueryResolveHandler }) => {
        createComponent({
        workItemQueryHandler: successQueryHandlerWithEmptyFlagListAndHasMRList,
        workItemQueryHandler: successQueryResolveHandler,
        });
        await waitForPromises();
        });
        it('should show the relationship list', () => {
        expect(findRelationshipList().exists()).toBe(true);
        });
        });
        },
        );
        });
        import { GlLink, GlIcon, GlTooltip } from '@gitlab/ui';
        import { shallowMount } from '@vue/test-utils';
        import { workItemRelatedBranchNodes } from 'jest/work_items/mock_data';
        import WorkItemDevelopmentBranchItem from '~/work_items/components/work_item_development/work_item_development_branch_item.vue';
        describe('WorkItemDevelopmentBranchItem', () => {
        let wrapper;
        const branchNode = workItemRelatedBranchNodes[0];
        const createComponent = ({ branch = branchNode } = {}) => {
        wrapper = shallowMount(WorkItemDevelopmentBranchItem, {
        propsData: {
        itemContent: branch,
        },
        });
        };
        const findIcon = () => wrapper.findComponent(GlIcon);
        const findLink = () => wrapper.findComponent(GlLink);
        const findTooltip = () => wrapper.findComponent(GlTooltip);
        it('should render the comparePath and name with icon', () => {
        createComponent();
        expect(findIcon().exists()).toBe(true);
        expect(findIcon().props('name')).toBe('branch');
        expect(findLink().attributes('href')).toBe(branchNode.comparePath);
        expect(findLink().text()).toBe(branchNode.name);
        });
        it('should render the tooltip with name', () => {
        createComponent();
        expect(findTooltip().exists()).toBe(true);
        expect(findTooltip().text()).toContain(`Branch ${branchNode.name}`);
        });
        });
        ......@@ -12,6 +12,10 @@ describe('WorkItemDevelopmentRelationshipList', () => {
        nodes: [workItemDevelopmentFragmentResponse().closingMergeRequests.nodes[0]],
        __typename: 'WorkItemClosingMergeRequestConnection',
        },
        relatedBranches: {
        nodes: [],
        __typename: 'WorkItemRelatedBranchConnection',
        },
        featureFlags: {
        nodes: [workItemDevelopmentFragmentResponse().featureFlags.nodes[0]],
        __typename: 'FeatureFlagConnection',
        ......@@ -24,6 +28,10 @@ describe('WorkItemDevelopmentRelationshipList', () => {
        nodes: [workItemDevelopmentFragmentResponse().closingMergeRequests.nodes[0]],
        __typename: 'WorkItemClosingMergeRequestConnection',
        },
        relatedBranches: {
        nodes: [],
        __typename: 'WorkItemRelatedBranchConnection',
        },
        featureFlags: {
        nodes: [
        workItemDevelopmentFragmentResponse().featureFlags.nodes[0],
        ......
        ......@@ -30,15 +30,21 @@ describe('WorkItemDevelopment CE', () => {
        });
        const workItemWithOneMR = workItemResponseFactory({
        developmentWidgetPresent: true,
        developmentItems: workItemDevelopmentFragmentResponse(
        [workItemDevelopmentMRNodes[0]],
        true,
        null,
        ),
        developmentItems: workItemDevelopmentFragmentResponse({
        mrNodes: [workItemDevelopmentMRNodes[0]],
        willAutoCloseByMergeRequest: true,
        featureFlagNodes: null,
        branchNodes: [],
        }),
        });
        const workItemWithMRList = workItemResponseFactory({
        developmentWidgetPresent: true,
        developmentItems: workItemDevelopmentFragmentResponse(workItemDevelopmentMRNodes, true, null),
        developmentItems: workItemDevelopmentFragmentResponse({
        mrNodes: workItemDevelopmentMRNodes,
        willAutoCloseByMergeRequest: true,
        featureFlagNodes: null,
        branchNodes: [],
        }),
        });
        const projectWorkItemResponseWithMRList = {
        ......@@ -95,22 +101,33 @@ describe('WorkItemDevelopment CE', () => {
        };
        const successQueryHandler = jest.fn().mockResolvedValue(projectWorkItemResponseWithMRList);
        const workItemWithEmptyMRList = workItemResponseFactory({
        const workItemWithNoDevItems = workItemResponseFactory({
        canUpdate: true,
        developmentWidgetPresent: true,
        developmentItems: workItemDevelopmentFragmentResponse([], false, null),
        developmentItems: workItemDevelopmentFragmentResponse({
        mrNodes: [],
        willAutoCloseByMergeRequest: false,
        featureFlagNodes: null,
        branchNodes: [],
        }),
        });
        const workItemWithAutoCloseFlagEnabled = workItemResponseFactory({
        developmentWidgetPresent: true,
        developmentItems: workItemDevelopmentFragmentResponse(workItemDevelopmentMRNodes, true, null),
        developmentItems: workItemDevelopmentFragmentResponse({
        mrNodes: workItemDevelopmentMRNodes,
        willAutoCloseByMergeRequest: true,
        featureFlagNodes: null,
        branchNodes: [],
        }),
        });
        const successQueryHandlerWithEmptyMRList = jest.fn().mockResolvedValue({
        const successQueryHandlerWithNoDevItems = jest.fn().mockResolvedValue({
        data: {
        workspace: {
        __typename: 'Project',
        id: 'gid://gitlab/Project/1',
        workItem: workItemWithEmptyMRList.data.workItem,
        workItem: workItemWithNoDevItems.data.workItem,
        },
        },
        });
        ......@@ -217,11 +234,11 @@ describe('WorkItemDevelopment CE', () => {
        ${true} | ${true}
        ${false} | ${false}
        `(
        'when the list of MRs is empty and workItemsAlpha is `$workItemsAlphaFFEnabled`',
        'when the list of dev items is empty and workItemsAlpha is `$workItemsAlphaFFEnabled`',
        ({ workItemsAlphaFFEnabled, shouldShowActionCtaButtons }) => {
        beforeEach(async () => {
        createComponent({
        workItemQueryHandler: successQueryHandlerWithEmptyMRList,
        workItemQueryHandler: successQueryHandlerWithNoDevItems,
        workItemsAlphaEnabled: workItemsAlphaFFEnabled,
        });
        await waitForPromises();
        ......
        ......@@ -1136,11 +1136,25 @@ export const workItemDevelopmentFeatureFlagNodes = [
        },
        ];
        export const workItemDevelopmentFragmentResponse = (
        export const workItemRelatedBranchNodes = [
        {
        name: '178-issue',
        comparePath: '/flightjs/Flight/-/compare/master...178-issue',
        __typename: 'WorkItemRelatedBranch',
        },
        {
        name: '178-issue-10',
        comparePath: '/flightjs/Flight/-/compare/master...178-issue-10',
        __typename: 'WorkItemRelatedBranch',
        },
        ];
        export const workItemDevelopmentFragmentResponse = ({
        mrNodes = workItemDevelopmentMRNodes,
        willAutoCloseByMergeRequest = false,
        featureFlagNodes = workItemDevelopmentFeatureFlagNodes,
        ) => {
        branchNodes = workItemRelatedBranchNodes,
        } = {}) => {
        return {
        type: 'DEVELOPMENT',
        willAutoCloseByMergeRequest,
        ......@@ -1148,6 +1162,10 @@ export const workItemDevelopmentFragmentResponse = (
        nodes: featureFlagNodes,
        __typename: 'FeatureFlagConnection',
        },
        relatedBranches: {
        nodes: branchNodes,
        __typename: 'WorkItemRelatedBranchConnection',
        },
        closingMergeRequests: {
        nodes: mrNodes,
        __typename: 'WorkItemClosingMergeRequestConnection',
        ......
        0% Loading or .
        You are about to add 0 people to the discussion. Proceed with caution.
        Finish editing this message first!
        Please register or to comment