diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-17 14:33:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-17 14:33:21 +0300 |
commit | 7021455bd1ed7b125c55eb1b33c5a01f2bc55ee0 (patch) | |
tree | 5bdc2229f5198d516781f8d24eace62fc7e589e9 /app/assets/javascripts/sidebar | |
parent | 185b095e93520f96e9cfc31d9c3e69b498cdab7c (diff) |
Add latest changes from gitlab-org/gitlab@15-6-stable-eev15.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/sidebar')
13 files changed, 657 insertions, 330 deletions
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index 6e18cf36690..2a9100f0cb5 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -55,6 +55,7 @@ export default { class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right" href="#" data-test-id="edit-link" + data-qa-selector="edit_link" data-track-action="click_edit_button" data-track-label="right_sidebar" data-track-property="assignee" diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index 29ea390a81d..cf07752a0b8 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -56,6 +56,7 @@ export default { type="button" class="gl-button btn-link gl-reset-color!" data-testid="assign-yourself" + data-qa-selector="assign_yourself_button" @click="assignSelf" > {{ __('assign yourself') }} diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue index 0e4d4c74160..d83ae782e26 100644 --- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue @@ -91,6 +91,7 @@ export default { <div class="gl-ml-3 gl-line-height-normal gl-display-grid gl-align-items-center" data-testid="username" + data-qa-selector="username" > <user-name-with-status :name="user.name" :availability="userAvailability(user)" /> </div> diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue index 98468583992..c262d65f6ce 100644 --- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue +++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue @@ -170,7 +170,7 @@ export default { this.$emit('closeForm'); }, openDatePicker() { - this.$refs.datePicker.calendar.show(); + this.$refs.datePicker.show(); }, setFixedDate(isFixed) { const date = this.issuable[dateFields[this.dateType].dateFixed]; diff --git a/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue new file mode 100644 index 00000000000..1fff089eab4 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue @@ -0,0 +1,115 @@ +<script> +import { GlDropdownItem } from '@gitlab/ui'; +import { TYPE_MILESTONE } from '~/graphql_shared/constants'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { IssuableType, WorkspaceType } from '~/issues/constants'; +import { __ } from '~/locale'; +import { IssuableAttributeType } from '../../constants'; +import SidebarDropdown from '../sidebar_dropdown.vue'; + +const noMilestone = { + id: 0, + title: __('No milestone'), +}; + +const placeholderMilestone = { + id: -1, + title: __('Select milestone'), +}; + +export default { + issuableAttribute: IssuableAttributeType.Milestone, + components: { + GlDropdownItem, + SidebarDropdown, + }, + props: { + attrWorkspacePath: { + required: true, + type: String, + }, + canAdminMilestone: { + type: Boolean, + required: false, + default: false, + }, + issuableType: { + type: String, + required: true, + validator(value) { + return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); + }, + }, + inputName: { + type: String, + required: false, + default: 'update[milestone_id]', + }, + milestoneId: { + type: String, + required: false, + default: '', + }, + milestoneTitle: { + type: String, + required: false, + default: '', + }, + projectMilestonesPath: { + type: String, + required: false, + default: '', + }, + workspaceType: { + type: String, + required: true, + validator(value) { + return [WorkspaceType.group, WorkspaceType.project].includes(value); + }, + }, + }, + data() { + return { + milestone: this.milestoneId + ? { id: convertToGraphQLId(TYPE_MILESTONE, this.milestoneId), title: this.milestoneTitle } + : placeholderMilestone, + }; + }, + computed: { + footerItemText() { + return this.canAdminMilestone ? __('Manage milestones') : __('View milestones'); + }, + value() { + return this.milestone.id === placeholderMilestone.id + ? undefined + : getIdFromGraphQLId(this.milestone.id); + }, + }, + methods: { + handleChange(milestone) { + this.milestone = milestone.id === null ? noMilestone : milestone; + }, + }, +}; +</script> + +<template> + <div> + <input type="hidden" :name="inputName" :value="value" /> + <sidebar-dropdown + :attr-workspace-path="attrWorkspacePath" + :current-attribute="milestone" + :issuable-attribute="$options.issuableAttribute" + :issuable-type="issuableType" + :workspace-type="workspaceType" + data-qa-selector="issuable_milestone_dropdown" + @change="handleChange" + > + <template #footer> + <gl-dropdown-item v-if="projectMilestonesPath" :href="projectMilestonesPath"> + {{ footerItemText }} + </gl-dropdown-item> + </template> + </sidebar-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index ad061dd2e6b..5f1350690eb 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -9,6 +9,8 @@ import eventHub from '~/sidebar/event_hub'; import Store from '~/sidebar/stores/sidebar_store'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import getMergeRequestReviewersQuery from '~/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql'; +import mergeRequestReviewersUpdatedSubscription from '~/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import ReviewerTitle from './reviewer_title.vue'; import Reviewers from './reviewers.vue'; @@ -66,6 +68,36 @@ export default { error() { createAlert({ message: __('An error occurred while fetching reviewers.') }); }, + subscribeToMore: { + document() { + return mergeRequestReviewersUpdatedSubscription; + }, + variables() { + return { + issuableId: this.issuable?.id, + }; + }, + skip() { + return !this.issuable?.id || !this.isRealtimeEnabled; + }, + updateQuery( + _, + { + subscriptionData: { + data: { mergeRequestReviewersUpdated }, + }, + }, + ) { + if (mergeRequestReviewersUpdated) { + this.store.setReviewersFromRealtime( + mergeRequestReviewersUpdated.reviewers.nodes.map((r) => ({ + ...r, + id: getIdFromGraphQLId(r.id), + })), + ); + } + }, + }, }, }, data() { @@ -87,6 +119,9 @@ export default { canUpdate() { return this.issuable.userPermissions?.adminMergeRequest || false; }, + isRealtimeEnabled() { + return this.glFeatures.realtimeReviewers; + }, }, created() { this.store = new Store(); diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue new file mode 100644 index 00000000000..a135dfdca72 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue @@ -0,0 +1,34 @@ +<script> +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { state } from './sidebar_reviewers.vue'; + +export default { + data() { + return state; + }, + computed: { + reviewers() { + return this.issuable?.reviewers?.nodes || []; + }, + }, + methods: { + getIdFromGraphQLId, + }, +}; +</script> + +<template> + <div> + <input + v-for="reviewer in reviewers" + :key="reviewer.id" + type="hidden" + name="merge_request[reviewer_ids][]" + :value="getIdFromGraphQLId(reviewer.id)" + :data-avatar-url="reviewer.avatarUrl" + :data-name="reviewer.name" + :data-username="reviewer.username" + :data-can-merge="reviewer.mergeRequestInteraction.canMerge" + /> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue new file mode 100644 index 00000000000..26e2bc96f54 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue @@ -0,0 +1,252 @@ +<script> +import { + GlDropdown, + GlDropdownDivider, + GlDropdownItem, + GlDropdownText, + GlLoadingIcon, + GlSearchBoxByType, +} from '@gitlab/ui'; +import { kebabCase, snakeCase } from 'lodash'; +import { IssuableType, WorkspaceType } from '~/issues/constants'; +import { __ } from '~/locale'; +import { + defaultEpicSort, + dropdowni18nText, + epicIidPattern, + issuableAttributesQueries, + IssuableAttributeState, + IssuableAttributeType, + IssuableAttributeTypeKeyMap, + LocalizedIssuableAttributeType, + noAttributeId, +} from 'ee_else_ce/sidebar/constants'; +import { createAlert } from '~/flash'; +import { PathIdSeparator } from '~/related_issues/constants'; + +export default { + noAttributeId, + i18n: { + expired: __('(expired)'), + }, + components: { + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlDropdownDivider, + GlSearchBoxByType, + GlLoadingIcon, + }, + inject: { + issuableAttributesQueries: { + default: issuableAttributesQueries, + }, + issuableAttributesState: { + default: IssuableAttributeState, + }, + widgetTitleText: { + default: { + [IssuableAttributeType.Milestone]: __('Milestone'), + expired: __('(expired)'), + none: __('None'), + }, + }, + }, + props: { + attrWorkspacePath: { + required: true, + type: String, + }, + currentAttribute: { + type: Object, + required: false, + default: () => ({}), + }, + issuableAttribute: { + type: String, + required: true, + }, + issuableType: { + type: String, + required: true, + validator(value) { + return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); + }, + }, + workspaceType: { + type: String, + required: false, + default: WorkspaceType.project, + validator(value) { + return [WorkspaceType.group, WorkspaceType.project].includes(value); + }, + }, + }, + data() { + return { + attributesList: [], + searchTerm: '', + skipQuery: true, + }; + }, + apollo: { + attributesList: { + query() { + const { list } = this.issuableAttributeQuery; + const { query } = list[this.issuableType]; + return query[this.workspaceType] || query; + }, + variables() { + if (!this.isEpic) { + return { + fullPath: this.attrWorkspacePath, + title: this.searchTerm, + state: this.issuableAttributesState[this.issuableAttribute], + }; + } + + const variables = { + fullPath: this.attrWorkspacePath, + state: this.issuableAttributesState[this.issuableAttribute], + sort: defaultEpicSort, + }; + + if (epicIidPattern.test(this.searchTerm)) { + const matches = this.searchTerm.match(epicIidPattern); + variables.iidStartsWith = matches.groups.iid; + } else if (this.searchTerm !== '') { + variables.in = 'TITLE'; + variables.title = this.searchTerm; + } + + return variables; + }, + update: (data) => data?.workspace?.attributes?.nodes ?? [], + error(error) { + createAlert({ message: this.i18n.listFetchError, captureError: true, error }); + }, + skip() { + if ( + this.isEpic && + this.searchTerm.startsWith(PathIdSeparator.Epic) && + this.searchTerm.length < 2 + ) { + return true; + } + return this.skipQuery; + }, + debounce: 250, + }, + }, + computed: { + attributeTypeTitle() { + return this.widgetTitleText[this.issuableAttribute]; + }, + dropdownText() { + return this.currentAttribute ? this.currentAttribute?.title : this.attributeTypeTitle; + }, + emptyPropsList() { + return this.attributesList.length === 0; + }, + i18n() { + const localizedAttribute = + LocalizedIssuableAttributeType[IssuableAttributeTypeKeyMap[this.issuableAttribute]]; + return dropdowni18nText(localizedAttribute, this.issuableType); + }, + isEpic() { + // MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311 + return this.issuableAttribute === IssuableType.Epic; + }, + issuableAttributeQuery() { + return this.issuableAttributesQueries[this.issuableAttribute]; + }, + formatIssuableAttribute() { + return { + kebab: kebabCase(this.issuableAttribute), + snake: snakeCase(this.issuableAttribute), + }; + }, + }, + methods: { + isAttributeChecked(attributeId) { + return ( + attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId) + ); + }, + isAttributeOverdue(attribute) { + return this.issuableAttribute === IssuableAttributeType.Milestone + ? attribute?.expired + : false; + }, + handleShow() { + this.skipQuery = false; + }, + setFocus() { + this.$refs.search.focusInput(); + }, + show() { + this.$refs.dropdown.show(); + }, + updateAttribute(attribute) { + this.$emit('change', attribute); + }, + }, +}; +</script> + +<template> + <gl-dropdown + ref="dropdown" + block + :header-text="i18n.assignAttribute" + lazy + :text="dropdownText" + toggle-class="gl-m-0" + @show="handleShow" + @shown="setFocus" + > + <gl-search-box-by-type ref="search" v-model="searchTerm" :placeholder="__('Search')" /> + <gl-dropdown-item + :data-testid="`no-${formatIssuableAttribute.kebab}-item`" + is-check-item + :is-checked="isAttributeChecked($options.noAttributeId)" + @click="$emit('change', { id: $options.noAttributeId })" + > + {{ i18n.noAttribute }} + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-loading-icon + v-if="$apollo.queries.attributesList.loading" + size="sm" + class="gl-py-4" + data-testid="loading-icon-dropdown" + /> + <template v-else> + <gl-dropdown-text v-if="emptyPropsList"> + {{ i18n.noAttributesFound }} + </gl-dropdown-text> + <slot + v-else + name="list" + :attributes-list="attributesList" + :is-attribute-checked="isAttributeChecked" + :update-attribute="updateAttribute" + > + <gl-dropdown-item + v-for="attrItem in attributesList" + :key="attrItem.id" + is-check-item + :is-checked="isAttributeChecked(attrItem.id)" + :data-testid="`${formatIssuableAttribute.kebab}-items`" + @click="updateAttribute(attrItem)" + > + {{ attrItem.title }} + <template v-if="isAttributeOverdue(attrItem)">{{ $options.i18n.expired }}</template> + </gl-dropdown-item> + </slot> + </template> + <template #footer> + <slot name="footer"></slot> + </template> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue index c33b1468ca4..a685929cdea 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -1,17 +1,5 @@ <script> -import { - GlLink, - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlSearchBoxByType, - GlDropdownDivider, - GlLoadingIcon, - GlIcon, - GlTooltipDirective, - GlPopover, - GlButton, -} from '@gitlab/ui'; +import { GlButton, GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui'; import { kebabCase, snakeCase } from 'lodash'; import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -22,19 +10,15 @@ import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue' import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { dropdowni18nText, - Tracking, - IssuableAttributeState, - IssuableAttributeType, LocalizedIssuableAttributeType, IssuableAttributeTypeKeyMap, issuableAttributesQueries, - noAttributeId, - defaultEpicSort, - epicIidPattern, + IssuableAttributeType, + Tracking, } from 'ee_else_ce/sidebar/constants'; +import SidebarDropdown from './sidebar_dropdown.vue'; export default { - noAttributeId, i18n: { expired: __('(expired)'), none: __('None'), @@ -43,17 +27,12 @@ export default { GlTooltip: GlTooltipDirective, }, components: { - SidebarEditableItem, GlLink, - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlDropdownDivider, - GlSearchBoxByType, GlIcon, - GlLoadingIcon, GlPopover, GlButton, + SidebarDropdown, + SidebarEditableItem, }, mixins: [glFeatureFlagMixin()], inject: { @@ -63,9 +42,6 @@ export default { issuableAttributesQueries: { default: issuableAttributesQueries, }, - issuableAttributesState: { - default: IssuableAttributeState, - }, widgetTitleText: { default: { [IssuableAttributeType.Milestone]: __('Milestone'), @@ -74,7 +50,6 @@ export default { }, }, }, - props: { issuableAttribute: { type: String, @@ -134,67 +109,14 @@ export default { }); }, }, - attributesList: { - query() { - const { list } = this.issuableAttributeQuery; - const { query } = list[this.issuableType]; - - return query; - }, - skip() { - if (this.isEpic && this.searchTerm.startsWith('&') && this.searchTerm.length < 2) { - return true; - } - - return !this.editing; - }, - debounce: 250, - variables() { - if (!this.isEpic) { - return { - fullPath: this.attrWorkspacePath, - title: this.searchTerm, - state: this.issuableAttributesState[this.issuableAttribute], - }; - } - - const variables = { - fullPath: this.attrWorkspacePath, - state: this.issuableAttributesState[this.issuableAttribute], - sort: defaultEpicSort, - }; - - if (epicIidPattern.test(this.searchTerm)) { - const matches = this.searchTerm.match(epicIidPattern); - variables.iidStartsWith = matches.groups.iid; - } else if (this.searchTerm !== '') { - variables.in = 'TITLE'; - variables.title = this.searchTerm; - } - - return variables; - }, - update(data) { - if (data?.workspace) { - return data?.workspace?.attributes.nodes; - } - return []; - }, - error(error) { - createAlert({ message: this.i18n.listFetchError, captureError: true, error }); - }, - }, }, data() { return { - searchTerm: '', - editing: false, updating: false, selectedTitle: null, currentAttribute: null, hasCurrentAttribute: false, editConfirmation: false, - attributesList: [], tracking: { event: Tracking.editEvent, label: Tracking.rightSidebarLabel, @@ -212,15 +134,9 @@ export default { attributeUrl() { return this.currentAttribute?.webUrl; }, - dropdownText() { - return this.currentAttribute ? this.currentAttribute?.title : this.attributeTypeTitle; - }, loading() { return this.$apollo.queries.currentAttribute.loading; }, - emptyPropsList() { - return this.attributesList.length === 0; - }, attributeTypeTitle() { return this.widgetTitleText[this.issuableAttribute]; }, @@ -256,16 +172,12 @@ export default { }, }, methods: { - updateAttribute(attributeId) { - if (this.currentAttribute === null && attributeId === null) return; - if (attributeId === this.currentAttribute?.id) return; + updateAttribute({ id }) { + if (this.currentAttribute === null && id === null) return; + if (id === this.currentAttribute?.id) return; this.updating = true; - const selectedAttribute = - Boolean(attributeId) && this.attributesList.find((p) => p.id === attributeId); - this.selectedTitle = selectedAttribute ? selectedAttribute.title : this.widgetTitleText.none; - const { current } = this.issuableAttributeQuery; const { mutation } = current[this.issuableType]; @@ -277,8 +189,8 @@ export default { attributeId: this.issuableAttribute === IssuableAttributeType.Milestone && this.issuableType === IssuableType.Issue - ? getIdFromGraphQLId(attributeId) - : attributeId, + ? getIdFromGraphQLId(id) + : id, iid: this.iid, }, }) @@ -298,32 +210,16 @@ export default { }) .finally(() => { this.updating = false; - this.searchTerm = ''; this.selectedTitle = null; }); }, - isAttributeChecked(attributeId = undefined) { - return ( - attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId) - ); - }, isAttributeOverdue(attribute) { return this.issuableAttribute === IssuableAttributeType.Milestone ? attribute?.expired : false; }, showDropdown() { - this.$refs.newDropdown.show(); - }, - handleOpen() { - this.editing = true; - this.showDropdown(); - }, - handleClose() { - this.editing = false; - }, - setFocus() { - this.$refs.search.focusInput(); + this.$refs.dropdown.show(); }, handlePopoverClose() { this.$refs.popover.$emit('close'); @@ -349,8 +245,7 @@ export default { :tracking="tracking" :should-show-confirmation-popover="shouldShowConfirmationPopover" :loading="updating || loading" - @open="handleOpen" - @close="handleClose" + @open="showDropdown" @edit-confirm="handleEditConfirmation" > <template #collapsed> @@ -432,58 +327,24 @@ export default { </gl-popover> </template> <template v-else #default> - <gl-dropdown - ref="newDropdown" - lazy - :header-text="i18n.assignAttribute" - :text="dropdownText" - :loading="loading" - class="gl-w-full" - toggle-class="gl-max-w-100" - block - @shown="setFocus" + <sidebar-dropdown + ref="dropdown" + :attr-workspace-path="attrWorkspacePath" + :current-attribute="currentAttribute" + :issuable-attribute="issuableAttribute" + :issuable-type="issuableType" + @change="updateAttribute" > - <gl-search-box-by-type ref="search" v-model="searchTerm" /> - <gl-dropdown-item - :data-testid="`no-${formatIssuableAttribute.kebab}-item`" - is-check-item - :is-checked="isAttributeChecked($options.noAttributeId)" - @click="updateAttribute($options.noAttributeId)" - > - {{ i18n.noAttribute }} - </gl-dropdown-item> - <gl-dropdown-divider /> - <gl-loading-icon - v-if="$apollo.queries.attributesList.loading" - size="sm" - class="gl-py-4" - data-testid="loading-icon-dropdown" - /> - <template v-else> - <gl-dropdown-text v-if="emptyPropsList"> - {{ i18n.noAttributesFound }} - </gl-dropdown-text> + <template #list="{ attributesList, isAttributeChecked, updateAttribute: update }"> <slot - v-else name="list" :attributes-list="attributesList" :is-attribute-checked="isAttributeChecked" - :update-attribute="updateAttribute" + :update-attribute="update" > - <gl-dropdown-item - v-for="attrItem in attributesList" - :key="attrItem.id" - is-check-item - :is-checked="isAttributeChecked(attrItem.id)" - :data-testid="`${formatIssuableAttribute.kebab}-items`" - @click="updateAttribute(attrItem.id)" - > - {{ attrItem.title }} - <span v-if="isAttributeOverdue(attrItem)">{{ $options.i18n.expired }}</span> - </gl-dropdown-item> </slot> </template> - </gl-dropdown> + </sidebar-dropdown> </template> </sidebar-editable-item> </template> diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index 6248bcb8e2d..67b9b540e91 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -53,6 +53,7 @@ import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/querie import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql'; import getEscalationStatusQuery from '~/sidebar/queries/escalation_status.query.graphql'; import updateEscalationStatusMutation from '~/sidebar/queries/update_escalation_status.mutation.graphql'; +import groupMilestonesQuery from './queries/group_milestones.query.graphql'; import projectIssueMilestoneMutation from './queries/project_issue_milestone.mutation.graphql'; import projectIssueMilestoneQuery from './queries/project_issue_milestone.query.graphql'; import projectMilestonesQuery from './queries/project_milestones.query.graphql'; @@ -241,10 +242,16 @@ export const issuableMilestoneQueries = { export const milestonesQueries = { [IssuableType.Issue]: { - query: projectMilestonesQuery, + query: { + [WorkspaceType.group]: groupMilestonesQuery, + [WorkspaceType.project]: projectMilestonesQuery, + }, }, [IssuableType.MergeRequest]: { - query: projectMilestonesQuery, + query: { + [WorkspaceType.group]: groupMilestonesQuery, + [WorkspaceType.project]: projectMilestonesQuery, + }, }, }; diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js index cc5de5e4083..afce59d304f 100644 --- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js @@ -5,7 +5,7 @@ import TimeTracker from './components/time_tracking/time_tracker.vue'; export default class SidebarMilestone { constructor() { - const el = document.getElementById('issuable-time-tracker'); + const el = document.querySelector('.js-sidebar-time-tracking-root'); if (!el) return; diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 9b5bad710dd..b37486283ca 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -18,6 +18,7 @@ import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assi import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; +import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue'; import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue'; import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue'; import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue'; @@ -33,6 +34,7 @@ import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue'; import SidebarEscalationStatus from './components/incidents/sidebar_escalation_status.vue'; import IssuableLockForm from './components/lock/issuable_lock_form.vue'; import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue'; +import SidebarReviewersInputs from './components/reviewers/sidebar_reviewers_inputs.vue'; import SidebarSeverity from './components/severity/sidebar_severity.vue'; import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue'; @@ -47,27 +49,24 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op return JSON.parse(sidebarOptEl.innerHTML); } -function mountSidebarToDoWidget() { - const el = document.querySelector('.js-issuable-todo'); +function mountSidebarTodoWidget() { + const el = document.querySelector('.js-sidebar-todo-widget-root'); if (!el) { - return false; + return null; } const { projectPath, iid, id } = el.dataset; return new Vue({ el, - name: 'SidebarTodoRoot', + name: 'SidebarTodoWidgetRoot', apolloProvider, - components: { - SidebarTodoWidget, - }, provide: { isClassicSidebar: true, }, render: (createElement) => - createElement('sidebar-todo-widget', { + createElement(SidebarTodoWidget, { props: { fullPath: projectPath, issuableId: @@ -97,23 +96,22 @@ function getSidebarAssigneeAvailabilityData() { ); } -function mountAssigneesComponentDeprecated(mediator) { - const el = document.getElementById('js-vue-sidebar-assignees'); +function mountSidebarAssigneesDeprecated(mediator) { + const el = document.querySelector('.js-sidebar-assignees-root'); - if (!el) return; + if (!el) { + return null; + } const { id, iid, fullPath } = getSidebarOptions(); const assigneeAvailabilityStatus = getSidebarAssigneeAvailabilityData(); - // eslint-disable-next-line no-new - new Vue({ + + return new Vue({ el, name: 'SidebarAssigneesRoot', apolloProvider, - components: { - SidebarAssignees, - }, render: (createElement) => - createElement('sidebar-assignees', { + createElement(SidebarAssignees, { props: { mediator, issuableIid: String(iid), @@ -131,10 +129,12 @@ function mountAssigneesComponentDeprecated(mediator) { }); } -function mountAssigneesComponent() { - const el = document.getElementById('js-vue-sidebar-assignees'); +function mountSidebarAssigneesWidget() { + const el = document.querySelector('.js-sidebar-assignees-root'); - if (!el) return; + if (!el) { + return; + } const { id, iid, fullPath, editable } = getSidebarOptions(); const isIssuablePage = isInIssuePage() || isInIncidentPage() || isInDesignPage(); @@ -144,9 +144,6 @@ function mountAssigneesComponent() { el, name: 'SidebarAssigneesRoot', apolloProvider, - components: { - SidebarAssigneesWidget, - }, provide: { canUpdate: editable, directlyInviteMembers: Object.prototype.hasOwnProperty.call( @@ -155,7 +152,7 @@ function mountAssigneesComponent() { ), }, render: (createElement) => - createElement('sidebar-assignees-widget', { + createElement(SidebarAssigneesWidget, { props: { iid: String(iid), fullPath, @@ -183,10 +180,12 @@ function mountAssigneesComponent() { } } -function mountReviewersComponent(mediator) { - const el = document.getElementById('js-vue-sidebar-reviewers'); +function mountSidebarReviewers(mediator) { + const el = document.querySelector('.js-sidebar-reviewers-root'); - if (!el) return; + if (!el) { + return; + } const { iid, fullPath } = getSidebarOptions(); // eslint-disable-next-line no-new @@ -194,11 +193,8 @@ function mountReviewersComponent(mediator) { el, name: 'SidebarReviewersRoot', apolloProvider, - components: { - SidebarReviewers, - }, render: (createElement) => - createElement('sidebar-reviewers', { + createElement(SidebarReviewers, { props: { mediator, issuableIid: String(iid), @@ -210,6 +206,18 @@ function mountReviewersComponent(mediator) { }), }); + const reviewersInputEl = document.querySelector('.js-reviewers-inputs'); + + if (reviewersInputEl) { + // eslint-disable-next-line no-new + new Vue({ + el: reviewersInputEl, + render(createElement) { + return createElement(SidebarReviewersInputs); + }, + }); + } + const reviewerDropdown = document.querySelector('.js-sidebar-reviewer-dropdown'); if (reviewerDropdown) { @@ -217,22 +225,21 @@ function mountReviewersComponent(mediator) { } } -function mountCrmContactsComponent() { - const el = document.getElementById('js-issue-crm-contacts'); +function mountSidebarCrmContacts() { + const el = document.querySelector('.js-sidebar-crm-contacts-root'); - if (!el) return; + if (!el) { + return null; + } const { issueId, groupIssuesPath } = el.dataset; - // eslint-disable-next-line no-new - new Vue({ + + return new Vue({ el, name: 'SidebarCrmContactsRoot', apolloProvider, - components: { - CrmContacts, - }, render: (createElement) => - createElement('crm-contacts', { + createElement(CrmContacts, { props: { issueId, groupIssuesPath, @@ -241,28 +248,25 @@ function mountCrmContactsComponent() { }); } -function mountMilestoneSelect() { - const el = document.querySelector('.js-milestone-select'); +function mountSidebarMilestoneWidget() { + const el = document.querySelector('.js-sidebar-milestone-widget-root'); if (!el) { - return false; + return null; } const { canEdit, projectPath, issueIid } = el.dataset; return new Vue({ el, - name: 'SidebarMilestoneRoot', + name: 'SidebarMilestoneWidgetRoot', apolloProvider, - components: { - SidebarDropdownWidget, - }, provide: { canUpdate: parseBoolean(canEdit), isClassicSidebar: true, }, render: (createElement) => - createElement('sidebar-dropdown-widget', { + createElement(SidebarDropdownWidget, { props: { attrWorkspacePath: projectPath, workspacePath: projectPath, @@ -276,21 +280,57 @@ function mountMilestoneSelect() { }); } -export function mountSidebarLabels() { - const el = document.querySelector('.js-sidebar-labels'); +export function mountMilestoneDropdown() { + const el = document.querySelector('.js-milestone-dropdown-root'); if (!el) { - return false; + return null; } + Vue.use(VueApollo); + + const { + canAdminMilestone, + fullPath, + inputName, + milestoneId, + milestoneTitle, + projectMilestonesPath, + workspaceType, + } = el.dataset; + return new Vue({ el, - name: 'SidebarLabelsRoot', + name: 'MilestoneDropdownRoot', apolloProvider, - - components: { - LabelsSelectWidget, + render(createElement) { + return createElement(MilestoneDropdown, { + props: { + attrWorkspacePath: fullPath, + canAdminMilestone, + inputName, + issuableType: isInIssuePage() ? IssuableType.Issue : IssuableType.MergeRequest, + milestoneId, + milestoneTitle, + projectMilestonesPath, + workspaceType, + }, + }); }, + }); +} + +export function mountSidebarLabelsWidget() { + const el = document.querySelector('.js-sidebar-labels-widget-root'); + + if (!el) { + return null; + } + + return new Vue({ + el, + name: 'SidebarLabelsWidgetRoot', + apolloProvider, provide: { ...el.dataset, canUpdate: parseBoolean(el.dataset.canEdit), @@ -300,7 +340,7 @@ export function mountSidebarLabels() { isClassicSidebar: true, }, render: (createElement) => - createElement('labels-select-widget', { + createElement(LabelsSelectWidget, { props: { iid: String(el.dataset.iid), fullPath: el.dataset.projectPath, @@ -327,31 +367,27 @@ export function mountSidebarLabels() { }); } -function mountConfidentialComponent() { - const el = document.getElementById('js-confidential-entry-point'); +function mountSidebarConfidentialityWidget() { + const el = document.querySelector('.js-sidebar-confidential-widget-root'); + if (!el) { - return; + return null; } const { fullPath, iid } = getSidebarOptions(); const dataNode = document.getElementById('js-confidential-issue-data'); const initialData = JSON.parse(dataNode.innerHTML); - // eslint-disable-next-line no-new - new Vue({ + return new Vue({ el, - name: 'SidebarConfidentialRoot', + name: 'SidebarConfidentialityWidgetRoot', apolloProvider, - components: { - SidebarConfidentialityWidget, - }, provide: { canUpdate: initialData.is_editable, isClassicSidebar: true, }, - render: (createElement) => - createElement('sidebar-confidentiality-widget', { + createElement(SidebarConfidentialityWidget, { props: { iid: String(iid), fullPath, @@ -364,28 +400,24 @@ function mountConfidentialComponent() { }); } -function mountDueDateComponent() { - const el = document.getElementById('js-due-date-entry-point'); +function mountSidebarDueDateWidget() { + const el = document.querySelector('.js-sidebar-due-date-widget-root'); + if (!el) { - return; + return null; } const { fullPath, iid, editable } = getSidebarOptions(); - // eslint-disable-next-line no-new - new Vue({ + return new Vue({ el, - name: 'SidebarDueDateRoot', + name: 'SidebarDueDateWidgetRoot', apolloProvider, - components: { - SidebarDueDateWidget, - }, provide: { canUpdate: editable, }, - render: (createElement) => - createElement('sidebar-due-date-widget', { + createElement(SidebarDueDateWidget, { props: { iid: String(iid), fullPath, @@ -395,29 +427,25 @@ function mountDueDateComponent() { }); } -function mountReferenceComponent() { - const el = document.getElementById('js-reference-entry-point'); +function mountSidebarReferenceWidget() { + const el = document.querySelector('.js-sidebar-reference-widget-root'); + if (!el) { - return; + return null; } const { fullPath, iid } = getSidebarOptions(); - // eslint-disable-next-line no-new - new Vue({ + return new Vue({ el, - name: 'SidebarReferenceRoot', + name: 'SidebarReferenceWidgetRoot', apolloProvider, - components: { - SidebarReferenceWidget, - }, provide: { iid: String(iid), fullPath, }, - render: (createElement) => - createElement('sidebar-reference-widget', { + createElement(SidebarReferenceWidget, { props: { issuableType: isInIssuePage() || isInIncidentPage() || isInDesignPage() @@ -428,17 +456,16 @@ function mountReferenceComponent() { }); } -function mountLockComponent(store) { - const el = document.getElementById('js-lock-entry-point'); +function mountIssuableLockForm(store) { + const el = document.querySelector('.js-sidebar-lock-root'); if (!el || !store) { - return; + return null; } const { fullPath, editable } = getSidebarOptions(); - // eslint-disable-next-line no-new - new Vue({ + return new Vue({ el, name: 'SidebarLockRoot', store, @@ -454,23 +481,21 @@ function mountLockComponent(store) { }); } -function mountParticipantsComponent() { - const el = document.querySelector('.js-sidebar-participants-entry-point'); +function mountSidebarParticipantsWidget() { + const el = document.querySelector('.js-sidebar-participants-widget-root'); - if (!el) return; + if (!el) { + return null; + } const { fullPath, iid } = getSidebarOptions(); - // eslint-disable-next-line no-new - new Vue({ + return new Vue({ el, - name: 'SidebarParticipantsRoot', + name: 'SidebarParticipantsWidgetRoot', apolloProvider, - components: { - SidebarParticipantsWidget, - }, render: (createElement) => - createElement('sidebar-participants-widget', { + createElement(SidebarParticipantsWidget, { props: { iid: String(iid), fullPath, @@ -483,26 +508,24 @@ function mountParticipantsComponent() { }); } -function mountSubscriptionsComponent() { - const el = document.querySelector('.js-sidebar-subscriptions-entry-point'); +function mountSidebarSubscriptionsWidget() { + const el = document.querySelector('.js-sidebar-subscriptions-widget-root'); - if (!el) return; + if (!el) { + return null; + } const { fullPath, iid, editable } = getSidebarOptions(); - // eslint-disable-next-line no-new - new Vue({ + return new Vue({ el, - name: 'SidebarSubscriptionsRoot', + name: 'SidebarSubscriptionsWidgetRoot', apolloProvider, - components: { - SidebarSubscriptionsWidget, - }, provide: { canUpdate: editable, }, render: (createElement) => - createElement('sidebar-subscriptions-widget', { + createElement(SidebarSubscriptionsWidget, { props: { iid: String(iid), fullPath, @@ -515,14 +538,15 @@ function mountSubscriptionsComponent() { }); } -function mountTimeTrackingComponent() { - const el = document.getElementById('issuable-time-tracker'); +function mountSidebarTimeTracking() { + const el = document.querySelector('.js-sidebar-time-tracking-root'); const { id, iid, fullPath, issuableType, timeTrackingLimitToHours } = getSidebarOptions(); - if (!el) return; + if (!el) { + return null; + } - // eslint-disable-next-line no-new - new Vue({ + return new Vue({ el, name: 'SidebarTimeTrackingRoot', apolloProvider, @@ -539,27 +563,24 @@ function mountTimeTrackingComponent() { }); } -function mountSeverityComponent() { - const severityContainerEl = document.querySelector('#js-severity'); +function mountSidebarSeverity() { + const el = document.querySelector('.js-sidebar-severity-root'); - if (!severityContainerEl) { - return false; + if (!el) { + return null; } const { fullPath, iid, severity, editable } = getSidebarOptions(); return new Vue({ - el: severityContainerEl, + el, name: 'SidebarSeverityRoot', apolloProvider, - components: { - SidebarSeverity, - }, provide: { canUpdate: editable, }, render: (createElement) => - createElement('sidebar-severity', { + createElement(SidebarSeverity, { props: { projectPath: fullPath, iid: String(iid), @@ -569,27 +590,25 @@ function mountSeverityComponent() { }); } -function mountEscalationStatusComponent() { - const statusContainerEl = document.querySelector('#js-escalation-status'); +function mountSidebarEscalationStatus() { + const el = document.querySelector('.js-sidebar-escalation-status-root'); - if (!statusContainerEl) { - return false; + if (!el) { + return null; } const { issuableType } = getSidebarOptions(); - const { canUpdate, issueIid, projectPath } = statusContainerEl.dataset; + const { canUpdate, issueIid, projectPath } = el.dataset; return new Vue({ - el: statusContainerEl, + el, + name: 'SidebarEscalationStatusRoot', apolloProvider, - components: { - SidebarEscalationStatus, - }, provide: { canUpdate: parseBoolean(canUpdate), }, render: (createElement) => - createElement('sidebar-escalation-status', { + createElement(SidebarEscalationStatus, { props: { iid: issueIid, issuableType, @@ -599,15 +618,16 @@ function mountEscalationStatusComponent() { }); } -function mountCopyEmailComponent() { - const el = document.getElementById('issuable-copy-email'); +function mountCopyEmailToClipboard() { + const el = document.querySelector('.js-sidebar-copy-email-root'); - if (!el) return; + if (!el) { + return null; + } const { createNoteEmail } = getSidebarOptions(); - // eslint-disable-next-line no-new - new Vue({ + return new Vue({ el, name: 'SidebarCopyEmailRoot', render: (createElement) => @@ -621,36 +641,32 @@ const isAssigneesWidgetShown = export function mountSidebar(mediator, store) { initInviteMembersModal(); initInviteMembersTrigger(); - - mountSidebarToDoWidget(); + mountSidebarTodoWidget(); if (isAssigneesWidgetShown) { - mountAssigneesComponent(); + mountSidebarAssigneesWidget(); } else { - mountAssigneesComponentDeprecated(mediator); + mountSidebarAssigneesDeprecated(mediator); } - mountReviewersComponent(mediator); - mountCrmContactsComponent(); - mountSidebarLabels(); - mountMilestoneSelect(); - mountConfidentialComponent(mediator); - mountDueDateComponent(mediator); - mountReferenceComponent(mediator); - mountLockComponent(store); - mountParticipantsComponent(); - mountSubscriptionsComponent(); - mountCopyEmailComponent(); + mountSidebarReviewers(mediator); + mountSidebarCrmContacts(); + mountSidebarLabelsWidget(); + mountSidebarMilestoneWidget(); + mountSidebarConfidentialityWidget(); + mountSidebarDueDateWidget(); + mountSidebarReferenceWidget(); + mountIssuableLockForm(store); + mountSidebarParticipantsWidget(); + mountSidebarSubscriptionsWidget(); + mountCopyEmailToClipboard(); + mountSidebarTimeTracking(); + mountSidebarSeverity(); + mountSidebarEscalationStatus(); new SidebarMoveIssue( mediator, $('.js-move-issue'), $('.js-move-issue-confirmation-button'), ).init(); - - mountTimeTrackingComponent(); - - mountSeverityComponent(); - - mountEscalationStatusComponent(); } export { getSidebarOptions }; diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js index e2581a8f30e..baf906bb96c 100644 --- a/app/assets/javascripts/sidebar/stores/sidebar_store.js +++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js @@ -138,6 +138,10 @@ export default class SidebarStore { this.assignees = data; } + setReviewersFromRealtime(data) { + this.reviewers = data; + } + setAutocompleteProjects(projects) { this.autocompleteProjects = projects; } |