diff options
Diffstat (limited to 'app/assets/javascripts/sidebar/components')
18 files changed, 321 insertions, 46 deletions
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index adb573db652..4b3b22f6db3 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -47,7 +47,7 @@ export default { <template> <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" /> + <gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" /> <a v-if="editable" class="js-sidebar-dropdown-toggle edit-link float-right" diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 9840aa4ed66..c6877226b7d 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -1,6 +1,6 @@ <script> import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; -import { deprecatedCreateFlash as Flash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; import eventHub from '~/sidebar/event_hub'; import Store from '~/sidebar/stores/sidebar_store'; @@ -113,7 +113,9 @@ export default { }) .catch(() => { this.loading = false; - return new Flash(__('Error occurred when saving assignees')); + return createFlash({ + message: __('Error occurred when saving assignees'), + }); }); }, exposeAvailabilityStatus(users) { 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 d9a974202a3..1dd05d3886e 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue @@ -3,6 +3,7 @@ import { GlDropdownItem } from '@gitlab/ui'; import { cloneDeep } from 'lodash'; import Vue from 'vue'; import createFlash from '~/flash'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { IssuableType } from '~/issue_show/constants'; import { __, n__ } from '~/locale'; import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue'; @@ -80,6 +81,8 @@ export default { selected: [], isSettingAssignees: false, isDirty: false, + oldIid: null, + oldSelected: null, }; }, apollo: { @@ -142,6 +145,14 @@ export default { return this.currentUser.username !== undefined; }, }, + watch: { + iid(_, oldIid) { + if (this.isDirty) { + this.oldIid = oldIid; + this.oldSelected = this.selected; + } + }, + }, created() { assigneesWidget.updateAssignees = this.updateAssignees; }, @@ -157,10 +168,14 @@ export default { variables: { ...this.queryVariables, assigneeUsernames, + iid: this.oldIid || this.iid, }, }) .then(({ data }) => { - this.$emit('assignees-updated', data.issuableSetAssignees.issuable.assignees.nodes); + this.$emit('assignees-updated', { + id: getIdFromGraphQLId(data.issuableSetAssignees.issuable.id), + assignees: data.issuableSetAssignees.issuable.assignees.nodes, + }); return data; }) .catch(() => { @@ -176,7 +191,10 @@ export default { saveAssignees() { if (this.isDirty) { this.isDirty = false; - this.updateAssignees(this.selected.map(({ username }) => username)); + const usernames = this.oldSelected || this.selected; + this.updateAssignees(usernames.map(({ username }) => username)); + this.oldIid = null; + this.oldSelected = null; } this.$el.dispatchEvent(hideDropdownEvent); }, diff --git a/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue b/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue index 41b3b6c9a45..bed84dc5706 100644 --- a/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue +++ b/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue @@ -22,8 +22,16 @@ export default { required: false, default: '', }, + pronouns: { + type: String, + required: false, + default: '', + }, }, computed: { + hasPronouns() { + return this.pronouns !== null && this.pronouns.trim() !== ''; + }, isBusy() { return isUserBusy(this.availability); }, @@ -32,9 +40,18 @@ export default { </script> <template> <span :class="containerClasses"> - <gl-sprintf v-if="isBusy" :message="s__('UserAvailability|%{author} (Busy)')"> - <template #author>{{ name }}</template> + <gl-sprintf :message="s__('UserAvailability|%{author} %{spanStart}(Busy)%{spanEnd}')"> + <template #author + >{{ name }} + <span v-if="hasPronouns" class="gl-text-gray-500 gl-font-sm gl-font-weight-normal" + >({{ pronouns }})</span + ></template + > + <template #span="{ content }" + ><span v-if="isBusy" class="gl-text-gray-500 gl-font-sm gl-font-weight-normal">{{ + content + }}</span> + </template> </gl-sprintf> - <template v-else>{{ name }}</template> </span> </template> diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue index 372368707af..dc0f2b54a7b 100644 --- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue +++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue @@ -4,7 +4,7 @@ import Vue from 'vue'; import createFlash from '~/flash'; import { __, sprintf } from '~/locale'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; -import { confidentialityQueries } from '~/sidebar/constants'; +import { confidentialityQueries, Tracking } from '~/sidebar/constants'; import SidebarConfidentialityContent from './sidebar_confidentiality_content.vue'; import SidebarConfidentialityForm from './sidebar_confidentiality_form.vue'; @@ -18,8 +18,8 @@ const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', { export default { tracking: { - event: 'click_edit_button', - label: 'right_sidebar', + event: Tracking.editEvent, + label: Tracking.rightSidebarLabel, property: 'confidentiality', }, components: { 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 c3dfa5f8b14..1ff24dec884 100644 --- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue +++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue @@ -5,7 +5,13 @@ import { IssuableType } from '~/issue_show/constants'; import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility'; import { __, sprintf } from '~/locale'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; -import { dateFields, dateTypes, dueDateQueries, startDateQueries } from '~/sidebar/constants'; +import { + dateFields, + dateTypes, + dueDateQueries, + startDateQueries, + Tracking, +} from '~/sidebar/constants'; import SidebarFormattedDate from './sidebar_formatted_date.vue'; import SidebarInheritDate from './sidebar_inherit_date.vue'; @@ -15,8 +21,8 @@ const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', { export default { tracking: { - event: 'click_edit_button', - label: 'right_sidebar', + event: Tracking.editEvent, + label: Tracking.rightSidebarLabel, }, directives: { GlTooltip: GlTooltipDirective, @@ -149,6 +155,9 @@ export default { }, }, methods: { + epicDatePopoverEl() { + return this.$refs?.epicDatePopover?.$el; + }, closeForm() { this.$refs.editable.collapse(); this.$el.dispatchEvent(hideDropdownEvent); @@ -249,12 +258,7 @@ export default { :aria-label="$options.i18n.help" data-testid="inherit-date-popover" /> - <gl-popover - :target="() => $refs.epicDatePopover.$el" - triggers="focus" - placement="left" - boundary="viewport" - > + <gl-popover :target="epicDatePopoverEl" triggers="focus" placement="left" boundary="viewport"> <p>{{ $options.i18n.dateHelpValidMessage }}</p> <gl-link :href="$options.dateHelpUrl" target="_blank">{{ $options.i18n.learnMore diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue index c3f31a3d220..42d2e456a07 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue @@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui'; import $ from 'jquery'; import { mapActions } from 'vuex'; -import { deprecatedCreateFlash as Flash } from '~/flash'; +import createFlash from '~/flash'; import { __, sprintf } from '../../../locale'; import eventHub from '../../event_hub'; @@ -52,7 +52,9 @@ export default { const flashMessage = __( 'Something went wrong trying to change the locked state of this %{issuableDisplayName}', ); - Flash(sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName })); + createFlash({ + message: sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }), + }); }) .finally(() => { this.closeForm(); diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index e85e416881c..650aa603f18 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -92,11 +92,11 @@ export default { @click="onClickCollapsedIcon" > <gl-icon name="users" /> - <gl-loading-icon v-if="loading" /> + <gl-loading-icon v-if="loading" size="sm" /> <span v-else data-testid="collapsed-count"> {{ participantCount }} </span> </div> <div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2"> - <gl-loading-icon v-if="loading" :inline="true" /> + <gl-loading-icon v-if="loading" size="sm" :inline="true" /> {{ participantLabel }} </div> <div class="participants-list hide-collapsed"> diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue index 88c0b18ccc7..295027186cc 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue @@ -35,7 +35,7 @@ export default { <template> <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" /> + <gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" /> <a v-if="editable" class="js-sidebar-dropdown-toggle edit-link float-right" diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index c0bd54c60da..e414aaf719b 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -2,7 +2,7 @@ // NOTE! For the first iteration, we are simply copying the implementation of Assignees // It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; -import { deprecatedCreateFlash as Flash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; import eventHub from '~/sidebar/event_hub'; import Store from '~/sidebar/stores/sidebar_store'; @@ -80,7 +80,9 @@ export default { }) .catch(() => { this.loading = false; - return new Flash(__('Error occurred when saving reviewers')); + return createFlash({ + message: __('Error occurred when saving reviewers'), + }); }); }, requestReview(data) { diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue index 592cfea5e32..fdf63c23552 100644 --- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue +++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue @@ -181,7 +181,7 @@ export default { </gl-dropdown-item> </gl-dropdown> - <gl-loading-icon v-if="isUpdating" :inline="true" /> + <gl-loading-icon v-if="isUpdating" size="sm" :inline="true" /> <severity-token v-else-if="!isDropdownShowing" :severity="selectedItem" /> </div> diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue index c80ccc928b3..2e00a23de7c 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -16,11 +16,13 @@ import { IssuableType } from '~/issue_show/constants'; import { __, s__, sprintf } from '~/locale'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import { + Tracking, IssuableAttributeState, IssuableAttributeType, issuableAttributesQueries, noAttributeId, -} from '../constants'; + defaultEpicSort, +} from '~/sidebar/constants'; export default { noAttributeId, @@ -28,6 +30,7 @@ export default { issuableAttributesQueries, i18n: { [IssuableAttributeType.Milestone]: __('Milestone'), + expired: __('(expired)'), none: __('None'), }, directives: { @@ -73,9 +76,14 @@ export default { type: String, required: true, validator(value) { - return value === IssuableType.Issue; + return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); }, }, + icon: { + type: String, + required: false, + default: undefined, + }, }, apollo: { currentAttribute: { @@ -117,7 +125,9 @@ export default { return { fullPath: this.attrWorkspacePath, title: this.searchTerm, + in: this.searchTerm && this.issuableAttribute === IssuableType.Epic ? 'TITLE' : undefined, state: this.$options.IssuableAttributeState[this.issuableAttribute], + sort: this.issuableAttribute === IssuableType.Epic ? defaultEpicSort : null, }; }, update(data) { @@ -140,8 +150,8 @@ export default { currentAttribute: null, attributesList: [], tracking: { - label: 'right_sidebar', - event: 'click_edit_button', + event: Tracking.editEvent, + label: Tracking.rightSidebarLabel, property: this.issuableAttribute, }, }; @@ -170,6 +180,9 @@ export default { attributeTypeTitle() { return this.$options.i18n[this.issuableAttribute]; }, + attributeTypeIcon() { + return this.icon || this.issuableAttribute; + }, i18n() { return { noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), { @@ -222,7 +235,8 @@ export default { variables: { fullPath: this.workspacePath, attributeId: - this.issuableAttribute === IssuableAttributeType.Milestone + this.issuableAttribute === IssuableAttributeType.Milestone && + this.issuableType === IssuableType.Issue ? getIdFromGraphQLId(attributeId) : attributeId, iid: this.iid, @@ -253,6 +267,11 @@ export default { attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId) ); }, + isAttributeOverdue(attribute) { + return this.issuableAttribute === IssuableAttributeType.Milestone + ? attribute?.expired + : false; + }, showDropdown() { this.$refs.newDropdown.show(); }, @@ -282,8 +301,10 @@ export default { > <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> + <gl-icon :size="16" :aria-label="attributeTypeTitle" :name="attributeTypeIcon" /> + <span class="collapse-truncated-title"> + {{ attributeTitle }} + </span> </div> <div :data-testid="`select-${issuableAttribute}`" @@ -300,8 +321,13 @@ export default { :attributeUrl="attributeUrl" :currentAttribute="currentAttribute" > - <gl-link class="gl-text-gray-900! gl-font-weight-bold" :href="attributeUrl"> + <gl-link + class="gl-text-gray-900! gl-font-weight-bold" + :href="attributeUrl" + :data-qa-selector="`${issuableAttribute}_link`" + > {{ attributeTitle }} + <span v-if="isAttributeOverdue(currentAttribute)">{{ $options.i18n.expired }}</span> </gl-link> </slot> </div> @@ -328,6 +354,7 @@ export default { <gl-dropdown-divider /> <gl-loading-icon v-if="$apollo.queries.attributesList.loading" + size="sm" class="gl-py-4" data-testid="loading-icon-dropdown" /> @@ -351,6 +378,7 @@ export default { @click="updateAttribute(attrItem.id)" > {{ attrItem.title }} + <span v-if="isAttributeOverdue(attrItem)">{{ $options.i18n.expired }}</span> </gl-dropdown-item> </slot> </template> diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue index 825d7ff5841..7c496cc422a 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue @@ -117,9 +117,15 @@ export default { {{ title }} </span> <slot name="title-extra"></slot> - <gl-loading-icon v-if="loading || initialLoading" inline class="gl-ml-2 hide-collapsed" /> + <gl-loading-icon + v-if="loading || initialLoading" + size="sm" + inline + class="gl-ml-2 hide-collapsed" + /> <gl-loading-icon v-if="loading && isClassicSidebar" + size="sm" inline class="gl-mx-auto gl-my-0 hide-expanded" /> 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 e97742a1339..bc7e377a966 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue @@ -2,17 +2,18 @@ import { GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui'; import createFlash from '~/flash'; import { IssuableType } from '~/issue_show/constants'; +import { isLoggedIn } from '~/lib/utils/common_utils'; import { __, sprintf } from '~/locale'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; -import { subscribedQueries } from '~/sidebar/constants'; +import { subscribedQueries, Tracking } from '~/sidebar/constants'; const ICON_ON = 'notifications'; const ICON_OFF = 'notifications-off'; export default { tracking: { - event: 'click_edit_button', - label: 'right_sidebar', + event: Tracking.editEvent, + label: Tracking.rightSidebarLabel, property: 'subscriptions', }, directives: { @@ -102,7 +103,7 @@ export default { }); }, isLoggedIn() { - return Boolean(gon.current_user_id); + return isLoggedIn(); }, canSubscribe() { return this.emailsDisabled || !this.isLoggedIn; @@ -195,7 +196,7 @@ export default { class="sidebar-collapsed-icon" @click="toggleSubscribed" > - <gl-loading-icon v-if="isLoading" class="sidebar-item-icon is-active" /> + <gl-loading-icon v-if="isLoading" size="sm" class="sidebar-item-icon is-active" /> <gl-icon v-else :name="notificationIcon" :size="16" class="sidebar-item-icon is-active" /> </span> <div v-show="emailsDisabled" class="gl-mt-3 hide-collapsed gl-text-gray-500"> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue index f91a78b7f1d..8a14998910b 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue @@ -1,6 +1,7 @@ <script> import { GlLoadingIcon, GlTable } from '@gitlab/ui'; import createFlash from '~/flash'; +import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import { __ } from '~/locale'; @@ -52,8 +53,7 @@ export default { return this.issuableType === 'issue'; }, getGraphQLEntityType() { - // eslint-disable-next-line @gitlab/require-i18n-strings - return this.isIssue() ? 'Issue' : 'MergeRequest'; + return this.isIssue() ? TYPE_ISSUE : TYPE_MERGE_REQUEST; }, extractTimelogs(data) { const timelogs = data?.issuable?.timelogs?.nodes || []; 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 87ddbbf256a..9a9d03353dc 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -200,7 +200,7 @@ export default { /> <div class="hide-collapsed gl-line-height-20 gl-text-gray-900"> {{ __('Time tracking') }} - <gl-loading-icon v-if="isTimeTrackingInfoLoading" inline /> + <gl-loading-icon v-if="isTimeTrackingInfoLoading" size="sm" inline /> <div v-if="!showHelpState" data-testid="helpButton" diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue new file mode 100644 index 00000000000..a9c4203af22 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue @@ -0,0 +1,195 @@ +<script> +import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { produce } from 'immer'; +import createFlash from '~/flash'; +import { __, sprintf } from '~/locale'; +import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants'; +import { todoLabel } from '~/vue_shared/components/sidebar/todo_toggle//utils'; +import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue'; + +export default { + components: { + GlButton, + GlIcon, + TodoButton, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + inject: { + isClassicSidebar: { + default: false, + }, + }, + props: { + issuableId: { + type: String, + required: true, + }, + issuableIid: { + type: String, + required: true, + }, + fullPath: { + type: String, + required: true, + }, + issuableType: { + required: true, + type: String, + }, + }, + data() { + return { + loading: false, + }; + }, + apollo: { + todoId: { + query() { + return todoQueries[this.issuableType].query; + }, + variables() { + return { + fullPath: this.fullPath, + iid: String(this.issuableIid), + }; + }, + update(data) { + return data.workspace?.issuable?.currentUserTodos.nodes[0]?.id; + }, + result({ data }) { + const currentUserTodos = data.workspace?.issuable?.currentUserTodos?.nodes ?? []; + this.todoId = currentUserTodos[0]?.id; + this.$emit('todoUpdated', currentUserTodos.length > 0); + }, + error() { + createFlash({ + message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), { + issuableType: this.issuableType, + }), + }); + }, + }, + }, + computed: { + todoIdQuery() { + return todoQueries[this.issuableType].query; + }, + todoIdQueryVariables() { + return { + fullPath: this.fullPath, + iid: String(this.issuableIid), + }; + }, + isLoading() { + return this.$apollo.queries?.todoId?.loading || this.loading; + }, + hasTodo() { + return Boolean(this.todoId); + }, + todoMutationType() { + if (this.hasTodo) { + return TodoMutationTypes.MarkDone; + } + return TodoMutationTypes.Create; + }, + collapsedButtonIcon() { + return this.hasTodo ? 'todo-done' : 'todo-add'; + }, + tootltipTitle() { + return todoLabel(this.hasTodo); + }, + }, + methods: { + toggleTodo() { + this.loading = true; + this.$apollo + .mutate({ + mutation: todoMutations[this.todoMutationType], + variables: { + input: { + targetId: !this.hasTodo ? this.issuableId : undefined, + id: this.hasTodo ? this.todoId : undefined, + }, + }, + update: ( + store, + { + data: { + todoMutation: { todo }, + }, + }, + ) => { + const queryProps = { + query: this.todoIdQuery, + variables: this.todoIdQueryVariables, + }; + + const sourceData = store.readQuery(queryProps); + const data = produce(sourceData, (draftState) => { + draftState.workspace.issuable.currentUserTodos.nodes = this.hasTodo ? [] : [todo]; + }); + store.writeQuery({ + data, + ...queryProps, + }); + }, + }) + .then( + ({ + data: { + todoMutation: { errors }, + }, + }) => { + if (errors.length) { + createFlash({ + message: errors[0], + }); + } + }, + ) + .catch(() => { + createFlash({ + message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), { + issuableType: this.issuableType, + }), + }); + }) + .finally(() => { + this.loading = false; + }); + }, + }, +}; +</script> + +<template> + <div data-testid="sidebar-todo"> + <todo-button + :issuable-type="issuableType" + :issuable-id="issuableId" + :is-todo="hasTodo" + :loading="isLoading" + size="small" + class="hide-collapsed" + @click.stop.prevent="toggleTodo" + /> + <gl-button + v-if="isClassicSidebar" + category="tertiary" + type="reset" + class="sidebar-collapsed-icon sidebar-collapsed-container gl-rounded-0! gl-shadow-none!" + @click.stop.prevent="toggleTodo" + > + <gl-icon + v-gl-tooltip.left.viewport + :title="tootltipTitle" + :size="16" + :class="{ 'todo-undone': hasTodo }" + :name="collapsedButtonIcon" + :aria-label="collapsedButtonIcon" + /> + </gl-button> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue index f589e7555b3..f7e76cc2b7f 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue @@ -85,6 +85,6 @@ export default { :name="collapsedButtonIcon" /> <span v-show="!collapsed" class="issuable-todo-inner">{{ buttonLabel }}</span> - <gl-loading-icon v-show="isActionActive" :inline="true" /> + <gl-loading-icon v-show="isActionActive" size="sm" :inline="true" /> </button> </template> |