diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
commit | a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch) | |
tree | fb69158581673816a8cd895f9d352dcb3c678b1e /app/assets/javascripts/sidebar | |
parent | d16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff) | |
download | gitlab-ce-a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4.tar.gz |
Add latest changes from gitlab-org/gitlab@14-0-stable-eev14.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/sidebar')
32 files changed, 798 insertions, 143 deletions
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index 26e88523abb..adb573db652 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -45,7 +45,7 @@ export default { }; </script> <template> - <div class="title hide-collapsed"> + <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900"> {{ assigneeTitle }} <gl-loading-icon v-if="loading" inline class="align-bottom" /> <a diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index c3c009e680a..e41bb41dc05 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -48,17 +48,15 @@ export default { <collapsed-assignee-list :users="sortedAssigness" :issuable-type="issuableType" /> <div data-testid="expanded-assignee" class="value hide-collapsed"> - <template v-if="hasNoUsers"> - <span class="assign-yourself no-value"> - {{ __('None') }} - <template v-if="editable"> - - - <button type="button" class="btn-link" @click="assignSelf"> - {{ __('assign yourself') }} - </button> - </template> - </span> - </template> + <span v-if="hasNoUsers" class="no-value" data-testid="no-value"> + {{ __('None') }} + <template v-if="editable"> + - + <button type="button" class="btn-link" data-testid="assign-yourself" @click="assignSelf"> + {{ __('assign yourself') }} + </button> + </template> + </span> <uncollapsed-assignee-list v-else :users="sortedAssigness" :issuable-type="issuableType" /> </div> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index ca95599742a..9840aa4ed66 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -149,7 +149,6 @@ export default { :users="exposeAvailabilityStatus(store.assignees)" :editable="store.editable" :issuable-type="issuableType" - class="value" @assign-self="assignSelf" /> </div> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue index 932be7addc0..d9a974202a3 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue @@ -61,7 +61,7 @@ export default { required: false, default: IssuableType.Issue, validator(value) { - return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); + return [IssuableType.Issue, IssuableType.MergeRequest, IssuableType.Alert].includes(value); }, }, issuableId: { @@ -229,7 +229,7 @@ export default { @expand-widget="expandWidget" /> </template> - <template #default> + <template #default="{ edit }"> <user-select ref="userSelect" v-model="selected" @@ -240,6 +240,7 @@ export default { :allow-multiple-assignees="allowMultipleAssignees" :current-user="currentUser" :issuable-type="issuableType" + :is-editing="edit" class="gl-w-full dropdown-menu-user" @toggle="collapseWidget" @error="showError" @@ -247,7 +248,7 @@ export default { > <template #footer> <gl-dropdown-item v-if="directlyInviteMembers"> - <sidebar-invite-members /> + <sidebar-invite-members :issuable-type="issuableType" /> </gl-dropdown-item> </template ></user-select> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue index 5c32d03e0d4..8ef65ef7308 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue @@ -9,6 +9,17 @@ export default { components: { InviteMembersTrigger, }, + props: { + issuableType: { + type: String, + required: true, + }, + }, + computed: { + triggerSource() { + return `${this.issuableType}-assignee-dropdown`; + }, + }, }; </script> @@ -18,6 +29,7 @@ export default { :display-text="$options.displayText" :event="$options.dataTrackEvent" :label="$options.dataTrackLabel" + :trigger-source="triggerSource" classes="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!" /> </template> 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 6a68e914b84..c3dfa5f8b14 100644 --- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue +++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue @@ -112,6 +112,9 @@ export default { dateValue() { return this.issuable?.[this.dateType] || null; }, + firstDay() { + return gon.first_day_of_week; + }, isLoading() { return this.$apollo.queries.issuable.loading || this.loading; }, @@ -286,6 +289,7 @@ export default { ref="datePicker" class="gl-relative" :default-date="parsedDate" + :first-day="firstDay" show-clear-button autocomplete="off" @input="setDate" diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue index c9b6616e067..b7832ca679c 100644 --- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue +++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue @@ -10,6 +10,8 @@ import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_req import { toLabelGid } from '~/sidebar/utils'; import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants'; import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; +import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; const mutationMap = { [IssuableType.Issue]: { @@ -25,8 +27,10 @@ const mutationMap = { export default { components: { LabelsSelect, + LabelsSelectWidget, }, variant: DropdownVariant.Sidebar, + mixins: [glFeatureFlagMixin()], inject: [ 'allowLabelCreate', 'allowLabelEdit', @@ -135,7 +139,32 @@ export default { </script> <template> + <labels-select-widget + v-if="glFeatures.labelsWidget" + class="block labels js-labels-block" + :allow-label-remove="allowLabelEdit" + :allow-label-create="allowLabelCreate" + :allow-label-edit="allowLabelEdit" + :allow-multiselect="true" + :allow-scoped-labels="allowScopedLabels" + :footer-create-label-title="__('Create project label')" + :footer-manage-label-title="__('Manage project labels')" + :labels-create-title="__('Create project label')" + :labels-fetch-path="labelsFetchPath" + :labels-filter-base-path="projectIssuesPath" + :labels-manage-path="labelsManagePath" + :labels-select-in-progress="isLabelsSelectInProgress" + :selected-labels="selectedLabels" + :variant="$options.sidebar" + data-qa-selector="labels_block" + @onDropdownClose="handleDropdownClose" + @onLabelRemove="handleLabelRemove" + @updateSelectedLabels="handleUpdateSelectedLabels" + > + {{ __('None') }} + </labels-select-widget> <labels-select + v-else class="block labels js-labels-block" :allow-label-remove="allowLabelEdit" :allow-label-create="allowLabelCreate" diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue index 3468acb38e7..81ee0a73739 100644 --- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue @@ -86,7 +86,7 @@ export default { <gl-icon :name="lockStatus.icon" class="sidebar-item-icon is-active" /> </div> - <div class="title hide-collapsed"> + <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900"> {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }} <a v-if="isEditable" diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue deleted file mode 100644 index 4ac515e552a..00000000000 --- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue +++ /dev/null @@ -1,31 +0,0 @@ -<script> -import Store from '../../stores/sidebar_store'; -import participants from './participants.vue'; - -export default { - components: { - participants, - }, - props: { - mediator: { - type: Object, - required: true, - }, - }, - data() { - return { - store: new Store(), - }; - }, -}; -</script> - -<template> - <div class="block participants"> - <participants - :loading="store.isFetching.participants" - :participants="store.participants" - :number-of-less-participants="7" - /> - </div> -</template> diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue index d3043e6f6aa..9927a0f9114 100644 --- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue +++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue @@ -64,5 +64,6 @@ export default { :loading="isLoading" :participants="participants" :number-of-less-participants="7" + class="block participants" /> </template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue index a461d992222..88c0b18ccc7 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue @@ -33,7 +33,7 @@ export default { }; </script> <template> - <div class="title hide-collapsed"> + <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900"> {{ reviewerTitle }} <gl-loading-icon v-if="loading" inline class="align-bottom" /> <a diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue index 2c52d7142f7..5729b958b5d 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 { <div class="value hide-collapsed"> <template v-if="hasNoUsers"> - <span class="assign-yourself no-value"> + <span class="no-value"> {{ __('None') }} </span> </template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index b5cf5df4957..c0bd54c60da 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -103,7 +103,6 @@ export default { :users="store.reviewers" :editable="store.editable" :issuable-type="issuableType" - class="value" @request-review="requestReview" /> </div> diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue index 6a6300dcde0..592cfea5e32 100644 --- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue +++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue @@ -148,7 +148,9 @@ export default { </div> <div class="hide-collapsed"> - <p class="title gl-display-flex gl-justify-content-space-between"> + <p + class="gl-line-height-20 gl-mb-0 gl-text-gray-900 gl-display-flex gl-justify-content-space-between" + > {{ $options.i18n.SEVERITY }} <gl-link data-testid="editButton" diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue new file mode 100644 index 00000000000..c80ccc928b3 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -0,0 +1,360 @@ +<script> +import { + GlLink, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, + GlDropdownDivider, + GlLoadingIcon, + GlIcon, + GlTooltipDirective, +} from '@gitlab/ui'; +import createFlash from '~/flash'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { IssuableType } from '~/issue_show/constants'; +import { __, s__, sprintf } from '~/locale'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import { + IssuableAttributeState, + IssuableAttributeType, + issuableAttributesQueries, + noAttributeId, +} from '../constants'; + +export default { + noAttributeId, + IssuableAttributeState, + issuableAttributesQueries, + i18n: { + [IssuableAttributeType.Milestone]: __('Milestone'), + none: __('None'), + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + SidebarEditableItem, + GlLink, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlDropdownDivider, + GlSearchBoxByType, + GlIcon, + GlLoadingIcon, + }, + inject: { + isClassicSidebar: { + default: false, + }, + }, + props: { + issuableAttribute: { + type: String, + required: true, + validator(value) { + return [IssuableAttributeType.Milestone].includes(value); + }, + }, + workspacePath: { + required: true, + type: String, + }, + iid: { + required: true, + type: String, + }, + attrWorkspacePath: { + required: true, + type: String, + }, + issuableType: { + type: String, + required: true, + validator(value) { + return value === IssuableType.Issue; + }, + }, + }, + apollo: { + currentAttribute: { + query() { + const { current } = this.issuableAttributeQuery; + const { query } = current[this.issuableType]; + + return query; + }, + variables() { + return { + fullPath: this.workspacePath, + iid: this.iid, + }; + }, + update(data) { + return data?.workspace?.issuable.attribute; + }, + error(error) { + createFlash({ + message: this.i18n.currentFetchError, + captureError: true, + error, + }); + }, + }, + attributesList: { + query() { + const { list } = this.issuableAttributeQuery; + const { query } = list[this.issuableType]; + + return query; + }, + skip() { + return !this.editing; + }, + debounce: 250, + variables() { + return { + fullPath: this.attrWorkspacePath, + title: this.searchTerm, + state: this.$options.IssuableAttributeState[this.issuableAttribute], + }; + }, + update(data) { + if (data?.workspace) { + return data?.workspace?.attributes.nodes; + } + return []; + }, + error(error) { + createFlash({ message: this.i18n.listFetchError, captureError: true, error }); + }, + }, + }, + data() { + return { + searchTerm: '', + editing: false, + updating: false, + selectedTitle: null, + currentAttribute: null, + attributesList: [], + tracking: { + label: 'right_sidebar', + event: 'click_edit_button', + property: this.issuableAttribute, + }, + }; + }, + computed: { + issuableAttributeQuery() { + return this.$options.issuableAttributesQueries[this.issuableAttribute]; + }, + attributeTitle() { + return this.currentAttribute?.title || this.i18n.noAttribute; + }, + attributeUrl() { + return this.currentAttribute?.webUrl; + }, + dropdownText() { + return this.currentAttribute + ? this.currentAttribute?.title + : this.$options.i18n[this.issuableAttribute]; + }, + loading() { + return this.$apollo.queries.currentAttribute.loading; + }, + emptyPropsList() { + return this.attributesList.length === 0; + }, + attributeTypeTitle() { + return this.$options.i18n[this.issuableAttribute]; + }, + i18n() { + return { + noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), { + issuableAttribute: this.issuableAttribute, + }), + assignAttribute: sprintf(s__('DropdownWidget|Assign %{issuableAttribute}'), { + issuableAttribute: this.issuableAttribute, + }), + noAttributesFound: sprintf(s__('DropdownWidget|No %{issuableAttribute} found'), { + issuableAttribute: this.issuableAttribute, + }), + updateError: sprintf( + s__( + 'DropdownWidget|Failed to set %{issuableAttribute} on this %{issuableType}. Please try again.', + ), + { issuableAttribute: this.issuableAttribute, issuableType: this.issuableType }, + ), + listFetchError: sprintf( + s__( + 'DropdownWidget|Failed to fetch the %{issuableAttribute} for this %{issuableType}. Please try again.', + ), + { issuableAttribute: this.issuableAttribute, issuableType: this.issuableType }, + ), + currentFetchError: sprintf( + s__( + 'DropdownWidget|An error occurred while fetching the assigned %{issuableAttribute} of the selected %{issuableType}.', + ), + { issuableAttribute: this.issuableAttribute, issuableType: this.issuableType }, + ), + }; + }, + }, + methods: { + updateAttribute(attributeId) { + if (this.currentAttribute === null && attributeId === null) return; + if (attributeId === this.currentAttribute?.id) return; + + this.updating = true; + + const selectedAttribute = + Boolean(attributeId) && this.attributesList.find((p) => p.id === attributeId); + this.selectedTitle = selectedAttribute ? selectedAttribute.title : this.$options.i18n.none; + + const { current } = this.issuableAttributeQuery; + const { mutation } = current[this.issuableType]; + + this.$apollo + .mutate({ + mutation, + variables: { + fullPath: this.workspacePath, + attributeId: + this.issuableAttribute === IssuableAttributeType.Milestone + ? getIdFromGraphQLId(attributeId) + : attributeId, + iid: this.iid, + }, + }) + .then(({ data }) => { + if (data.issuableSetAttribute?.errors?.length) { + createFlash({ + message: data.issuableSetAttribute.errors[0], + captureError: true, + error: data.issuableSetAttribute.errors[0], + }); + } else { + this.$emit('attribute-updated', data); + } + }) + .catch((error) => { + createFlash({ message: this.i18n.updateError, captureError: true, error }); + }) + .finally(() => { + this.updating = false; + this.searchTerm = ''; + this.selectedTitle = null; + }); + }, + isAttributeChecked(attributeId = undefined) { + return ( + attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId) + ); + }, + showDropdown() { + this.$refs.newDropdown.show(); + }, + handleOpen() { + this.editing = true; + this.showDropdown(); + }, + handleClose() { + this.editing = false; + }, + setFocus() { + this.$refs.search.focusInput(); + }, + }, +}; +</script> + +<template> + <sidebar-editable-item + ref="editable" + :title="attributeTypeTitle" + :data-testid="`${issuableAttribute}-edit`" + :tracking="tracking" + :loading="updating || loading" + @open="handleOpen" + @close="handleClose" + > + <template #collapsed> + <div v-if="isClassicSidebar" v-gl-tooltip class="sidebar-collapsed-icon"> + <gl-icon :size="16" :aria-label="attributeTypeTitle" :name="issuableAttribute" /> + <span class="collapse-truncated-title">{{ attributeTitle }}</span> + </div> + <div + :data-testid="`select-${issuableAttribute}`" + :class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'" + > + <span v-if="updating" class="gl-font-weight-bold">{{ selectedTitle }}</span> + <span v-else-if="!currentAttribute" class="gl-text-gray-500"> + {{ $options.i18n.none }} + </span> + <slot + v-else + name="value" + :attributeTitle="attributeTitle" + :attributeUrl="attributeUrl" + :currentAttribute="currentAttribute" + > + <gl-link class="gl-text-gray-900! gl-font-weight-bold" :href="attributeUrl"> + {{ attributeTitle }} + </gl-link> + </slot> + </div> + </template> + <template #default> + <gl-dropdown + ref="newDropdown" + lazy + :header-text="i18n.assignAttribute" + :text="dropdownText" + :loading="loading" + class="gl-w-full" + @shown="setFocus" + > + <gl-search-box-by-type ref="search" v-model="searchTerm" /> + <gl-dropdown-item + :data-testid="`no-${issuableAttribute}-item`" + :is-check-item="true" + :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" + 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" + :attributesList="attributesList" + :isAttributeChecked="isAttributeChecked" + :updateAttribute="updateAttribute" + > + <gl-dropdown-item + v-for="attrItem in attributesList" + :key="attrItem.id" + :is-check-item="true" + :is-checked="isAttributeChecked(attrItem.id)" + :data-testid="`${issuableAttribute}-items`" + @click="updateAttribute(attrItem.id)" + > + {{ attrItem.title }} + </gl-dropdown-item> + </slot> + </template> + </gl-dropdown> + </template> + </sidebar-editable-item> +</template> diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue index 0fb8d762c7c..825d7ff5841 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue @@ -109,8 +109,13 @@ export default { <template> <div> - <div class="gl-display-flex gl-align-items-center" @click.self="collapse"> - <span class="hide-collapsed" data-testid="title" @click="collapse">{{ title }}</span> + <div + class="gl-display-flex gl-align-items-center gl-line-height-20 gl-mb-2 gl-text-gray-900" + @click.self="collapse" + > + <span class="hide-collapsed" data-testid="title" @click="collapse"> + {{ title }} + </span> <slot name="title-extra"></slot> <gl-loading-icon v-if="loading || initialLoading" inline class="gl-ml-2 hide-collapsed" /> <gl-loading-icon @@ -135,7 +140,7 @@ export default { </gl-button> </div> <template v-if="!initialLoading"> - <div v-show="!edit" data-testid="collapsed-content"> + <div v-show="!edit" data-testid="collapsed-content" class="gl-line-height-14"> <slot name="collapsed">{{ __('None') }}</slot> </div> <div v-show="edit" data-testid="expanded-content" :class="{ 'gl-mt-3': !isClassicSidebar }"> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue index ee7502e3457..e97742a1339 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue @@ -24,7 +24,6 @@ export default { GlToggle, SidebarEditableItem, }, - inject: ['canUpdate'], props: { iid: { type: String, @@ -102,6 +101,12 @@ export default { parent: this.parentIsGroup ? 'group' : 'project', }); }, + isLoggedIn() { + return Boolean(gon.current_user_id); + }, + canSubscribe() { + return this.emailsDisabled || !this.isLoggedIn; + }, }, methods: { setSubscribed(subscribed) { @@ -174,7 +179,7 @@ export default { <gl-toggle :value="subscribed" :is-loading="isLoading" - :disabled="emailsDisabled || !canUpdate" + :disabled="canSubscribe" class="hide-collapsed gl-ml-auto" data-testid="subscription-toggle" :label="$options.i18n.notifications" diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index 99302993b9a..3705d725a15 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -70,7 +70,7 @@ export default { </script> <template> - <div data-testid="timeTrackingComparisonPane"> + <div class="gl-mt-2" data-testid="timeTrackingComparisonPane"> <div v-gl-tooltip data-testid="compareMeter" diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue index 67242b3b5b7..f91a78b7f1d 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue @@ -13,13 +13,17 @@ export default { GlLoadingIcon, GlTable, }, - inject: ['issuableId', 'issuableType'], + inject: ['issuableType'], props: { limitToHours: { type: Boolean, default: false, required: false, }, + issuableId: { + type: String, + required: true, + }, }, data() { return { report: [], isLoading: true }; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue index c70d99ac178..58167b3934a 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue @@ -5,19 +5,27 @@ import { intersection } from 'lodash'; import '~/smart_interval'; import eventHub from '../../event_hub'; -import Mediator from '../../sidebar_mediator'; -import Store from '../../stores/sidebar_store'; import IssuableTimeTracker from './time_tracker.vue'; export default { components: { IssuableTimeTracker, }, - data() { - return { - mediator: new Mediator(), - store: new Store(), - }; + props: { + fullPath: { + type: String, + required: false, + default: '', + }, + issuableIid: { + type: String, + required: true, + }, + limitToHours: { + type: Boolean, + required: false, + default: false, + }, }, mounted() { this.listenForQuickActions(); @@ -41,7 +49,7 @@ export default { changedCommands = []; } if (changedCommands && intersection(subscribedCommands, changedCommands).length) { - this.mediator.fetch(); + eventHub.$emit('timeTracker:refresh'); } }, }, @@ -51,11 +59,9 @@ export default { <template> <div class="block"> <issuable-time-tracker - :time-estimate="store.timeEstimate" - :time-spent="store.totalTimeSpent" - :human-time-estimate="store.humanTimeEstimate" - :human-time-spent="store.humanTotalTimeSpent" - :limit-to-hours="store.timeTrackingLimitToHours" + :full-path="fullPath" + :issuable-iid="issuableIid" + :limit-to-hours="limitToHours" /> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index 64f2ddc1d16..3feff8639a1 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -1,6 +1,9 @@ <script> -import { GlIcon, GlLink, GlModal, GlModalDirective } from '@gitlab/ui'; +import { GlIcon, GlLink, GlModal, GlModalDirective, GlLoadingIcon } from '@gitlab/ui'; +import { IssuableType } from '~/issue_show/constants'; import { s__, __ } from '~/locale'; +import { timeTrackingQueries } from '~/sidebar/constants'; + import eventHub from '../../event_hub'; import TimeTrackingCollapsedState from './collapsed_state.vue'; import TimeTrackingComparisonPane from './comparison_pane.vue'; @@ -18,6 +21,7 @@ export default { GlIcon, GlLink, GlModal, + GlLoadingIcon, TimeTrackingCollapsedState, TimeTrackingSpentOnlyPane, TimeTrackingComparisonPane, @@ -27,29 +31,27 @@ export default { directives: { GlModal: GlModalDirective, }, + inject: ['issuableType'], props: { - timeEstimate: { - type: Number, - required: true, - }, - timeSpent: { - type: Number, - required: true, + limitToHours: { + type: Boolean, + default: false, + required: false, }, - humanTimeEstimate: { + fullPath: { type: String, required: false, default: '', }, - humanTimeSpent: { + issuableIid: { type: String, required: false, default: '', }, - limitToHours: { - type: Boolean, - default: false, + initialTimeTracking: { + type: Object, required: false, + default: null, }, /* In issue list, "time-tracking-collapsed-state" is always rendered even if the sidebar isn't collapsed. @@ -70,47 +72,103 @@ export default { data() { return { showHelp: false, + timeTracking: { + ...this.initialTimeTracking, + }, }; }, + apollo: { + issuableTimeTracking: { + query() { + return timeTrackingQueries[this.issuableType].query; + }, + skip() { + // We don't fetch info via GraphQL in following cases + // 1. Time tracking info was provided via prop + // 2. issuableIid and fullPath are not provided. + if (!this.initialTimeTracking) { + return false; + } else if (this.issuableIid && this.fullPath) { + return false; + } + return true; + }, + variables() { + return { + iid: this.issuableIid, + fullPath: this.fullPath, + }; + }, + update(data) { + this.timeTracking = { + ...data.workspace?.issuable, + }; + }, + }, + }, computed: { - hasTimeSpent() { - return Boolean(this.timeSpent); + isTimeTrackingInfoLoading() { + return this.$apollo?.queries.issuableTimeTracking.loading ?? false; + }, + timeEstimate() { + return this.timeTracking?.timeEstimate || 0; + }, + totalTimeSpent() { + return this.timeTracking?.totalTimeSpent || 0; + }, + humanTimeEstimate() { + return this.timeTracking?.humanTimeEstimate || ''; + }, + humanTotalTimeSpent() { + return this.timeTracking?.humanTotalTimeSpent || ''; + }, + hasTotalTimeSpent() { + return Boolean(this.totalTimeSpent); }, hasTimeEstimate() { return Boolean(this.timeEstimate); }, showComparisonState() { - return this.hasTimeEstimate && this.hasTimeSpent; + return this.hasTimeEstimate && this.hasTotalTimeSpent; }, showEstimateOnlyState() { - return this.hasTimeEstimate && !this.hasTimeSpent; + return this.hasTimeEstimate && !this.hasTotalTimeSpent; }, showSpentOnlyState() { - return this.hasTimeSpent && !this.hasTimeEstimate; + return this.hasTotalTimeSpent && !this.hasTimeEstimate; }, showNoTimeTrackingState() { - return !this.hasTimeEstimate && !this.hasTimeSpent; + return !this.hasTimeEstimate && !this.hasTotalTimeSpent; }, showHelpState() { return Boolean(this.showHelp); }, + isTimeReportSupported() { + return ( + [IssuableType.Issue, IssuableType.MergeRequest].includes(this.issuableType) && + this.issuableIid + ); + }, + }, + watch: { + /** + * When `initialTimeTracking` is provided via prop, + * we don't query the same via GraphQl and instead + * monitor it for any updates (eg; Epic Swimlanes) + */ + initialTimeTracking(timeTracking) { + this.timeTracking = timeTracking; + }, }, created() { - eventHub.$on('timeTracker:updateData', this.update); + eventHub.$on('timeTracker:refresh', this.refresh); }, methods: { toggleHelpState(show) { this.showHelp = show; }, - update(data) { - const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = data; - - /* eslint-disable vue/no-mutating-props */ - this.timeEstimate = timeEstimate; - this.timeSpent = timeSpent; - this.humanTimeEstimate = humanTimeEstimate; - this.humanTimeSpent = humanTimeSpent; - /* eslint-enable vue/no-mutating-props */ + refresh() { + this.$apollo.queries.issuableTimeTracking.refetch(); }, }, }; @@ -125,11 +183,12 @@ export default { :show-help-state="showHelpState" :show-spent-only-state="showSpentOnlyState" :show-estimate-only-state="showEstimateOnlyState" - :time-spent-human-readable="humanTimeSpent" + :time-spent-human-readable="humanTotalTimeSpent" :time-estimate-human-readable="humanTimeEstimate" /> - <div class="title hide-collapsed gl-mb-3"> + <div class="hide-collapsed gl-line-height-20 gl-text-gray-900"> {{ __('Time tracking') }} + <gl-loading-icon v-if="isTimeTrackingInfoLoading" inline /> <div v-if="!showHelpState" data-testid="helpButton" @@ -147,14 +206,14 @@ export default { <gl-icon name="close" /> </div> </div> - <div class="time-tracking-content hide-collapsed"> + <div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed"> <div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane"> <span class="gl-font-weight-bold">{{ $options.i18n.estimatedOnlyText }} </span >{{ humanTimeEstimate }} </div> <time-tracking-spent-only-pane v-if="showSpentOnlyState" - :time-spent-human-readable="humanTimeSpent" + :time-spent-human-readable="humanTotalTimeSpent" /> <div v-if="showNoTimeTrackingState" data-testid="noTrackingPane"> <span class="gl-text-gray-500">{{ $options.i18n.noTimeTrackingText }}</span> @@ -162,26 +221,28 @@ export default { <time-tracking-comparison-pane v-if="showComparisonState" :time-estimate="timeEstimate" - :time-spent="timeSpent" - :time-spent-human-readable="humanTimeSpent" + :time-spent="totalTimeSpent" + :time-spent-human-readable="humanTotalTimeSpent" :time-estimate-human-readable="humanTimeEstimate" :limit-to-hours="limitToHours" /> - <gl-link - v-if="hasTimeSpent" - v-gl-modal="'time-tracking-report'" - data-testid="reportLink" - href="#" - class="btn-link" - >{{ __('Time tracking report') }}</gl-link - > - <gl-modal - modal-id="time-tracking-report" - :title="__('Time tracking report')" - :hide-footer="true" - > - <time-tracking-report :limit-to-hours="limitToHours" /> - </gl-modal> + <template v-if="isTimeReportSupported"> + <gl-link + v-if="hasTotalTimeSpent" + v-gl-modal="'time-tracking-report'" + data-testid="reportLink" + href="#" + > + {{ __('Time tracking report') }} + </gl-link> + <gl-modal + modal-id="time-tracking-report" + :title="__('Time tracking report')" + :hide-footer="true" + > + <time-tracking-report :limit-to-hours="limitToHours" :issuable-iid="issuableIid" /> + </gl-modal> + </template> <transition name="help-state-toggle"> <time-tracking-help-state v-if="showHelpState" /> </transition> diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index a4e6d8854d1..e8e69c19d9f 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -9,8 +9,10 @@ import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.g import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql'; import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql'; +import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.graphql'; import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql'; import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql'; +import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql'; import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql'; import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql'; import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql'; @@ -19,6 +21,8 @@ import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_conf import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql'; import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql'; import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql'; +import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql'; +import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql'; import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql'; import issueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; import getIssueTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql'; @@ -27,6 +31,9 @@ import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql'; import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql'; import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.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'; export const ASSIGNEES_DEBOUNCE_DELAY = 250; @@ -40,6 +47,10 @@ export const assigneesQueries = { query: getMergeRequestAssignees, mutation: updateMergeRequestAssigneesMutation, }, + [IssuableType.Alert]: { + query: getAlertAssignees, + mutation: updateAlertAssigneesMutation, + }, }; export const participantsQueries = { @@ -52,6 +63,10 @@ export const participantsQueries = { [IssuableType.Epic]: { query: epicParticipantsQuery, }, + [IssuableType.Alert]: { + query: '', + skipQuery: true, + }, }; export const confidentialityQueries = { @@ -107,6 +122,15 @@ export const subscribedQueries = { }, }; +export const timeTrackingQueries = { + [IssuableType.Issue]: { + query: issueTimeTrackingQuery, + }, + [IssuableType.MergeRequest]: { + query: mergeRequestTimeTrackingQuery, + }, +}; + export const dueDateQueries = { [IssuableType.Issue]: { query: issueDueDateQuery, @@ -133,3 +157,33 @@ export const timelogQueries = { query: getMrTimelogsQuery, }, }; + +export const noAttributeId = null; + +export const issuableMilestoneQueries = { + [IssuableType.Issue]: { + query: projectIssueMilestoneQuery, + mutation: projectIssueMilestoneMutation, + }, +}; + +export const milestonesQueries = { + [IssuableType.Issue]: { + query: projectMilestonesQuery, + }, +}; + +export const IssuableAttributeType = { + Milestone: 'milestone', +}; + +export const IssuableAttributeState = { + [IssuableAttributeType.Milestone]: 'active', +}; + +export const issuableAttributesQueries = { + [IssuableAttributeType.Milestone]: { + current: issuableMilestoneQueries, + list: milestonesQueries, + }, +}; diff --git a/app/assets/javascripts/sidebar/graphql.js b/app/assets/javascripts/sidebar/graphql.js index 8615b52f1b8..1a806a051b7 100644 --- a/app/assets/javascripts/sidebar/graphql.js +++ b/app/assets/javascripts/sidebar/graphql.js @@ -1,5 +1,7 @@ import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; +import produce from 'immer'; import VueApollo from 'vue-apollo'; +import getIssueStateQuery from '~/issue_show/queries/get_issue_state.query.graphql'; import createDefaultClient from '~/lib/graphql'; import introspectionQueryResultData from './fragmentTypes.json'; @@ -7,15 +9,24 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({ introspectionQueryResultData, }); -export const defaultClient = createDefaultClient( - {}, - { - cacheConfig: { - fragmentMatcher, +const resolvers = { + Mutation: { + updateIssueState: (_, { issueType = undefined, isDirty = false }, { cache }) => { + const sourceData = cache.readQuery({ query: getIssueStateQuery }); + const data = produce(sourceData, (draftData) => { + draftData.issueState = { issueType, isDirty }; + }); + cache.writeQuery({ query: getIssueStateQuery, data }); }, - assumeImmutableResults: true, }, -); +}; + +export const defaultClient = createDefaultClient(resolvers, { + cacheConfig: { + fragmentMatcher, + }, + assumeImmutableResults: true, +}); export const apolloProvider = new VueApollo({ defaultClient, diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js index b11c8f76a6d..270b22fcdf9 100644 --- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { IssuableType } from '~/issue_show/constants'; import { parseBoolean } from '~/lib/utils/common_utils'; import timeTracker from './components/time_tracking/time_tracker.vue'; @@ -8,7 +9,14 @@ export default class SidebarMilestone { if (!el) return; - const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent, limitToHours } = el.dataset; + const { + timeEstimate, + timeSpent, + humanTimeEstimate, + humanTimeSpent, + limitToHours, + iid, + } = el.dataset; // eslint-disable-next-line no-new new Vue({ @@ -16,14 +24,20 @@ export default class SidebarMilestone { components: { timeTracker, }, + provide: { + issuableType: IssuableType.Milestone, + }, render: (createElement) => createElement('timeTracker', { props: { - timeEstimate: parseInt(timeEstimate, 10), - timeSpent: parseInt(timeSpent, 10), - humanTimeEstimate, - humanTimeSpent, limitToHours: parseBoolean(limitToHours), + issuableIid: iid.toString(), + initialTimeTracking: { + timeEstimate: parseInt(timeEstimate, 10), + totalTimeSpent: parseInt(timeSpent, 10), + humanTimeEstimate, + humanTotalTimeSpent: humanTimeSpent, + }, }, }), }); diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 3f24fdc75dc..f53760eab93 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -2,6 +2,8 @@ import $ from 'jquery'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createFlash from '~/flash'; +import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; +import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { IssuableType } from '~/issue_show/constants'; import { isInIssuePage, @@ -14,14 +16,15 @@ 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 SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue'; import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue'; import { apolloProvider } from '~/sidebar/graphql'; +import trackShowInviteMemberLink from '~/sidebar/track_invite_members'; import Translate from '../vue_shared/translate'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue'; import SidebarLabels from './components/labels/sidebar_labels.vue'; import IssuableLockForm from './components/lock/issuable_lock_form.vue'; -import sidebarParticipants from './components/participants/sidebar_participants.vue'; import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue'; import SidebarSeverity from './components/severity/sidebar_severity.vue'; import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue'; @@ -123,6 +126,12 @@ function mountAssigneesComponent() { }, }), }); + + const assigneeDropdown = document.querySelector('.js-sidebar-assignee-dropdown'); + + if (assigneeDropdown) { + trackShowInviteMemberLink(assigneeDropdown); + } } function mountReviewersComponent(mediator) { @@ -149,6 +158,12 @@ function mountReviewersComponent(mediator) { }, }), }); + + const reviewerDropdown = document.querySelector('.js-sidebar-reviewer-dropdown'); + + if (reviewerDropdown) { + trackShowInviteMemberLink(reviewerDropdown); + } } export function mountSidebarLabels() { @@ -191,6 +206,7 @@ function mountConfidentialComponent() { }, provide: { canUpdate: initialData.is_editable, + isClassicSidebar: true, }, render: (createElement) => @@ -314,21 +330,29 @@ function mountLockComponent() { }); } -function mountParticipantsComponent(mediator) { +function mountParticipantsComponent() { const el = document.querySelector('.js-sidebar-participants-entry-point'); if (!el) return; + const { fullPath, iid } = getSidebarOptions(); + // eslint-disable-next-line no-new new Vue({ el, + apolloProvider, components: { - sidebarParticipants, + SidebarParticipantsWidget, }, render: (createElement) => - createElement('sidebar-participants', { + createElement('sidebar-participants-widget', { props: { - mediator, + iid: String(iid), + fullPath, + issuableType: + isInIssuePage() || isInIncidentPage() || isInDesignPage() + ? IssuableType.Issue + : IssuableType.MergeRequest, }, }), }); @@ -367,7 +391,7 @@ function mountSubscriptionsComponent() { function mountTimeTrackingComponent() { const el = document.getElementById('issuable-time-tracker'); - const { id, issuableType } = getSidebarOptions(); + const { iid, fullPath, issuableType, timeTrackingLimitToHours } = getSidebarOptions(); if (!el) return; @@ -375,8 +399,15 @@ function mountTimeTrackingComponent() { new Vue({ el, apolloProvider, - provide: { issuableId: id, issuableType }, - render: (createElement) => createElement(SidebarTimeTracking, {}), + provide: { issuableType }, + render: (createElement) => + createElement(SidebarTimeTracking, { + props: { + fullPath, + issuableIid: iid.toString(), + limitToHours: timeTrackingLimitToHours, + }, + }), }); } @@ -425,6 +456,9 @@ const isAssigneesWidgetShown = (isInIssuePage() || isInDesignPage()) && gon.features.issueAssigneesWidget; export function mountSidebar(mediator) { + initInviteMembersModal(); + initInviteMembersTrigger(); + if (isAssigneesWidgetShown) { mountAssigneesComponent(); } else { @@ -435,7 +469,7 @@ export function mountSidebar(mediator) { mountDueDateComponent(mediator); mountReferenceComponent(mediator); mountLockComponent(); - mountParticipantsComponent(mediator); + mountParticipantsComponent(); mountSubscriptionsComponent(); mountCopyEmailComponent(); diff --git a/app/assets/javascripts/sidebar/queries/issue_time_tracking.query.graphql b/app/assets/javascripts/sidebar/queries/issue_time_tracking.query.graphql new file mode 100644 index 00000000000..7ac989b5c63 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/issue_time_tracking.query.graphql @@ -0,0 +1,13 @@ +query issueTimeTracking($fullPath: ID!, $iid: String) { + workspace: project(fullPath: $fullPath) { + __typename + issuable: issue(iid: $iid) { + __typename + id + humanTimeEstimate + humanTotalTimeSpent + timeEstimate + totalTimeSpent + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/merge_request_time_tracking.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_time_tracking.query.graphql new file mode 100644 index 00000000000..b1ab1bcbe87 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/merge_request_time_tracking.query.graphql @@ -0,0 +1,13 @@ +query mergeRequestTimeTracking($fullPath: ID!, $iid: String!) { + workspace: project(fullPath: $fullPath) { + __typename + issuable: mergeRequest(iid: $iid) { + __typename + id + humanTimeEstimate + humanTotalTimeSpent + timeEstimate + totalTimeSpent + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql b/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql new file mode 100644 index 00000000000..8db5359dac0 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql @@ -0,0 +1,5 @@ +fragment MilestoneFragment on Milestone { + id + title + webUrl: webPath +} diff --git a/app/assets/javascripts/sidebar/queries/project_issue_milestone.mutation.graphql b/app/assets/javascripts/sidebar/queries/project_issue_milestone.mutation.graphql new file mode 100644 index 00000000000..d88ad8b1087 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/project_issue_milestone.mutation.graphql @@ -0,0 +1,17 @@ +mutation projectIssueMilestoneMutation($fullPath: ID!, $iid: String!, $attributeId: ID) { + issuableSetAttribute: updateIssue( + input: { projectPath: $fullPath, iid: $iid, milestoneId: $attributeId } + ) { + __typename + errors + issuable: issue { + __typename + id + attribute: milestone { + title + id + state + } + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql b/app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql new file mode 100644 index 00000000000..2bc42a0b011 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql @@ -0,0 +1,14 @@ +#import "./milestone.fragment.graphql" + +query projectIssueMilestone($fullPath: ID!, $iid: String!) { + workspace: project(fullPath: $fullPath) { + __typename + issuable: issue(iid: $iid) { + __typename + id + attribute: milestone { + ...MilestoneFragment + } + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql b/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql new file mode 100644 index 00000000000..1237640c468 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql @@ -0,0 +1,13 @@ +#import "./milestone.fragment.graphql" + +query projectMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) { + workspace: project(fullPath: $fullPath) { + __typename + attributes: milestones(searchTitle: $title, state: $state) { + nodes { + ...MilestoneFragment + state + } + } + } +} diff --git a/app/assets/javascripts/sidebar/track_invite_members.js b/app/assets/javascripts/sidebar/track_invite_members.js new file mode 100644 index 00000000000..eab15578f0f --- /dev/null +++ b/app/assets/javascripts/sidebar/track_invite_members.js @@ -0,0 +1,12 @@ +import $ from 'jquery'; +import Tracking from '~/tracking'; + +export default function initTrackInviteMembers(userDropdown) { + const { trackEvent, trackLabel } = userDropdown.querySelector('.js-invite-members-track').dataset; + + $(userDropdown).on('shown.bs.dropdown', () => { + Tracking.event(undefined, trackEvent, { + label: trackLabel, + }); + }); +} |