diff --git a/app/assets/javascripts/work_items/components/create_work_item.vue b/app/assets/javascripts/work_items/components/create_work_item.vue index bcab1d96e9300b097d6523d4f9c1d8fc181f57ec..7e6ea35febabb9ce2dd1332c02245d5d4e839913 100644 --- a/app/assets/javascripts/work_items/components/create_work_item.vue +++ b/app/assets/javascripts/work_items/components/create_work_item.vue @@ -29,6 +29,7 @@ import { WIDGET_TYPE_DESCRIPTION, NEW_WORK_ITEM_GID, WIDGET_TYPE_LABELS, + WIDGET_TYPE_WEIGHT, WIDGET_TYPE_ROLLEDUP_DATES, WIDGET_TYPE_CRM_CONTACTS, WIDGET_TYPE_LINKED_ITEMS, @@ -62,6 +63,7 @@ export default { WorkItemLoading, WorkItemCrmContacts, WorkItemProjectsListbox, + WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'), WorkItemHealthStatus: () => import('ee_component/work_items/components/work_item_health_status.vue'), WorkItemColor: () => import('ee_component/work_items/components/work_item_color.vue'), @@ -213,6 +215,9 @@ export default { workItemIteration() { return findWidget(WIDGET_TYPE_ITERATION, this.workItem); }, + workItemWeight() { + return findWidget(WIDGET_TYPE_WEIGHT, this.workItem); + }, workItemHealthStatus() { return findWidget(WIDGET_TYPE_HEALTH_STATUS, this.workItem); }, @@ -279,6 +284,10 @@ export default { const labelsWidget = findWidget(WIDGET_TYPE_LABELS, this.workItem); return labelsWidget?.labels?.nodes?.map((label) => label.id) || []; }, + workItemWeightValue() { + const weightWidget = findWidget(WIDGET_TYPE_WEIGHT, this.workItem); + return weightWidget?.weight ?? null; + }, workItemCrmContactIds() { return this.workItemCrmContacts?.contacts?.nodes?.map((item) => item.id) || []; }, @@ -425,6 +434,12 @@ export default { }; } + if (this.isWidgetSupported(WIDGET_TYPE_WEIGHT)) { + workItemCreateInput.weightWidget = { + weight: this.workItemWeightValue, + }; + } + if (this.isWidgetSupported(WIDGET_TYPE_ROLLEDUP_DATES)) { workItemCreateInput.rolledupDatesWidget = { dueDateIsFixed: this.workItemDueDateIsFixed, @@ -625,6 +640,18 @@ export default { @error="$emit('error', $event)" /> </template> + <template v-if="workItemWeight"> + <work-item-weight + class="work-item-attributes-item" + :can-update="canUpdate" + :full-path="fullPath" + :widget="workItemWeight" + :work-item-id="workItemId" + :work-item-iid="workItemIid" + :work-item-type="selectedWorkItemTypeName" + @error="$emit('error', $event)" + /> + </template> <template v-if="workItemRolledupDates"> <work-item-rolledup-dates class="work-item-attributes-item" diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue index 8f45a703e7e1c4a25f0d626733131296bc0c6d69..febd6eeba16ac8d4c922d7b1251464b32abbf2a5 100644 --- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue +++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue @@ -129,9 +129,6 @@ export default { workItemWeight() { return this.isWidgetPresent(WIDGET_TYPE_WEIGHT); }, - isWorkItemWeightEditable() { - return this.workItemWeight?.widgetDefinition?.editable; - }, workItemProgress() { return this.isWidgetPresent(WIDGET_TYPE_PROGRESS); }, @@ -216,11 +213,12 @@ export default { @labelsUpdated="$emit('attributesUpdated', { type: $options.ListType.label, ids: $event })" /> </template> - <template v-if="isWorkItemWeightEditable"> + <template v-if="workItemWeight"> <work-item-weight class="work-item-attributes-item" :can-update="canUpdate" - :weight="workItemWeight.weight" + :full-path="fullPath" + :widget="workItemWeight" :work-item-id="workItem.id" :work-item-iid="workItem.iid" :work-item-type="workItemType" diff --git a/app/assets/javascripts/work_items/graphql/resolvers.js b/app/assets/javascripts/work_items/graphql/resolvers.js index 240af0d8f1c0aaaea89c78babeb16f0a57e3221e..d420826f5485907ca55d6f8f5155046f70988f56 100644 --- a/app/assets/javascripts/work_items/graphql/resolvers.js +++ b/app/assets/javascripts/work_items/graphql/resolvers.js @@ -13,6 +13,7 @@ import { WIDGET_TYPE_DESCRIPTION, WIDGET_TYPE_CRM_CONTACTS, WIDGET_TYPE_ITERATION, + WIDGET_TYPE_WEIGHT, NEW_WORK_ITEM_IID, } from '../constants'; import workItemByIidQuery from './work_item_by_iid.query.graphql'; @@ -63,6 +64,7 @@ export const updateNewWorkItemCache = (input, cache) => { rolledUpDates, crmContacts, iteration, + weight, } = input; const query = workItemByIidQuery; @@ -109,6 +111,11 @@ export const updateNewWorkItemCache = (input, cache) => { newData: iteration, nodePath: 'iteration', }, + { + widgetType: WIDGET_TYPE_WEIGHT, + newData: weight, + nodePath: 'weight', + }, ]; widgetUpdates.forEach(({ widgetType, newData, nodePath }) => { diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql index 6122976e26a75d61b4966cc3da554a3f34bc55de..c713134913d35184baa3f137e24ee4364fbf5b21 100644 --- a/app/assets/javascripts/work_items/graphql/typedefs.graphql +++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql @@ -85,6 +85,7 @@ input LocalUpdateNewWorkItemInput { iteration: [LocalIterationInput] rolledUpDates: [LocalRolledUpDatesInput] crmContacts: [LocalCrmContactsInput] + weight: Int } extend type Mutation { diff --git a/ee/app/assets/javascripts/work_items/components/work_item_weight.vue b/ee/app/assets/javascripts/work_items/components/work_item_weight.vue index feb7ef3d8f8ca275f7726a54acfefedbf7c39199..ce13849ef227b4cd59c6046e2cadab6f323bbbe8 100644 --- a/ee/app/assets/javascripts/work_items/components/work_item_weight.vue +++ b/ee/app/assets/javascripts/work_items/components/work_item_weight.vue @@ -7,7 +7,9 @@ import { I18N_WORK_ITEM_ERROR_UPDATING, TRACKING_CATEGORY_SHOW, } from '~/work_items/constants'; +import updateNewWorkItemMutation from '~/work_items/graphql/update_new_work_item.mutation.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import { newWorkItemId } from '~/work_items/utils'; export default { inputId: 'weight-widget-input', @@ -28,10 +30,13 @@ export default { required: false, default: false, }, - weight: { - type: Number, - required: false, - default: null, + fullPath: { + type: String, + required: true, + }, + widget: { + type: Object, + required: true, }, workItemId: { type: String, @@ -55,6 +60,9 @@ export default { }; }, computed: { + weight() { + return this.widget.weight; + }, hasWeight() { return this.weight !== null; }, @@ -68,6 +76,16 @@ export default { property: `type_${this.workItemType}`, }; }, + createFlow() { + return this.workItemId === newWorkItemId(this.workItemType); + }, + isWorkItemWidgetAvailable() { + // `editable` means if it is available for that work item type (not related to user permission) + return this.widget?.widgetDefinition?.editable; + }, + displayWeightWidget() { + return this.hasIssueWeightsFeature && this.isWorkItemWidgetAvailable; + }, }, methods: { blurInput() { @@ -97,6 +115,24 @@ export default { this.isUpdating = true; this.track('updated_weight'); + + if (this.createFlow) { + this.$apollo.mutate({ + mutation: updateNewWorkItemMutation, + variables: { + input: { + workItemType: this.workItemType, + fullPath: this.fullPath, + weight, + }, + }, + }); + + this.isUpdating = false; + this.isEditing = false; + return; + } + this.$apollo .mutate({ mutation: updateWorkItemMutation, @@ -129,7 +165,7 @@ export default { </script> <template> - <div v-if="hasIssueWeightsFeature" data-testid="work-item-weight"> + <div v-if="displayWeightWidget" data-testid="work-item-weight"> <div class="gl-flex gl-items-center gl-justify-between"> <!-- hide header when editing, since we then have a form label. Keep it reachable for screenreader nav --> <h3 :class="{ 'gl-sr-only': isEditing }" class="gl-heading-5 !gl-mb-0"> diff --git a/ee/spec/frontend/work_items/components/create_work_item_spec.js b/ee/spec/frontend/work_items/components/create_work_item_spec.js index b5a7488e9f7e03272fdbd50cb4b1bf6ab16984e2..344bd4887700f67535dd0baf3b60f7506e2611aa 100644 --- a/ee/spec/frontend/work_items/components/create_work_item_spec.js +++ b/ee/spec/frontend/work_items/components/create_work_item_spec.js @@ -9,6 +9,7 @@ import CreateWorkItem from '~/work_items/components/create_work_item.vue'; import WorkItemHealthStatus from 'ee/work_items/components/work_item_health_status.vue'; import WorkItemColor from 'ee/work_items/components/work_item_color.vue'; import WorkItemIteration from 'ee/work_items/components/work_item_iteration.vue'; +import WorkItemWeight from 'ee/work_items/components/work_item_weight.vue'; import WorkItemRolledupDates from 'ee/work_items/components/work_item_rolledup_dates.vue'; import { WORK_ITEM_TYPE_ENUM_EPIC, WORK_ITEM_TYPE_ENUM_ISSUE } from '~/work_items/constants'; import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql'; @@ -22,7 +23,7 @@ import { Vue.use(VueApollo); -describe('Create work item component', () => { +describe('EE Create work item component', () => { let wrapper; let mockApollo; const workItemTypeEpicId = @@ -43,6 +44,7 @@ describe('Create work item component', () => { const findHealthStatusWidget = () => wrapper.findComponent(WorkItemHealthStatus); const findIterationWidget = () => wrapper.findComponent(WorkItemIteration); + const findWeightWidget = () => wrapper.findComponent(WorkItemWeight); const findColorWidget = () => wrapper.findComponent(WorkItemColor); const findRolledupDatesWidget = () => wrapper.findComponent(WorkItemRolledupDates); const findSelect = () => wrapper.findComponent(GlFormSelect); @@ -85,6 +87,7 @@ describe('Create work item component', () => { fullPath: 'full-path', hasIssuableHealthStatusFeature: false, hasIterationsFeature: true, + hasIssueWeightsFeature: true, }, }); }; @@ -116,7 +119,7 @@ describe('Create work item component', () => { gon.current_user_avatar_url = mockCurrentUser.avatar_url; }); - describe('Create work item widgets for epic work item type', () => { + describe('Create work item widgets for Epic work item type', () => { beforeEach(async () => { await initialiseComponentAndSelectWorkItem(); }); @@ -142,8 +145,16 @@ describe('Create work item component', () => { }); }); + it('renders the work item health status widget', () => { + expect(findHealthStatusWidget().exists()).toBe(true); + }); + it('renders the work item iteration widget', () => { expect(findIterationWidget().exists()).toBe(true); }); + + it('renders the work item weight widget', () => { + expect(findWeightWidget().exists()).toBe(true); + }); }); }); diff --git a/ee/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/ee/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js index 889ed8885e3505fca5c9003072df101cf5570fc1..fbff21ba2e97aae150811ae5247bdfc751751525 100644 --- a/ee/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js +++ b/ee/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js @@ -12,7 +12,6 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { workItemResponseFactory, epicType, - issueType, mockParticipantWidget, } from 'jest/work_items/mock_data'; import WorkItemParent from '~/work_items/components/work_item_parent.vue'; @@ -120,42 +119,23 @@ describe('EE WorkItemAttributesWrapper component', () => { }); describe('weight widget', () => { - describe.each` - description | editableWeightWidget | weightWidgetPresent | exists - ${'when widget is returned from API'} | ${true} | ${true} | ${true} - ${'when widget is not returned from API'} | ${true} | ${false} | ${false} - ${'when widget is returned from API'} | ${false} | ${true} | ${false} - ${'when widget is not returned from API'} | ${false} | ${false} | ${false} - `('$description', ({ weightWidgetPresent, exists, editableWeightWidget }) => { - it(`when the weight widget is ${editableWeightWidget ? 'editable' : 'not editable'} ${weightWidgetPresent && editableWeightWidget ? 'renders' : 'does not render'} weight component`, async () => { - const response = workItemResponseFactory({ weightWidgetPresent, editableWeightWidget }); - createComponent({ workItem: response.data.workItem }); + it('allows widget to render if it exists', async () => { + const response = workItemResponseFactory({ weightWidgetPresent: true }); + createComponent({ workItem: response.data.workItem }); - await waitForPromises(); + await waitForPromises(); - expect(findWorkItemWeight().exists()).toBe(exists); - }); + expect(findWorkItemWeight().exists()).toBe(true); }); - it.each` - workItemType | typeName | description | expected | editableWeightWidget - ${epicType} | ${`Epic`} | ${'does not render'} | ${false} | ${false} - ${issueType} | ${`Issue`} | ${'renders'} | ${true} | ${true} - `( - '$description WorkItemWeight when workItemType is $typeName', - async ({ workItemType, expected, editableWeightWidget }) => { - const response = workItemResponseFactory({ - weightWidgetPresent: true, - workItemType, - editableWeightWidget, - }); - createComponent({ workItem: response.data.workItem }); + it('hides widget if data doesn"t exist', async () => { + const response = workItemResponseFactory({ weightWidgetPresent: false }); + createComponent({ workItem: response.data.workItem }); - await waitForPromises(); + await waitForPromises(); - expect(findWorkItemWeight().exists()).toBe(expected); - }, - ); + expect(findWorkItemWeight().exists()).toBe(false); + }); it('emits an error event to the wrapper', async () => { const response = workItemResponseFactory({ weightWidgetPresent: true }); diff --git a/ee/spec/frontend/work_items/components/work_item_weight_spec.js b/ee/spec/frontend/work_items/components/work_item_weight_spec.js index a06cfbac856f3027d8a377fb92386130e479d0d8..2dbd70ded2a23ec08367c7f0cdf13847ffd470fd 100644 --- a/ee/spec/frontend/work_items/components/work_item_weight_spec.js +++ b/ee/spec/frontend/work_items/components/work_item_weight_spec.js @@ -16,7 +16,7 @@ describe('WorkItemWeight component', () => { let wrapper; const workItemId = 'gid://gitlab/WorkItem/1'; - const workItemType = 'Task'; + const defaultWorkItemType = 'Task'; const findHeader = () => wrapper.find('h3'); const findEditButton = () => wrapper.find('[data-testid="edit-weight"]'); @@ -29,17 +29,24 @@ describe('WorkItemWeight component', () => { const createComponent = ({ canUpdate = true, + fullPath = 'gitlab-org/gitlab', hasIssueWeightsFeature = true, isEditing = false, - weight, + weight = null, + editable = true, workItemIid = '1', + workItemType = defaultWorkItemType, mutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse), } = {}) => { wrapper = mountExtended(WorkItemWeight, { apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]), propsData: { canUpdate, - weight, + fullPath, + widget: { + weight, + widgetDefinition: { editable }, + }, workItemId, workItemIid, workItemType, @@ -54,13 +61,25 @@ describe('WorkItemWeight component', () => { } }; - it('renders nothing if license not available', async () => { - createComponent({ hasIssueWeightsFeature: false }); + describe('rendering widget', () => { + it('renders nothing if license not available', async () => { + createComponent({ hasIssueWeightsFeature: false }); + + await nextTick(); + + expect(findHeader().exists()).toBe(false); + expect(findForm().exists()).toBe(false); + }); + + // 'editable' property means if it's available for that work item type + it('renders nothing if not editable', async () => { + createComponent({ editable: false }); - await nextTick(); + await nextTick(); - expect(findHeader().exists()).toBe(false); - expect(findForm().exists()).toBe(false); + expect(findHeader().exists()).toBe(false); + expect(findForm().exists()).toBe(false); + }); }); describe('label', () => { diff --git a/ee/spec/frontend/work_items/graphql/resolvers_spec.js b/ee/spec/frontend/work_items/graphql/resolvers_spec.js index 13ef325bf1ca8db74bc53dedab534489ab7722d4..b1b58f9ccccefcf5ad6cfdea8204a49106e85784 100644 --- a/ee/spec/frontend/work_items/graphql/resolvers_spec.js +++ b/ee/spec/frontend/work_items/graphql/resolvers_spec.js @@ -7,10 +7,11 @@ import { WIDGET_TYPE_ROLLEDUP_DATES, WIDGET_TYPE_HEALTH_STATUS, WIDGET_TYPE_ITERATION, + WIDGET_TYPE_WEIGHT, } from '~/work_items/constants'; import { createWorkItemQueryResponse } from 'jest/work_items/mock_data'; -describe('work items graphql resolvers', () => { +describe('EE work items graphql resolvers', () => { describe('updateNewWorkItemCache', () => { let mockApolloClient; @@ -132,5 +133,35 @@ describe('work items graphql resolvers', () => { }); }); }); + + describe('with weight input', () => { + it('updates weight if value is a number', async () => { + await mutate({ weight: 2 }); + + const queryResult = await query(WIDGET_TYPE_WEIGHT); + expect(queryResult).toMatchObject({ weight: 2 }); + }); + + it('updates weight if value is number 0', async () => { + await mutate({ weight: 0 }); + + const queryResult = await query(WIDGET_TYPE_WEIGHT); + expect(queryResult).toMatchObject({ weight: 0 }); + }); + + it('does not update weight if value is not changed/undefined', async () => { + await mutate({ weight: undefined }); + + const queryResult = await query(WIDGET_TYPE_WEIGHT); + expect(queryResult).toMatchObject({ weight: 2 }); + }); + + it('updates weight if cleared', async () => { + await mutate({ weight: null }); + + const queryResult = await query(WIDGET_TYPE_WEIGHT); + expect(queryResult).toMatchObject({ weight: null }); + }); + }); }); }); diff --git a/spec/frontend/work_items/components/create_work_item_spec.js b/spec/frontend/work_items/components/create_work_item_spec.js index 3ae0a1c60dde812c70583dffe27b3f4429695ed5..9820118e725da9c8e99ff5625d2fdf9fd24fb215 100644 --- a/spec/frontend/work_items/components/create_work_item_spec.js +++ b/spec/frontend/work_items/components/create_work_item_spec.js @@ -119,6 +119,7 @@ describe('Create work item component', () => { fullPath: 'full-path', hasIssuableHealthStatusFeature: false, hasIterationsFeature: true, + hasIssueWeightsFeature: false, }, }); }; diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index d1e7b21a0e0d96f7550e287bd69a48aa687338c0..c0495159b52478bab39a7f8e2807191c07d09434 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -1209,6 +1209,7 @@ export const workItemResponseFactory = ({ hasParent = false, healthStatus = 'onTrack', rolledUpHealthStatus = [], + weight = null, rolledUpWeight = 0, rolledUpCompletedWeight = 0, descriptionText = 'some **great** text', @@ -1302,7 +1303,7 @@ export const workItemResponseFactory = ({ weightWidgetPresent ? { type: 'WEIGHT', - weight: null, + weight, rolledUpWeight, rolledUpCompletedWeight, widgetDefinition: { @@ -5192,6 +5193,18 @@ export const createWorkItemQueryResponse = { }, __typename: 'WorkItemWidgetCrmContacts', }, + { + type: 'WEIGHT', + weight: 2, + rolledUpWeight: 0, + rolledUpCompletedWeight: 0, + widgetDefinition: { + editable: true, + rollUp: false, + __typename: 'WorkItemWidgetDefinitionWeight', + }, + __typename: 'WorkItemWidgetWeight', + }, ], __typename: 'WorkItem', },