Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/sidebar')
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidentiality_dropdown.vue55
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/escalation_status.vue67
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_button.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/severity/severity.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/constants.js1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/set_time_estimate_form.vue215
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue62
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue1
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js1
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js17
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_set_time_estimate.mutation.graphql10
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_set_time_estimate.mutation.graphql10
28 files changed, 423 insertions, 49 deletions
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 5cdebee04ad..9d6a8bf47e0 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { TYPE_ISSUE } from '~/issues/constants';
import CollapsedAssigneeList from './collapsed_assignee_list.vue';
@@ -48,7 +49,7 @@ export default {
<div>
<collapsed-assignee-list :users="sortedAssigness" :issuable-type="issuableType" />
- <div data-testid="expanded-assignee" class="value hide-collapsed">
+ <div class="value hide-collapsed">
<span v-if="hasNoUsers" class="no-value" data-testid="no-value">
{{ __('None') }}
<template v-if="editable">
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidentiality_dropdown.vue b/app/assets/javascripts/sidebar/components/confidential/confidentiality_dropdown.vue
new file mode 100644
index 00000000000..d1463bb813a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/confidential/confidentiality_dropdown.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlCollapsibleListbox,
+ },
+ data() {
+ return {
+ value: null,
+ };
+ },
+ i18n: {
+ defaultDropdownText: __('Select confidentiality'),
+ headerText: __('Change confidentiality'),
+ resetText: __('Reset'),
+ },
+ computed: {
+ toggleText() {
+ return this.value ? null : this.$options.i18n.defaultDropdownText;
+ },
+ },
+ methods: {
+ handleReset() {
+ this.value = null;
+ },
+ },
+ dropdownOptions: [
+ {
+ text: __('Confidential'),
+ value: 'true',
+ },
+ {
+ text: __('Not confidential'),
+ value: 'false',
+ },
+ ],
+};
+</script>
+
+<template>
+ <div>
+ <input type="hidden" name="update[confidentiality]" :value="value" />
+ <gl-collapsible-listbox
+ v-model="value"
+ block
+ :header-text="$options.i18n.headerText"
+ :reset-button-label="$options.i18n.resetText"
+ :toggle-text="toggleText"
+ :items="$options.dropdownOptions"
+ @reset="handleReset"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue b/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue
index 72a572087c7..8203dce67cd 100644
--- a/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue
+++ b/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import {
INCIDENTS_I18N as i18n,
STATUS_ACKNOWLEDGED,
@@ -14,8 +14,7 @@ export default {
i18n,
STATUS_LIST,
components: {
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
},
props: {
value: {
@@ -26,52 +25,64 @@ export default {
return [...STATUS_LIST, null].includes(value);
},
},
- preventDropdownClose: {
- type: Boolean,
+ headerText: {
+ type: String,
required: false,
- default: false,
+ default: null,
+ },
+ statusSubtexts: {
+ type: Object,
+ required: false,
+ default() {
+ return {};
+ },
},
},
+ data() {
+ return {
+ selected: this.value,
+ };
+ },
computed: {
+ statusDropdownOptions() {
+ return this.$options.STATUS_LIST.map((status) => ({
+ text: this.getStatusLabel(status),
+ subtext: this.statusSubtexts[status],
+ value: status,
+ }));
+ },
currentStatusLabel() {
return this.getStatusLabel(this.value);
},
},
+
methods: {
show() {
- this.$refs.dropdown.show();
+ this.$refs.dropdown.open();
},
hide() {
- this.$refs.dropdown.hide();
+ this.$refs.dropdown.close();
},
getStatusLabel,
- hideDropdown(event) {
- if (this.preventDropdownClose) {
- event.preventDefault();
- }
- },
},
};
</script>
<template>
- <gl-dropdown
+ <gl-collapsible-listbox
ref="dropdown"
+ v-model="selected"
+ :header-text="headerText"
block
- :text="currentStatusLabel"
+ :toggle-text="currentStatusLabel"
+ :items="statusDropdownOptions"
toggle-class="dropdown-menu-toggle gl-mb-2"
- @hide="hideDropdown"
+ data-testid="escalation-status-dropdown"
+ @select="$emit('input', selected)"
>
- <slot name="header"> </slot>
- <gl-dropdown-item
- v-for="status in $options.STATUS_LIST"
- :key="status"
- data-testid="status-dropdown-item"
- is-check-item
- :is-checked="status === value"
- @click="$emit('input', status)"
- >
- {{ getStatusLabel(status) }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item="{ item }">
+ <span class="gl-display-block">{{ item.text }}</span>
+ <span v-if="item.subtext" class="gl-font-sm gl-text-gray-500">{{ item.subtext }}</span>
+ </template>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_button.vue
index 864d9b308e7..33299ab56e0 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_button.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_button.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
+// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters } from 'vuex';
// @deprecated This component should only be used when there is no GraphQL API.
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents.vue
index 1c27df2418d..86c544ec52a 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents.vue
@@ -1,4 +1,5 @@
<script>
+// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState } from 'vuex';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue
index 8535398decf..1d4a1601a27 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue
@@ -1,5 +1,6 @@
<script>
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
+// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions } from 'vuex';
// @deprecated This component should only be used when there is no GraphQL API.
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
index 3db962c7fe8..3e4297887f0 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
@@ -7,6 +7,7 @@ import {
GlLink,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters, mapActions } from 'vuex';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
index 1e9edd222c5..50fcd3c9350 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions } from 'vuex';
// @deprecated This component should only be used when there is no GraphQL API.
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
index 583f060be8a..5ca18969f0b 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
@@ -1,6 +1,7 @@
<script>
import { GlLabel } from '@gitlab/ui';
import { sortBy } from 'lodash';
+// eslint-disable-next-line no-restricted-imports
import { mapState } from 'vuex';
import { isScopedLabel } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue
index 74e47b333ef..af4215b663c 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue
@@ -1,5 +1,6 @@
<script>
import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
index 1d8b21700c3..19fe78aca87 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
+import { GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -9,7 +9,6 @@ import LabelItem from './label_item.vue';
export default {
components: {
- GlDropdownForm,
GlDropdownItem,
GlLoadingIcon,
GlIntersectionObserver,
@@ -142,7 +141,7 @@ export default {
<template>
<gl-intersection-observer @appear="onDropdownAppear">
- <gl-dropdown-form class="labels-select-contents-list js-labels-list">
+ <div class="js-labels-list">
<div ref="labelsListContainer" data-testid="dropdown-content">
<gl-loading-icon
v-if="labelsFetchInProgress"
@@ -171,6 +170,6 @@ export default {
</gl-dropdown-item>
</template>
</div>
- </gl-dropdown-form>
+ </div>
</gl-intersection-observer>
</template>
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
index 72567b7d4a4..74c3f08a47b 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
@@ -412,6 +412,7 @@ export default {
:workspace-type="workspaceType"
:attr-workspace-path="attrWorkspacePath"
:label-create-type="labelCreateType"
+ class="gl-mt-3"
@setLabels="handleDropdownClose"
@closeDropdown="collapseEditableItem"
/>
@@ -421,8 +422,8 @@ export default {
<template v-else>
<dropdown-contents
ref="dropdownContents"
- :allow-multiselect="allowMultiselect"
:dropdown-button-text="dropdownButtonText"
+ :allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle"
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
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 606d374158b..fa6ae8f6a1b 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
import $ from 'jquery';
+// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
import { createAlert } from '~/alert';
import { __, sprintf } from '~/locale';
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 1ea8ab19012..165499696de 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -6,6 +6,7 @@ import {
GlTooltipDirective,
GlOutsideDirective as Outside,
} from '@gitlab/ui';
+// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions } from 'vuex';
import { TYPE_ISSUE } from '~/issues/constants';
import { __, sprintf } from '~/locale';
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index bad73273409..7b288e15a3e 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index bd1d9fbff0c..a3282932f84 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/multi-word-component-names -->
<script>
// 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
diff --git a/app/assets/javascripts/sidebar/components/severity/severity.vue b/app/assets/javascripts/sidebar/components/severity/severity.vue
index 776dab98f01..bf1a67d86a1 100644
--- a/app/assets/javascripts/sidebar/components/severity/severity.vue
+++ b/app/assets/javascripts/sidebar/components/severity/severity.vue
@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlIcon } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index c0424dc2873..b13f594603b 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/constants.js b/app/assets/javascripts/sidebar/components/time_tracking/constants.js
index 56e986e3b27..ddfbf5ab2a6 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/constants.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/constants.js
@@ -1 +1,2 @@
export const CREATE_TIMELOG_MODAL_ID = 'create-timelog-modal';
+export const SET_TIME_ESTIMATE_MODAL_ID = 'set-time-estimate-modal';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index 109e1af85ec..70d8024f46a 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlLoadingIcon, GlTableLite, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/alert';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/set_time_estimate_form.vue b/app/assets/javascripts/sidebar/components/time_tracking/set_time_estimate_form.vue
new file mode 100644
index 00000000000..44c5896d658
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/set_time_estimate_form.vue
@@ -0,0 +1,215 @@
+<script>
+import { GlFormGroup, GlFormInput, GlModal, GlAlert, GlLink } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
+import { s__, __, sprintf } from '~/locale';
+import issueSetTimeEstimateMutation from '../../queries/issue_set_time_estimate.mutation.graphql';
+import mergeRequestSetTimeEstimateMutation from '../../queries/merge_request_set_time_estimate.mutation.graphql';
+import { SET_TIME_ESTIMATE_MODAL_ID } from './constants';
+
+const MUTATIONS = {
+ [TYPE_ISSUE]: issueSetTimeEstimateMutation,
+ [TYPE_MERGE_REQUEST]: mergeRequestSetTimeEstimateMutation,
+};
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ GlModal,
+ GlAlert,
+ GlLink,
+ },
+ inject: ['issuableType'],
+ props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ issuableIid: {
+ type: String,
+ required: true,
+ },
+ /**
+ * This object must contain the following keys, used to show
+ * the initial time estimate in the form:
+ * - timeEstimate: the time estimate numeric value
+ * - humanTimeEstimate: the time estimate in human readable format
+ */
+ timeTracking: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ currentEstimate: this.timeTracking.timeEstimate ?? 0,
+ timeEstimate: this.timeTracking.humanTimeEstimate ?? '0h',
+ isSaving: false,
+ isResetting: false,
+ saveError: '',
+ };
+ },
+ computed: {
+ submitDisabled() {
+ return this.isSaving || this.isResetting || this.timeEstimate === '';
+ },
+ resetDisabled() {
+ return this.isSaving || this.isResetting || this.currentEstimate === 0;
+ },
+ primaryProps() {
+ return {
+ text: __('Save'),
+ attributes: {
+ variant: 'confirm',
+ disabled: this.submitDisabled,
+ loading: this.isSaving,
+ },
+ };
+ },
+ secondaryProps() {
+ return this.currentEstimate === 0
+ ? null
+ : {
+ text: __('Remove'),
+ attributes: {
+ disabled: this.resetDisabled,
+ loading: this.isResetting,
+ },
+ };
+ },
+ cancelProps() {
+ return {
+ text: __('Cancel'),
+ };
+ },
+ timeTrackingDocsPath() {
+ return helpPagePath('user/project/time_tracking.md');
+ },
+ modalTitle() {
+ return this.currentEstimate === 0
+ ? s__('TimeTracking|Set time estimate')
+ : s__('TimeTracking|Edit time estimate');
+ },
+ isIssue() {
+ return this.issuableType === TYPE_ISSUE;
+ },
+ modalText() {
+ return sprintf(s__('TimeTracking|Set estimated time to complete this %{issuableTypeName}.'), {
+ issuableTypeName: this.isIssue ? __('issue') : __('merge request'),
+ });
+ },
+ },
+ watch: {
+ timeTracking() {
+ this.currentEstimate = this.timeTracking.timeEstimate ?? 0;
+ this.timeEstimate = this.timeTracking.humanTimeEstimate ?? '0h';
+ },
+ },
+ methods: {
+ resetModal() {
+ this.isSaving = false;
+ this.isResetting = false;
+ this.saveError = '';
+ },
+ close() {
+ this.$refs.modal.close();
+ },
+ saveTimeEstimate(event) {
+ event?.preventDefault();
+
+ if (this.timeEstimate === '') {
+ return;
+ }
+
+ this.isSaving = true;
+ this.updateEstimatedTime(this.timeEstimate);
+ },
+ resetTimeEstimate() {
+ this.isResetting = true;
+ this.updateEstimatedTime('0');
+ },
+ updateEstimatedTime(timeEstimate) {
+ this.saveError = '';
+
+ this.$apollo
+ .mutate({
+ mutation: MUTATIONS[this.issuableType],
+ variables: {
+ input: {
+ projectPath: this.fullPath,
+ iid: this.issuableIid,
+ timeEstimate,
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.issuableSetTimeEstimate?.errors.length) {
+ this.saveError =
+ data.issuableSetTimeEstimate.errors[0].message ||
+ data.issuableSetTimeEstimate.errors[0];
+ } else {
+ this.close();
+ }
+ })
+ .catch((error) => {
+ this.saveError =
+ error?.message || s__('TimeTracking|An error occurred while saving the time estimate.');
+ })
+ .finally(() => {
+ this.isSaving = false;
+ this.isResetting = false;
+ });
+ },
+ },
+ SET_TIME_ESTIMATE_MODAL_ID,
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="modal"
+ :title="modalTitle"
+ :modal-id="$options.SET_TIME_ESTIMATE_MODAL_ID"
+ size="sm"
+ data-testid="set-time-estimate-modal"
+ :action-primary="primaryProps"
+ :action-secondary="secondaryProps"
+ :action-cancel="cancelProps"
+ @hidden="resetModal"
+ @primary.prevent="saveTimeEstimate"
+ @secondary.prevent="resetTimeEstimate"
+ @cancel="close"
+ >
+ <p data-testid="timetracking-docs-link">
+ {{ modalText }}
+
+ <gl-link :href="timeTrackingDocsPath">{{
+ s__('TimeTracking|How do I estimate and track time?')
+ }}</gl-link>
+ </p>
+ <form class="js-quick-submit" @submit.prevent="saveTimeEstimate">
+ <gl-form-group
+ label-for="time-estimate"
+ :label="s__('TimeTracking|Estimate')"
+ :description="
+ s__(
+ `TimeTracking|Enter time as a total duration (for example, 1mo 2w 3d 5h 10m), or specify hours and minutes (for example, 75:30).`,
+ )
+ "
+ >
+ <gl-form-input
+ id="time-estimate"
+ v-model="timeEstimate"
+ data-testid="time-estimate"
+ autocomplete="off"
+ />
+ </gl-form-group>
+ <gl-alert v-if="saveError" variant="danger" class="gl-mt-5" :dismissible="false">
+ {{ saveError }}
+ </gl-alert>
+ <!-- This is needed to have the quick-submit behaviour (with Ctrl + Enter or Cmd + Enter) -->
+ <input type="submit" hidden />
+ </form>
+ </gl-modal>
+</template>
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 06adc048942..54f10cac075 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
@@ -35,6 +35,11 @@ export default {
required: false,
default: true,
},
+ canSetTimeEstimate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
mounted() {
this.listenForQuickActions();
@@ -73,6 +78,7 @@ export default {
:issuable-iid="issuableIid"
:limit-to-hours="limitToHours"
:can-add-time-entries="canAddTimeEntries"
+ :can-set-time-estimate="canSetTimeEstimate"
/>
</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 f6968558122..1d427a871e1 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -18,8 +18,9 @@ import TimeTrackingCollapsedState from './collapsed_state.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
import TimeTrackingReport from './report.vue';
import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
-import { CREATE_TIMELOG_MODAL_ID } from './constants';
+import { CREATE_TIMELOG_MODAL_ID, SET_TIME_ESTIMATE_MODAL_ID } from './constants';
import CreateTimelogForm from './create_timelog_form.vue';
+import SetTimeEstimateForm from './set_time_estimate_form.vue';
export default {
name: 'IssuableTimeTracker',
@@ -38,6 +39,7 @@ export default {
TimeTrackingComparisonPane,
TimeTrackingReport,
CreateTimelogForm,
+ SetTimeEstimateForm,
},
directives: {
GlModal: GlModalDirective,
@@ -94,6 +96,11 @@ export default {
required: false,
default: true,
},
+ canSetTimeEstimate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -181,6 +188,11 @@ export default {
timeTrackingIconName() {
return this.showHelpState ? 'close' : 'question-o';
},
+ timeEstimateTooltip() {
+ return this.hasTimeEstimate
+ ? s__('TimeTracking|Edit estimate')
+ : s__('TimeTracking|Set estimate');
+ },
},
watch: {
/**
@@ -203,6 +215,7 @@ export default {
this.$root.$emit(BV_SHOW_MODAL, CREATE_TIMELOG_MODAL_ID);
},
},
+ setTimeEstimateModalId: SET_TIME_ESTIMATE_MODAL_ID,
};
</script>
@@ -223,18 +236,31 @@ export default {
>
{{ __('Time tracking') }}
<gl-loading-icon v-if="isTimeTrackingInfoLoading" size="sm" class="gl-ml-2" inline />
- <gl-button
- v-if="canAddTimeEntries"
- v-gl-tooltip.left
- category="tertiary"
- size="small"
- class="gl-ml-auto"
- data-testid="add-time-entry-button"
- :title="__('Add time entry')"
- @click="openRegisterTimeSpentModal()"
- >
- <gl-icon name="plus" class="gl-text-gray-900!" />
- </gl-button>
+ <div v-if="canSetTimeEstimate || canAddTimeEntries" class="gl-ml-auto gl-display-flex">
+ <gl-button
+ v-if="canSetTimeEstimate"
+ v-gl-modal="$options.setTimeEstimateModalId"
+ v-gl-tooltip.top
+ category="tertiary"
+ size="small"
+ data-testid="set-time-estimate-button"
+ :title="timeEstimateTooltip"
+ :aria-label="timeEstimateTooltip"
+ >
+ <gl-icon name="timer" class="gl-text-gray-900!" />
+ </gl-button>
+ <gl-button
+ v-if="canAddTimeEntries"
+ v-gl-tooltip.top
+ category="tertiary"
+ size="small"
+ data-testid="add-time-entry-button"
+ :title="__('Add time entry')"
+ @click="openRegisterTimeSpentModal()"
+ >
+ <gl-icon name="plus" class="gl-text-gray-900!" />
+ </gl-button>
+ </div>
</div>
<div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed">
<div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane">
@@ -255,10 +281,11 @@ export default {
:time-estimate-human-readable="humanTimeEstimate"
:limit-to-hours="limitToHours"
/>
- <template v-if="isTimeReportSupported">
+ <div v-if="isTimeReportSupported">
<gl-link
v-if="hasTotalTimeSpent"
v-gl-modal="'time-tracking-report'"
+ class="gl-text-black-normal"
data-testid="reportLink"
href="#"
>
@@ -272,8 +299,13 @@ export default {
>
<time-tracking-report :limit-to-hours="limitToHours" :issuable-id="issuableId" />
</gl-modal>
- </template>
+ </div>
<create-timelog-form :issuable-id="issuableId" />
+ <set-time-estimate-form
+ :full-path="fullPath"
+ :issuable-iid="issuableIid"
+ :time-tracking="timeTracking"
+ />
</div>
</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 d5782e4b371..2c8c23c1152 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlLoadingIcon, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index b0060e4c28d..cb6d503d6ef 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -36,6 +36,7 @@ export default class SidebarMilestone {
humanTotalTimeSpent: humanTimeSpent,
},
canAddTimeEntries: false,
+ canSetTimeEstimate: false,
},
}),
});
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 8f6b855ecd6..1f3119e14db 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -42,6 +42,7 @@ import { IssuableAttributeType } from './constants';
import CrmContacts from './components/crm_contacts/crm_contacts.vue';
import trackShowInviteMemberLink from './track_invite_members';
import MoveIssueButton from './components/move/move_issue_button.vue';
+import ConfidentialityDropdown from './components/confidential/confidentiality_dropdown.vue';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -545,6 +546,7 @@ function mountSidebarTimeTracking() {
issuableType,
timeTrackingLimitToHours,
canCreateTimelogs,
+ editable,
} = getSidebarOptions();
if (!el) {
@@ -564,6 +566,7 @@ function mountSidebarTimeTracking() {
issuableIid: iid.toString(),
limitToHours: timeTrackingLimitToHours,
canAddTimeEntries: canCreateTimelogs,
+ canSetTimeEstimate: parseBoolean(editable),
},
}),
});
@@ -694,6 +697,20 @@ export function mountSubscriptionsDropdown() {
});
}
+export function mountConfidentialityDropdown() {
+ const el = document.querySelector('.js-confidentiality-dropdown');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'ConfidentialityDropdownRoot',
+ render: (createElement) => createElement(ConfidentialityDropdown),
+ });
+}
+
export function mountMoveIssueButton() {
const el = document.querySelector('.js-sidebar-move-issue-block');
diff --git a/app/assets/javascripts/sidebar/queries/issue_set_time_estimate.mutation.graphql b/app/assets/javascripts/sidebar/queries/issue_set_time_estimate.mutation.graphql
new file mode 100644
index 00000000000..3e3ebb3869e
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/issue_set_time_estimate.mutation.graphql
@@ -0,0 +1,10 @@
+mutation issueSetTimeEstimate($input: UpdateIssueInput!) {
+ issuableSetTimeEstimate: updateIssue(input: $input) {
+ errors
+ issuable: issue {
+ id
+ humanTimeEstimate
+ timeEstimate
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/merge_request_set_time_estimate.mutation.graphql b/app/assets/javascripts/sidebar/queries/merge_request_set_time_estimate.mutation.graphql
new file mode 100644
index 00000000000..398b3b1c520
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/merge_request_set_time_estimate.mutation.graphql
@@ -0,0 +1,10 @@
+mutation mergeRequestSetTimeEstimate($input: MergeRequestUpdateInput!) {
+ issuableSetTimeEstimate: mergeRequestUpdate(input: $input) {
+ errors
+ issuable: mergeRequest {
+ id
+ humanTimeEstimate
+ timeEstimate
+ }
+ }
+}