From aaf419222d28a097f38d37440e3e3acf02a20805 Mon Sep 17 00:00:00 2001 From: NataliaTepluhina Date: Wed, 3 May 2023 09:10:49 +0200 Subject: [PATCH] Added issuable drawer - added feature flag to display the drawer - implemented work item detail view in the drawer - fixed UX issues Removed an unused query Fixed drawer height Replaced hardcoded string with a constant Added early return Simplified persisted fields check Simplified issuable iid check Change the milestone in FF Added comment about refetching issuables Change class to use isActive Changed FF to prop Moved addCild event Passed prevent redirect prop down Apply 1 suggestion(s) to 1 file(s) Remove unnecessary provide Apply 1 suggestion(s) to 1 file(s) Updated a snapshot --- .../list/components/issues_list_app.vue | 106 +++++++++ .../javascripts/issues/list/constants.js | 1 + app/assets/javascripts/issues/list/graphql.js | 5 +- app/assets/javascripts/issues/list/index.js | 5 + app/assets/javascripts/issues/list/utils.js | 66 ++++++ .../lib/apollo/persistence_mapper.js | 2 +- .../list/components/issuable_item.vue | 22 +- .../list/components/issuable_list_root.vue | 16 ++ .../components/work_item_actions.vue | 1 + .../components/work_item_detail.vue | 4 + .../work_item_links/work_item_links_form.vue | 1 + .../work_item_links/work_item_tree.vue | 1 + .../javascripts/work_items/constants.js | 10 + .../graphql/milestone.fragment.graphql | 1 + .../graphql/work_item_by_iid.query.graphql | 2 +- .../stylesheets/page_bundles/issues_list.scss | 10 + app/controllers/projects/issues_controller.rb | 1 + app/helpers/issues_helper.rb | 4 +- .../development/issues_list_drawer.yml | 8 + .../external_issues_list_root_spec.js.snap | 2 + locale/gitlab.pot | 6 + .../list/components/issues_list_app_spec.js | 223 +++++++++++++++++- .../list/components/issuable_item_spec.js | 37 +++ .../components/issuable_list_root_spec.js | 24 ++ .../components/work_item_actions_spec.js | 1 + .../work_item_links_form_spec.js | 3 +- .../work_item_links/work_item_tree_spec.js | 10 + spec/frontend/work_items/mock_data.js | 7 +- 28 files changed, 569 insertions(+), 10 deletions(-) create mode 100644 config/feature_flags/development/issues_list_drawer.yml diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index f7693dd7102b8e..9372618fb2a62c 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -6,8 +6,12 @@ import { GlDisclosureDropdownGroup, GlFilteredSearchToken, GlTooltipDirective, + GlDrawer, + GlLink, } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; + +import produce from 'immer'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; import { isEmpty } from 'lodash'; import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue'; @@ -63,6 +67,9 @@ import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_ro import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue'; +import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; +import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql'; +import { WORK_ITEM_TYPE_ENUM_OBJECTIVE } from '~/work_items/constants'; import { CREATED_DESC, defaultTypeTokenOptions, @@ -98,6 +105,7 @@ import { getSortKey, getSortOptions, isSortKey, + mapWorkItemWidgetsToIssueFields, } from '../utils'; import { hasNewIssueDropdown } from '../has_new_issue_dropdown_mixin'; import EmptyStateWithAnyIssues from './empty_state_with_any_issues.vue'; @@ -131,12 +139,15 @@ export default { EmptyStateWithoutAnyIssues, GlButton, GlButtonGroup, + GlDrawer, IssuableByEmail, IssuableList, IssueCardStatistics, IssueCardTimeInfo, NewResourceDropdown, LocalStorageSync, + WorkItemDetail, + GlLink, }, directives: { GlTooltip: GlTooltipDirective, @@ -218,6 +229,7 @@ export default { }, ], }, + activeIssuable: null, }; }, apollo: { @@ -230,6 +242,7 @@ export default { return data[this.namespace]?.issues.nodes ?? []; }, fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, + nextFetchPolicy: fetchPolicies.CACHE_FIRST, // We need this for handling loading state when using frontend cache // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106004#note_1217325202 for details notifyOnNetworkStatusChange: true, @@ -536,6 +549,12 @@ export default { isGridView() { return this.viewType === ISSUES_GRID_VIEW_KEY; }, + isIssuableSelected() { + return !isEmpty(this.activeIssuable); + }, + issuesDrawerEnabled() { + return this.glFeatures?.issuesListDrawer; + }, }, watch: { $route(newValue, oldValue) { @@ -805,12 +824,96 @@ export default { // The default view is list view this.viewType = ISSUES_LIST_VIEW_KEY; }, + handleSelectIssuable(issuable) { + this.activeIssuable = issuable; + }, + updateIssuablesCache(workItem) { + const client = this.$apollo.provider.clients.defaultClient; + const issuesList = client.readQuery({ + query: getIssuesQuery, + variables: this.queryVariables, + }); + + const activeIssuable = issuesList.project.issues.nodes.find( + (issue) => issue.iid === workItem.iid, + ); + + // when we change issuable state, it's moved to a different tab + // to ensure that we show 20 items of the first page, we need to refetch issuables + if (!activeIssuable.state.includes(workItem.state.toLowerCase())) { + this.refetchIssuables(); + return; + } + + // handle all other widgets + const data = mapWorkItemWidgetsToIssueFields(issuesList, workItem); + + client.writeQuery({ query: getIssuesQuery, variables: this.queryVariables, data }); + }, + promoteToObjective(workItemIid) { + const { cache } = this.$apollo.provider.clients.defaultClient; + + cache.updateQuery({ query: getIssuesQuery, variables: this.queryVariables }, (issuesList) => + produce(issuesList, (draftData) => { + const activeItem = draftData.project.issues.nodes.find( + (issue) => issue.iid === workItemIid, + ); + + activeItem.type = WORK_ITEM_TYPE_ENUM_OBJECTIVE; + }), + ); + }, + refetchIssuables() { + this.$apollo.queries.issues.refetch(); + this.$apollo.queries.issuesCounts.refetch(); + }, + deleteIssuable({ workItemId }) { + this.$apollo + .mutate({ + mutation: deleteWorkItemMutation, + variables: { input: { id: workItemId } }, + }) + .then(({ data }) => { + if (data.workItemDelete.errors?.length) { + throw new Error(data.workItemDelete.errors[0]); + } + this.activeIssuable = null; + this.refetchIssuables(); + }) + .catch((error) => { + this.issuesError = this.$options.i18n.deleteError; + Sentry.captureException(error); + }); + }, }, };