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/components | |
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/components')
21 files changed, 577 insertions, 122 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> |