From 839e879bcf197a283da8481ddcb15b177172784d Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 17 Feb 2021 09:09:36 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../components/board_assignee_dropdown.vue | 202 --------- .../boards/components/board_sidebar.js | 48 +-- app/assets/javascripts/boards/index.js | 2 +- app/assets/javascripts/boards/models/issue.js | 4 + .../javascripts/boards/stores/actions.js | 37 +- .../javascripts/boards/stores/boards_store.js | 4 + .../design_management/pages/index.vue | 13 +- .../queries/issue_sidebar.query.graphql | 1 + .../components/assignees/assignees.vue | 15 +- .../assignees/issuable_assignees.vue | 10 +- .../assignees/sidebar_assignees.vue | 14 +- .../assignees/sidebar_assignees_widget.vue | 403 ++++++++++++++++++ .../components/reviewers/reviewers.vue | 2 +- .../components/sidebar_editable_item.vue | 95 +++++ app/assets/javascripts/sidebar/constants.js | 16 + .../javascripts/sidebar/mount_sidebar.js | 24 ++ app/assets/javascripts/users_select/index.js | 7 +- .../sidebar/multiselect_dropdown.vue | 2 +- .../getIssueParticipants.query.graphql | 13 - .../get_issue_participants.query.graphql | 19 + .../queries/get_mr_participants.query.graphql | 19 + ...> update_issue_assignees.mutation.graphql} | 18 +- .../update_mr_assignees.mutation.graphql | 21 + .../projects/services_controller.rb | 2 +- app/helpers/boards_helper.rb | 17 - .../merge_requests/post_merge_service.rb | 31 ++ app/services/merge_requests/update_service.rb | 16 +- app/services/system_note_service.rb | 11 +- .../system_notes/merge_requests_service.rb | 20 +- .../components/sidebar/_assignee.html.haml | 36 +- .../issuable/_sidebar_assignees.html.haml | 2 +- ...2-display-busy-status-in-issue-sidebar.yml | 5 + ...rt-the-assignees-feature-into-a-widget.yml | 5 + .../fix-overflowing-design-buttons.yml | 5 + changelogs/unreleased/retarget-branch.yml | 5 + .../development/jira_for_vulnerabilities.yml | 2 +- .../development/retarget_merge_requests.yml | 8 + doc/.vale/gitlab/spelling-exceptions.txt | 4 + doc/administration/instance_limits.md | 8 + .../create_issue_from_vulnerability_v13_3.png | Bin 5079 -> 0 bytes doc/user/application_security/img/issue.png | Bin 4780 -> 0 bytes doc/user/application_security/index.md | 27 +- .../vulnerabilities/index.md | 105 +++-- .../vulnerability_report/index.md | 12 +- doc/user/project/integrations/jira.md | 60 ++- .../project/integrations/jira_integrations.md | 1 + .../project/merge_requests/getting_started.md | 46 ++ locale/gitlab.pot | 8 +- spec/features/boards/sidebar_spec.rb | 36 +- .../profiles/user_edit_profile_spec.rb | 32 ++ .../board_assignee_dropdown_spec.js | 377 ---------------- spec/frontend/boards/stores/actions_spec.js | 42 +- .../assignees/sidebar_editable_item_spec.js | 120 ++++++ .../merge_requests/post_merge_service_spec.rb | 142 +++++- .../merge_requests/refresh_service_spec.rb | 41 +- .../merge_requests/update_service_spec.rb | 27 ++ spec/services/system_note_service_spec.rb | 11 +- .../merge_requests_service_spec.rb | 28 +- 58 files changed, 1358 insertions(+), 923 deletions(-) delete mode 100644 app/assets/javascripts/boards/components/board_assignee_dropdown.vue create mode 100644 app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue create mode 100644 app/assets/javascripts/sidebar/components/sidebar_editable_item.vue create mode 100644 app/assets/javascripts/sidebar/constants.js delete mode 100644 app/assets/javascripts/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql rename app/assets/javascripts/vue_shared/components/sidebar/queries/{updateAssignees.mutation.graphql => update_issue_assignees.mutation.graphql} (52%) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql create mode 100644 changelogs/unreleased/263452-display-busy-status-in-issue-sidebar.yml create mode 100644 changelogs/unreleased/292035-convert-the-assignees-feature-into-a-widget.yml create mode 100644 changelogs/unreleased/fix-overflowing-design-buttons.yml create mode 100644 changelogs/unreleased/retarget-branch.yml create mode 100644 config/feature_flags/development/retarget_merge_requests.yml delete mode 100644 doc/user/application_security/img/create_issue_from_vulnerability_v13_3.png delete mode 100644 doc/user/application_security/img/issue.png delete mode 100644 spec/frontend/boards/components/board_assignee_dropdown_spec.js create mode 100644 spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue deleted file mode 100644 index 59d91f4ae72..00000000000 --- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue +++ /dev/null @@ -1,202 +0,0 @@ - - - diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index f303c45a40e..6d5a13be3ac 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -7,7 +7,6 @@ import { GlLabel } from '@gitlab/ui'; import $ from 'jquery'; import Vue from 'vue'; import DueDateSelectors from '~/due_date_select'; -import { deprecatedCreateFlash as Flash } from '~/flash'; import IssuableContext from '~/issuable_context'; import LabelsSelect from '~/labels_select'; import { isScopedLabel } from '~/lib/utils/common_utils'; @@ -16,6 +15,7 @@ import MilestoneSelect from '~/milestone_select'; import Sidebar from '~/right_sidebar'; import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue'; import Assignees from '~/sidebar/components/assignees/assignees.vue'; +import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue'; import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; import eventHub from '~/sidebar/event_hub'; @@ -32,6 +32,7 @@ export default Vue.extend({ RemoveBtn, Subscriptions, TimeTracker, + SidebarAssigneesWidget, }, props: { currentUser: { @@ -78,12 +79,6 @@ export default Vue.extend({ detail: { handler() { if (this.issue.id !== this.detail.issue.id) { - $('.block.assignee') - .find('input:not(.js-vue)[name="issue[assignee_ids][]"]') - .each((i, el) => { - $(el).remove(); - }); - $('.js-issue-board-sidebar', this.$el).each((i, el) => { $(el).data('deprecatedJQueryDropdown').clearMenu(); }); @@ -96,18 +91,9 @@ export default Vue.extend({ }, }, created() { - // Get events from deprecatedJQueryDropdown - eventHub.$on('sidebar.removeAssignee', this.removeAssignee); - eventHub.$on('sidebar.addAssignee', this.addAssignee); - eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); - eventHub.$on('sidebar.saveAssignees', this.saveAssignees); eventHub.$on('sidebar.closeAll', this.closeSidebar); }, beforeDestroy() { - eventHub.$off('sidebar.removeAssignee', this.removeAssignee); - eventHub.$off('sidebar.addAssignee', this.addAssignee); - eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); - eventHub.$off('sidebar.saveAssignees', this.saveAssignees); eventHub.$off('sidebar.closeAll', this.closeSidebar); }, mounted() { @@ -121,34 +107,8 @@ export default Vue.extend({ closeSidebar() { this.detail.issue = {}; }, - assignSelf() { - // Notify gl dropdown that we are now assigning to current user - this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself')); - - this.addAssignee(this.currentUser); - this.saveAssignees(); - }, - removeAssignee(a) { - boardsStore.detail.issue.removeAssignee(a); - }, - addAssignee(a) { - boardsStore.detail.issue.addAssignee(a); - }, - removeAllAssignees() { - boardsStore.detail.issue.removeAllAssignees(); - }, - saveAssignees() { - this.loadingAssignees = true; - - boardsStore.detail.issue - .update() - .then(() => { - this.loadingAssignees = false; - }) - .catch(() => { - this.loadingAssignees = false; - Flash(__('An error occurred while saving assignees')); - }); + setAssignees(data) { + boardsStore.detail.issue.setAssignees(data.issueSetAssignees.issue.assignees.nodes); }, showScopedLabels(label) { return boardsStore.scopedLabels.enabled && isScopedLabel(label); diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 15f7e2191a2..859295318ed 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -86,7 +86,7 @@ export default () => { groupId: Number($boardApp.dataset.groupId), rootPath: $boardApp.dataset.rootPath, currentUserId: gon.current_user_id || null, - canUpdate: $boardApp.dataset.canUpdate, + canUpdate: parseBoolean($boardApp.dataset.canUpdate), labelsFetchPath: $boardApp.dataset.labelsFetchPath, labelsManagePath: $boardApp.dataset.labelsManagePath, labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath, diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index bc23639df67..46d1239457d 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -53,6 +53,10 @@ class ListIssue { return boardsStore.findIssueAssignee(this, findAssignee); } + setAssignees(assignees) { + boardsStore.setIssueAssignees(this, assignees); + } + removeAssignee(removeAssignee) { boardsStore.removeIssueAssignee(this, removeAssignee); } diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index e2d07f9128c..a7cf1e9e647 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -1,13 +1,9 @@ import { pick } from 'lodash'; - import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; import { BoardType, ListType, inactiveId, flashAnimationDuration } from '~/boards/constants'; -import createFlash from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils'; -import { __ } from '~/locale'; -import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql'; import { formatBoardLists, formatListIssues, @@ -333,34 +329,11 @@ export default { }, setAssignees: ({ commit, getters }, assigneeUsernames) => { - commit(types.SET_ASSIGNEE_LOADING, true); - - return gqlClient - .mutate({ - mutation: updateAssigneesMutation, - variables: { - iid: getters.activeIssue.iid, - projectPath: getters.activeIssue.referencePath.split('#')[0], - assigneeUsernames, - }, - }) - .then(({ data }) => { - const { nodes } = data.issueSetAssignees?.issue?.assignees || []; - - commit('UPDATE_ISSUE_BY_ID', { - issueId: getters.activeIssue.id, - prop: 'assignees', - value: nodes, - }); - - return nodes; - }) - .catch(() => { - createFlash({ message: __('An error occurred while updating assignees.') }); - }) - .finally(() => { - commit(types.SET_ASSIGNEE_LOADING, false); - }); + commit('UPDATE_ISSUE_BY_ID', { + issueId: getters.activeIssue.id, + prop: 'assignees', + value: assigneeUsernames, + }); }, setActiveIssueMilestone: async ({ commit, getters }, input) => { diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 39d95552084..fbff736c7e1 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -724,6 +724,10 @@ const boardsStore = { } }, + setIssueAssignees(issue, assignees) { + issue.assignees = [...assignees]; + }, + removeIssueLabels(issue, labels) { labels.forEach(issue.removeLabel.bind(issue)); }, diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index 0f2afb779fd..c73c8fb6ca4 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -347,15 +347,20 @@ export default { >
-
-
+
+
{{ s__('DesignManagement|Designs') }}
-
+
-
+
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue index 3c1b3afe889..e2dc37a0ac2 100644 --- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue @@ -8,12 +8,16 @@ export default { GlButton, UncollapsedAssigneeList, }, - inject: ['rootPath'], props: { users: { type: Array, required: true, }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, }, computed: { assigneesText() { @@ -36,9 +40,9 @@ export default { variant="link" @click="$emit('assign-self')" > - {{ __('assign yourself') }} + {{ __('assign yourself') }}
- +
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 9b0d2228395..6595debf9a5 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -44,6 +44,11 @@ export default { type: String, required: true, }, + assigneeAvailabilityStatus: { + type: Object, + required: false, + default: () => ({}), + }, }, data() { return { @@ -101,6 +106,13 @@ export default { return new Flash(__('Error occurred when saving assignees')); }); }, + exposeAvailabilityStatus(users) { + return users.map(({ username, ...rest }) => ({ + ...rest, + username, + availability: this.assigneeAvailabilityStatus[username] || '', + })); + }, }, }; @@ -123,7 +135,7 @@ export default { +import { + GlDropdownItem, + GlDropdownDivider, + GlAvatarLabeled, + GlAvatarLink, + GlSearchBoxByType, + GlLoadingIcon, +} from '@gitlab/ui'; +import { cloneDeep } from 'lodash'; +import Vue from 'vue'; +import createFlash from '~/flash'; +import searchUsers from '~/graphql_shared/queries/users_search.query.graphql'; +import { IssuableType } from '~/issue_show/constants'; +import { __, n__ } from '~/locale'; +import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import { assigneesQueries } from '~/sidebar/constants'; +import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; + +export const assigneesWidget = Vue.observable({ + updateAssignees: null, +}); + +export default { + i18n: { + unassigned: __('Unassigned'), + assignee: __('Assignee'), + assignees: __('Assignees'), + assignTo: __('Assign to'), + }, + assigneesQueries, + components: { + SidebarEditableItem, + IssuableAssignees, + MultiSelectDropdown, + GlDropdownItem, + GlDropdownDivider, + GlAvatarLabeled, + GlAvatarLink, + GlSearchBoxByType, + GlLoadingIcon, + }, + props: { + iid: { + type: String, + required: true, + }, + fullPath: { + type: String, + required: true, + }, + initialAssignees: { + type: Array, + required: false, + default: null, + }, + issuableType: { + type: String, + required: false, + default: IssuableType.Issue, + validator(value) { + return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); + }, + }, + multipleAssignees: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + return { + search: '', + issuable: {}, + searchUsers: [], + selected: [], + isSettingAssignees: false, + isSearching: false, + }; + }, + apollo: { + issuable: { + query() { + return this.$options.assigneesQueries[this.issuableType].query; + }, + variables() { + return this.queryVariables; + }, + update(data) { + return data.issuable || data.project?.issuable; + }, + result({ data }) { + const issuable = data.issuable || data.project?.issuable; + if (issuable) { + this.selected = this.moveCurrentUserToStart(cloneDeep(issuable.assignees.nodes)); + } + }, + error() { + createFlash({ message: __('An error occurred while fetching participants.') }); + }, + }, + searchUsers: { + query: searchUsers, + variables() { + return { + search: this.search, + }; + }, + update(data) { + return data.users?.nodes || []; + }, + debounce: 250, + skip() { + return this.isSearchEmpty; + }, + error() { + createFlash({ message: __('An error occurred while searching users.') }); + this.isSearching = false; + }, + result() { + this.isSearching = false; + }, + }, + }, + computed: { + queryVariables() { + return { + iid: this.iid, + fullPath: this.fullPath, + }; + }, + assignees() { + const currentAssignees = this.$apollo.queries.issuable.loading + ? this.initialAssignees + : this.issuable?.assignees?.nodes; + return currentAssignees || []; + }, + participants() { + const users = + this.isSearchEmpty || this.isSearching + ? this.issuable?.participants?.nodes + : this.searchUsers; + return this.moveCurrentUserToStart(users); + }, + assigneeText() { + const items = this.$apollo.queries.issuable.loading ? this.initialAssignees : this.selected; + return n__('Assignee', '%d Assignees', items.length); + }, + selectedFiltered() { + if (this.isSearchEmpty || this.isSearching) { + return this.selected; + } + + const foundUsernames = this.searchUsers.map(({ username }) => username); + return this.selected.filter(({ username }) => foundUsernames.includes(username)); + }, + unselectedFiltered() { + return ( + this.participants?.filter(({ username }) => !this.selectedUserNames.includes(username)) || + [] + ); + }, + selectedIsEmpty() { + return this.selectedFiltered.length === 0; + }, + selectedUserNames() { + return this.selected.map(({ username }) => username); + }, + isSearchEmpty() { + return this.search === ''; + }, + currentUser() { + return { + username: gon?.current_username, + name: gon?.current_user_fullname, + avatarUrl: gon?.current_user_avatar_url, + }; + }, + isAssigneesLoading() { + return !this.initialAssignees && this.$apollo.queries.issuable.loading; + }, + isCurrentUserInParticipants() { + const isCurrentUser = (user) => user.username === this.currentUser.username; + return this.selected.some(isCurrentUser) || this.participants.some(isCurrentUser); + }, + noUsersFound() { + return !this.isSearchEmpty && this.unselectedFiltered.length === 0; + }, + showCurrentUser() { + return !this.isCurrentUserInParticipants && (this.isSearchEmpty || this.isSearching); + }, + }, + watch: { + // We need to add this watcher to track the moment when user is alredy typing + // but query is still not started due to debounce + search(newVal) { + if (newVal) { + this.isSearching = true; + } + }, + }, + created() { + assigneesWidget.updateAssignees = this.updateAssignees; + }, + destroyed() { + assigneesWidget.updateAssignees = null; + }, + methods: { + updateAssignees(assigneeUsernames) { + this.isSettingAssignees = true; + return this.$apollo + .mutate({ + mutation: this.$options.assigneesQueries[this.issuableType].mutation, + variables: { + ...this.queryVariables, + assigneeUsernames, + }, + }) + .then(({ data }) => { + this.$emit('assignees-updated', data); + return data; + }) + .catch(() => { + createFlash({ message: __('An error occurred while updating assignees.') }); + }) + .finally(() => { + this.isSettingAssignees = false; + }); + }, + selectAssignee(name) { + if (name === undefined) { + this.clearSelected(); + return; + } + + if (!this.multipleAssignees) { + this.selected = [name]; + this.collapseWidget(); + } else { + this.selected = this.selected.concat(name); + } + }, + unselect(name) { + this.selected = this.selected.filter((user) => user.username !== name); + + if (!this.multipleAssignees) { + this.collapseWidget(); + } + }, + assignSelf() { + this.updateAssignees(this.currentUser.username); + }, + clearSelected() { + this.selected = []; + }, + saveAssignees() { + this.updateAssignees(this.selectedUserNames); + }, + isChecked(id) { + return this.selectedUserNames.includes(id); + }, + async focusSearch() { + await this.$nextTick(); + this.$refs.search.focusInput(); + }, + moveCurrentUserToStart(users) { + if (!users) { + return []; + } + const usersCopy = [...users]; + const currentUser = usersCopy.find((user) => user.username === this.currentUser.username); + + if (currentUser) { + const index = usersCopy.indexOf(currentUser); + usersCopy.splice(0, 0, usersCopy.splice(index, 1)[0]); + } + + return usersCopy; + }, + collapseWidget() { + this.$refs.toggle.collapse(); + }, + }, +}; + + + diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue index 00a2456c6b6..2c52d7142f7 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue @@ -59,7 +59,7 @@ export default {
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue new file mode 100644 index 00000000000..9da839cd133 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue @@ -0,0 +1,95 @@ + + + diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js new file mode 100644 index 00000000000..274aa237aea --- /dev/null +++ b/app/assets/javascripts/sidebar/constants.js @@ -0,0 +1,16 @@ +import { IssuableType } from '~/issue_show/constants'; +import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; +import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql'; +import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql'; +import updateMergeRequestParticipantsMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql'; + +export const assigneesQueries = { + [IssuableType.Issue]: { + query: getIssueParticipants, + mutation: updateAssigneesMutation, + }, + [IssuableType.MergeRequest]: { + query: getMergeRequestParticipants, + mutation: updateMergeRequestParticipantsMutation, + }, +}; diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index bc019f09c85..662edbc4f8d 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -30,6 +30,28 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op return JSON.parse(sidebarOptEl.innerHTML); } +/** + * Extracts the list of assignees with availability information from a hidden input + * field and converts to a key:value pair for use in the sidebar assignees component. + * The assignee username is used as the key and their busy status is the value + * + * e.g { root: 'busy', admin: '' } + * + * @returns {Object} + */ +function getSidebarAssigneeAvailabilityData() { + const sidebarAssigneeEl = document.querySelectorAll('.js-sidebar-assignee-data input'); + return Array.from(sidebarAssigneeEl) + .map((el) => el.dataset) + .reduce( + (acc, { username, availability = '' }) => ({ + ...acc, + [username]: availability, + }), + {}, + ); +} + function mountAssigneesComponent(mediator) { const el = document.getElementById('js-vue-sidebar-assignees'); const apolloProvider = new VueApollo({ @@ -39,6 +61,7 @@ function mountAssigneesComponent(mediator) { if (!el) return; const { iid, fullPath } = getSidebarOptions(); + const assigneeAvailabilityStatus = getSidebarAssigneeAvailabilityData(); // eslint-disable-next-line no-new new Vue({ el, @@ -56,6 +79,7 @@ function mountAssigneesComponent(mediator) { signedIn: el.hasAttribute('data-signed-in'), issuableType: isInIssuePage() || isInIncidentPage() || isInDesignPage() ? 'issue' : 'merge_request', + assigneeAvailabilityStatus, }, }), }); diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js index 951f512b784..e1a4a74b982 100644 --- a/app/assets/javascripts/users_select/index.js +++ b/app/assets/javascripts/users_select/index.js @@ -9,6 +9,7 @@ import { AJAX_USERS_SELECT_PARAMS_MAP, } from 'ee_else_ce/users_select/constants'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; +import { isUserBusy } from '~/set_status_modal/utils'; import { fixTitle, dispose } from '~/tooltips'; import ModalStore from '../boards/stores/modal_store'; import axios from '../lib/utils/axios_utils'; @@ -795,13 +796,17 @@ UsersSelect.prototype.renderRow = function ( ? `data-container="body" data-placement="left" data-title="${tooltip}"` : ''; + const name = + user?.availability && isUserBusy(user.availability) + ? sprintf(__('%{name} (Busy)'), { name: user.name }) + : user.name; return `
  • ${this.renderRowAvatar(issuableType, user, img)} - ${escape(user.name)} + ${escape(name)} ${ username diff --git a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue index c5bbe1b33fb..132abcab82b 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue @@ -20,7 +20,7 @@ export default {