summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/sidebar/components
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-06-16 18:25:58 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-06-16 18:25:58 +0000
commita5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch)
treefb69158581673816a8cd895f9d352dcb3c678b1e /app/assets/javascripts/sidebar/components
parentd16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff)
downloadgitlab-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')
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue12
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue29
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue31
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue360
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue11
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue32
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue161
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>