diff --git a/.eslintrc.yml b/.eslintrc.yml index 56fc7a15cbd..5f128c1c8d0 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -244,3 +244,11 @@ overrides: - 'ee/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql' rules: '@graphql-eslint/require-id-when-available': off + - files: + - '{,spec/}tooling/**/*' + rules: + 'no-undef': off + 'import/no-commonjs': off + 'import/no-extraneous-dependencies': off + 'no-restricted-syntax': off + '@gitlab/require-i18n-strings': off diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index 3033ce8c584..e8ec0781bc3 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -2078,7 +2078,6 @@ RSpec/ContextWording: - 'spec/models/packages/dependency_spec.rb' - 'spec/models/packages/package_file_spec.rb' - 'spec/models/packages/package_spec.rb' - - 'spec/models/pages_domain_spec.rb' - 'spec/models/personal_access_token_spec.rb' - 'spec/models/plan_limits_spec.rb' - 'spec/models/preloaders/labels_preloader_spec.rb' diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml index 617d3398afd..a4d20610cc2 100644 --- a/.rubocop_todo/rspec/named_subject.yml +++ b/.rubocop_todo/rspec/named_subject.yml @@ -2648,7 +2648,6 @@ RSpec/NamedSubject: - 'spec/models/packages/package_file_spec.rb' - 'spec/models/packages/package_spec.rb' - 'spec/models/packages/rpm/repository_file_spec.rb' - - 'spec/models/pages_domain_spec.rb' - 'spec/models/personal_access_token_spec.rb' - 'spec/models/plan_limits_spec.rb' - 'spec/models/project_authorization_spec.rb' diff --git a/.rubocop_todo/rspec/return_from_stub.yml b/.rubocop_todo/rspec/return_from_stub.yml index 3d94a601dfc..343f03e36e4 100644 --- a/.rubocop_todo/rspec/return_from_stub.yml +++ b/.rubocop_todo/rspec/return_from_stub.yml @@ -143,7 +143,6 @@ RSpec/ReturnFromStub: - 'spec/models/internal_id_spec.rb' - 'spec/models/issue_spec.rb' - 'spec/models/merge_request_spec.rb' - - 'spec/models/pages_domain_spec.rb' - 'spec/models/project_spec.rb' - 'spec/models/project_statistics_spec.rb' - 'spec/models/snippet_statistics_spec.rb' diff --git a/.stylelintrc b/.stylelintrc index ac64963959c..a2fc8bb15f1 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,18 +1,32 @@ { - "extends": ["@gitlab/stylelint-config"], - "ignoreFiles": [ - "app/assets/stylesheets/pages/emojis.scss", - "app/assets/stylesheets/startup/startup-*.scss", - "ee/app/assets/stylesheets/startup/startup-*.scss", - "app/assets/stylesheets/highlight/themes/*.scss", - "app/assets/stylesheets/lazy_bundles/cropper.css" - ], - "overrides": [ + "extends": ["@gitlab/stylelint-config"], + "plugins": ["./tooling/stylelint/gitlab_no_gl_class.plugin.js"], + "rules": { + "gitlab/no-gl-class": true + }, + "ignoreFiles": [ + "app/assets/stylesheets/pages/emojis.scss", + "app/assets/stylesheets/startup/startup-*.scss", + "ee/app/assets/stylesheets/startup/startup-*.scss", + "app/assets/stylesheets/highlight/themes/*.scss", + "app/assets/stylesheets/lazy_bundles/cropper.css" + ], + "overrides": [ { "files": ["app/assets/stylesheets/mailers/mailer.scss"], "rules": { "color-hex-length": "long" } + }, + { + "files": [ + "app/assets/stylesheets/framework/**/*.scss", + "app/assets/stylesheets/themes/dark_mode_overrides.scss", + "app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss" + ], + "rules": { + "gitlab/no-gl-class": null + } } - ] + ] } diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index b3fe519572b..ee403046d61 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -5,18 +5,24 @@ import produce from 'immer'; import Draggable from 'vuedraggable'; import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue'; import { s__ } from '~/locale'; import { defaultSortableOptions, DRAG_DELAY } from '~/sortable/constants'; +import { mapWorkItemWidgetsToIssueFields } from '~/issues/list/utils'; import { DraggableItemTypes, flashAnimationDuration, listsQuery, updateListQueries, ListType, + listIssuablesQueries, + DEFAULT_BOARD_LIST_ITEMS_SIZE, } from 'ee_else_ce/boards/constants'; import { calculateNewPosition } from 'ee_else_ce/boards/boards_util'; import { setError } from '../graphql/cache_updates'; import BoardColumn from './board_column.vue'; +import BoardDrawerWrapper from './board_drawer_wrapper.vue'; export default { draggableItemTypes: DraggableItemTypes, @@ -24,13 +30,25 @@ export default { BoardAddNewColumn, BoardAddNewColumnTrigger, BoardColumn, + BoardDrawerWrapper, BoardContentSidebar: () => import('~/boards/components/board_content_sidebar.vue'), EpicBoardContentSidebar: () => import('ee_component/boards/components/epic_board_content_sidebar.vue'), EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'), GlAlert, + WorkItemDrawer, }, - inject: ['boardType', 'canAdminList', 'isIssueBoard', 'isEpicBoard', 'disabled', 'issuableType'], + mixins: [glFeatureFlagsMixin()], + inject: [ + 'boardType', + 'canAdminList', + 'isIssueBoard', + 'isEpicBoard', + 'disabled', + 'issuableType', + 'isGroupBoard', + 'fullPath', + ], props: { boardId: { type: String, @@ -108,6 +126,9 @@ export default { const closedList = this.boardListsToUse.find((list) => list.listType === ListType.closed); return closedList?.id || ''; }, + issuesDrawerEnabled() { + return this.glFeatures.issuesListDrawer; + }, }, methods: { afterFormEnters() { @@ -193,6 +214,24 @@ export default { }); } }, + updateBoardCard(workItem, activeCard) { + const { cache } = this.$apollo.provider.clients.defaultClient; + + const variables = { + id: activeCard.listId, + filters: this.filterParams, + fullPath: this.fullPath, + boardId: this.boardId, + isGroup: this.isGroupBoard, + isProject: !this.isGroupBoard, + first: DEFAULT_BOARD_LIST_ITEMS_SIZE, + }; + + cache.updateQuery( + { query: listIssuablesQueries[this.issuableType].query, variables }, + (boardList) => mapWorkItemWidgetsToIssueFields(boardList, workItem, true), + ); + }, }, }; @@ -283,9 +322,34 @@ export default { /> + + + +import { union } from 'lodash'; +import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql'; +import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql'; +import { TYPE_ISSUE } from '~/issues/constants'; +import { ListType } from 'ee_else_ce/boards/constants'; +import { identifyAffectedLists } from '../graphql/cache_updates'; + +export default { + name: 'BoardDrawerWrapper', + inject: { + issuableType: { + default: TYPE_ISSUE, + }, + }, + props: { + backlogListId: { + type: String, + required: true, + }, + closedListId: { + type: String, + required: true, + }, + }, + data() { + return { + affectedListTypes: [], + updatedAttributeIds: [], + }; + }, + apollo: { + activeBoardItem: { + query: activeBoardItemQuery, + variables: { + isIssue: true, + }, + }, + }, + computed: { + apolloClient() { + return this.$apollo.getClient(); + }, + }, + methods: { + async onDrawerClosed() { + const item = this.activeBoardItem; + + await this.$apollo.mutate({ + mutation: setActiveBoardItemMutation, + variables: { + boardItem: null, + listId: null, + }, + }); + + if (item.listId !== this.closedListId || this.affectedListTypes.includes(ListType.closed)) { + await this.refetchAffectedLists(item); + } + this.affectedListTypes = []; + this.updatedAttributeIds = []; + }, + onAttributeUpdated({ ids, type }) { + if (!this.affectedListTypes.includes(type)) { + this.affectedListTypes.push(type); + } + this.updatedAttributeIds = union(this.updatedAttributeIds, ids); + }, + refetchAffectedLists(item) { + if (!this.affectedListTypes.length) { + return; + } + + const affectedLists = identifyAffectedLists({ + client: this.apolloClient, + item, + issuableType: TYPE_ISSUE, + affectedListTypes: this.affectedListTypes, + updatedAttributeIds: this.updatedAttributeIds, + }); + + if (this.backlogListId && !affectedLists.includes(this.backlogListId)) { + affectedLists.push(this.backlogListId); + } + + if (this.closedListId && this.affectedListTypes.includes(ListType.closed)) { + affectedLists.push(this.closedListId); + } + + this.refetchActiveIssuableLists(item); + + this.apolloClient.refetchQueries({ + updateCache(cache) { + affectedLists.forEach((listId) => { + cache.evict({ + id: cache.identify({ + __typename: 'BoardList', + id: listId, + }), + fieldName: 'issues', + }); + cache.evict({ + id: cache.identify({ + __typename: 'BoardList', + id: listId, + }), + fieldName: 'issuesCount', + }); + }); + }, + }); + }, + refetchActiveIssuableLists(item) { + this.apolloClient.refetchQueries({ + updateCache(cache) { + cache.evict({ id: cache.identify(item) }); + }, + }); + }, + onStateUpdated() { + this.affectedListTypes.push(ListType.closed); + }, + }, + render() { + return this.$scopedSlots.default({ + activeIssuable: this.activeBoardItem, + onDrawerClosed: this.onDrawerClosed, + onAttributeUpdated: this.onAttributeUpdated, + onIssuableDeleted: this.refetchActiveIssuableLists, + onStateUpdated: this.onStateUpdated, + }); + }, +}; + diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 01cf520f518..4ae4d619774 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -22,7 +22,15 @@ const apolloProvider = new VueApollo({ }); function mountBoardApp(el) { - const { boardId, groupId, fullPath, rootPath, hasScopedLabelsFeature } = el.dataset; + const { + boardId, + groupId, + fullPath, + rootPath, + hasScopedLabelsFeature, + wiGroupPath, + wiCanAdminLabel, + } = el.dataset; const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true }); @@ -43,9 +51,11 @@ function mountBoardApp(el) { groupId: Number(groupId), rootPath, fullPath, + groupPath: wiGroupPath, initialFilterParams, boardBaseUrl: el.dataset.boardBaseUrl, boardType, + isGroup: boardType === WORKSPACE_GROUP, isGroupBoard: boardType === WORKSPACE_GROUP, isProjectBoard: boardType === WORKSPACE_PROJECT, currentUserId: gon.current_user_id || null, @@ -60,6 +70,8 @@ function mountBoardApp(el) { weights: el.dataset.weights ? JSON.parse(el.dataset.weights) : [], isIssueBoard: true, isEpicBoard: false, + reportAbusePath: el.dataset.wiReportAbusePath, + issuesListPath: el.dataset.wiIssuesListPath, // Permissions canUpdate: parseBoolean(el.dataset.canUpdate), canAdminList: parseBoolean(el.dataset.canAdminList), @@ -67,6 +79,7 @@ function mountBoardApp(el) { allowLabelCreate: parseBoolean(el.dataset.canUpdate), allowLabelEdit: parseBoolean(el.dataset.canUpdate), isSignedIn: isLoggedIn(), + canAdminLabel: parseBoolean(wiCanAdminLabel), // Features multipleAssigneesFeatureAvailable: parseBoolean(el.dataset.multipleAssigneesFeatureAvailable), epicFeatureAvailable: parseBoolean(el.dataset.epicFeatureAvailable), @@ -83,6 +96,10 @@ function mountBoardApp(el) { scopedIssueBoardFeatureEnabled: parseBoolean(el.dataset.scopedIssueBoardFeatureEnabled), allowSubEpics: false, hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature), + hasIterationsFeature: parseBoolean(el.dataset.iterationFeatureAvailable), + hasIssueWeightsFeature: parseBoolean(el.dataset.weightFeatureAvailable), + hasIssuableHealthStatusFeature: parseBoolean(el.dataset.healthStatusFeatureAvailable), + hasSubepicsFeature: parseBoolean(el.dataset.subEpicsFeatureAvailable), }, render: (createComponent) => createComponent(BoardApp), }); 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 efeebdb5b89..6ec8b90aebd 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -70,7 +70,6 @@ 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 deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql'; import { WORK_ITEM_TYPE_ENUM_OBJECTIVE } from '~/work_items/constants'; import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue'; import { @@ -864,23 +863,9 @@ export default { 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 = __('An error occurred while deleting an issuable.'); - Sentry.captureException(error); - }); + deleteIssuable() { + this.activeIssuable = null; + this.refetchIssuables(); }, updateIssuableEmojis(workItem) { const client = this.$apollo.provider.clients.defaultClient; @@ -907,7 +892,8 @@ export default { @work-item-updated="updateIssuablesCache" @work-item-emoji-updated="updateIssuableEmojis" @addChild="refetchIssuables" - @deleteWorkItem="deleteIssuable" + @deleteWorkItemError="issuesError = __('An error occurred while deleting an issuable.')" + @workItemDeleted="deleteIssuable" @promotedToObjective="promoteToObjective" /> widget.type === type); } -export function mapWorkItemWidgetsToIssueFields(issuesList, workItem) { +export function mapWorkItemWidgetsToIssueFields(issuesList, workItem, isBoard = false) { return produce(issuesList, (draftData) => { - const activeItem = draftData.project.issues.nodes.find((issue) => issue.iid === workItem.iid); + const activeList = isBoard + ? draftData.project.board.lists.nodes[0].issues.nodes + : draftData.project.issues.nodes; + + const activeItem = activeList.find((issue) => issue.iid === workItem.iid); Object.keys(WORK_ITEM_TO_ISSUE_MAP).forEach((type) => { const currentWidget = findWidget(type, workItem); diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue index f08953648fb..e50ce175782 100644 --- a/app/assets/javascripts/work_items/components/work_item_actions.vue +++ b/app/assets/javascripts/work_items/components/work_item_actions.vue @@ -86,7 +86,8 @@ export default { }, workItemState: { type: String, - required: true, + required: false, + default: null, }, workItemId: { type: String, @@ -403,6 +404,7 @@ export default { :full-path="fullPath" show-as-dropdown-item @error="emitStateToggleError" + @workItemStateUpdated="$emit('workItemStateUpdated')" /> import Participants from '~/sidebar/components/participants/participants.vue'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { ListType } from '~/boards/constants'; import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_HEALTH_STATUS, @@ -27,6 +28,7 @@ import WorkItemTimeTracking from './work_item_time_tracking.vue'; import WorkItemDevelopment from './work_item_development/work_item_development.vue'; export default { + ListType, components: { Participants, WorkItemLabels, @@ -159,6 +161,9 @@ export default { :work-item-type="workItemType" :can-invite-members="workItemAssignees.canInviteMembers" @error="$emit('error', $event)" + @assigneesUpdated=" + $emit('attributesUpdated', { type: $options.ListType.assignee, ids: $event }) + " />