diff options
Diffstat (limited to 'app/assets/javascripts/sidebar')
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 + } + } +} |