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