diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-09 03:08:46 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-09 03:08:46 +0300 |
commit | 4596c2f5a5aef62aee84c24c26d9dc8db538ef3e (patch) | |
tree | c8d6979bc588b9f7c8553c9ed1603a550a9d46a2 /app | |
parent | 929b0ad5007d1b9a006b8b9b477f01702f9a780f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
22 files changed, 463 insertions, 89 deletions
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue index 9394bac4ee8..6e200f987bf 100644 --- a/app/assets/javascripts/boards/components/board_content_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue @@ -6,7 +6,7 @@ import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdow import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql'; import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql'; import { __, s__, sprintf } from '~/locale'; -import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue'; +import SidebarTimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import { INCIDENT } from '~/boards/constants'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -27,7 +27,7 @@ export default { SidebarAssigneesWidget, SidebarDateWidget, SidebarConfidentialityWidget, - BoardSidebarTimeTracker, + SidebarTimeTracker, SidebarLabelsWidget, SidebarSubscriptionsWidget, SidebarDropdownWidget, @@ -75,6 +75,9 @@ export default { isApolloBoard: { default: false, }, + timeTrackingLimitToHours: { + default: false, + }, }, inheritAttrs: false, apollo: { @@ -257,7 +260,15 @@ export default { data-testid="iteration-edit" /> </div> - <board-sidebar-time-tracker /> + <sidebar-time-tracker + :can-add-time-entries="canUpdate" + :can-set-time-estimate="canUpdate" + :full-path="projectPathForActiveIssue" + :issuable-id="activeBoardIssuable.id" + :issuable-iid="activeBoardIssuable.iid" + :limit-to-hours="timeTrackingLimitToHours" + :show-collapsed="false" + /> <sidebar-date-widget :iid="activeBoardIssuable.iid" :full-path="projectPathForActiveIssue" diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue deleted file mode 100644 index b70294c9db3..00000000000 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue +++ /dev/null @@ -1,39 +0,0 @@ -<script> -import { mapGetters } from 'vuex'; -import IssuableTimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; - -export default { - components: { - IssuableTimeTracker, - }, - inject: ['timeTrackingLimitToHours', 'canUpdate'], - computed: { - ...mapGetters(['activeBoardItem']), - initialTimeTracking() { - const { - timeEstimate, - totalTimeSpent, - humanTimeEstimate, - humanTotalTimeSpent, - } = this.activeBoardItem; - return { - timeEstimate, - totalTimeSpent, - humanTimeEstimate, - humanTotalTimeSpent, - }; - }, - }, -}; -</script> - -<template> - <issuable-time-tracker - :issuable-id="activeBoardItem.id.toString()" - :issuable-iid="activeBoardItem.iid.toString()" - :limit-to-hours="timeTrackingLimitToHours" - :initial-time-tracking="initialTimeTracking" - :show-collapsed="false" - :can-add-time-entries="canUpdate" - /> -</template> diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js index 6dbf12054cf..4e5e07c57e4 100644 --- a/app/assets/javascripts/deprecated_notes.js +++ b/app/assets/javascripts/deprecated_notes.js @@ -136,9 +136,9 @@ export default class Notes { // Reopen and close actions for Issue/MR combined with note form submit this.$wrapperEl.on( 'click', - // this oddly written selector needs to match the old style (input with class) as + // this oddly written selector needs to match the old style (button with class) as // well as the new DOM styling from the Vue-based note form - 'input.js-comment-submit-button, .js-comment-submit-button > button:first-child', + 'button.js-comment-submit-button, .js-comment-submit-button > button:first-child', this.postComment, ); this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment); 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/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/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..47563ae1dc3 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -545,6 +545,7 @@ function mountSidebarTimeTracking() { issuableType, timeTrackingLimitToHours, canCreateTimelogs, + editable, } = getSidebarOptions(); if (!el) { @@ -564,6 +565,7 @@ function mountSidebarTimeTracking() { issuableIid: iid.toString(), limitToHours: timeTrackingLimitToHours, canAddTimeEntries: canCreateTimelogs, + canSetTimeEstimate: parseBoolean(editable), }, }), }); 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 + } + } +} diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue index 37aedc4ff09..31dd49ca415 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue @@ -7,8 +7,10 @@ import { isScopedLabel } from '~/lib/utils/common_utils'; import { isExternal, setUrlFragment } from '~/lib/utils/url_utility'; import { __, n__, sprintf } from '~/locale'; import IssuableAssignees from '~/issuable/components/issue_assignees.vue'; -import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; +import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; +import { STATE_CLOSED } from '~/work_items/constants'; +import { isAssigneesWidget, isLabelsWidget } from '~/work_items/utils'; export default { components: { @@ -90,26 +92,41 @@ export default { reference() { return this.issuable.reference || `${this.issuableSymbol}${this.issuable.iid}`; }, + type() { + return this.issuable.type || this.issuable.workItemType?.name.toUpperCase(); + }, labels() { - return this.issuable.labels?.nodes || this.issuable.labels || []; + return ( + this.issuable.labels?.nodes || + this.issuable.labels || + this.issuable.widgets?.find(isLabelsWidget)?.labels.nodes || + [] + ); }, labelIdsString() { return JSON.stringify(this.labels.map((label) => getIdFromGraphQLId(label.id))); }, assignees() { - return this.issuable.assignees?.nodes || this.issuable.assignees || []; + return ( + this.issuable.assignees?.nodes || + this.issuable.assignees || + this.issuable.widgets?.find(isAssigneesWidget)?.assignees.nodes || + [] + ); }, createdAt() { return this.timeFormatted(this.issuable.createdAt); }, + isClosed() { + return this.issuable.state === STATUS_CLOSED || this.issuable.state === STATE_CLOSED; + }, timestamp() { - if (this.issuable.state === STATUS_CLOSED && this.issuable.closedAt) { - return this.issuable.closedAt; - } - return this.issuable.updatedAt; + return this.isClosed && this.issuable.closedAt + ? this.issuable.closedAt + : this.issuable.updatedAt; }, formattedTimestamp() { - if (this.issuable.state === STATUS_CLOSED && this.issuable.closedAt) { + if (this.isClosed && this.issuable.closedAt) { return sprintf(__('closed %{timeago}'), { timeago: this.timeFormatted(this.issuable.closedAt), }); @@ -167,7 +184,10 @@ export default { return Boolean(this.$slots[slotName]); }, scopedLabel(label) { - return this.hasScopedLabelsFeature && isScopedLabel(label); + const allowsScopedLabels = + this.hasScopedLabelsFeature || + this.issuable.widgets?.find(isLabelsWidget)?.allowsScopedLabels; + return allowsScopedLabels && isScopedLabel(label); }, labelTitle(label) { return label.title || label.name; @@ -213,7 +233,7 @@ export default { :checked="checked" :data-id="issuableId" :data-iid="issuableIid" - :data-type="issuable.type" + :data-type="type" @input="$emit('checked-input', $event)" > <span class="gl-sr-only">{{ issuable.title }}</span> @@ -222,7 +242,7 @@ export default { <div data-testid="issuable-title" class="issue-title title"> <work-item-type-icon v-if="showWorkItemTypeIcon" - :work-item-type="issuable.type" + :work-item-type="type" show-tooltip-on-hover /> <gl-icon diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue index a2667a379e1..92560f2da9e 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue @@ -3,7 +3,7 @@ import { GlAvatarLink, GlAvatar } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import toast from '~/vue_shared/plugins/global_toast'; import { __ } from '~/locale'; -import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_ASSIGNEES } from '~/work_items/constants'; +import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import Tracking from '~/tracking'; import { updateDraft, clearDraft } from '~/lib/utils/autosave'; import { renderMarkdown } from '~/notes/utils'; @@ -17,6 +17,7 @@ import NoteActions from '~/work_items/components/notes/work_item_note_actions.vu import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import updateWorkItemNoteMutation from '../../graphql/notes/update_work_item_note.mutation.graphql'; import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql'; +import { isAssigneesWidget } from '../../utils'; import WorkItemCommentForm from './work_item_comment_form.vue'; import WorkItemNoteAwardsList from './work_item_note_awards_list.vue'; @@ -228,8 +229,6 @@ export default { newAssignees = [...this.assignees, this.author]; } - const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES; - const assigneesWidgetIndex = this.workItem.widgets.findIndex(isAssigneesWidget); const editedWorkItemWidgets = [...this.workItem.widgets]; diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue index 5bfb65fe91c..1405a12a101 100644 --- a/app/assets/javascripts/work_items/components/work_item_labels.vue +++ b/app/assets/javascripts/work_items/components/work_item_labels.vue @@ -9,13 +9,8 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { isScopedLabel } from '~/lib/utils/common_utils'; import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql'; - -import { - i18n, - I18N_WORK_ITEM_ERROR_FETCHING_LABELS, - TRACKING_CATEGORY_SHOW, - WIDGET_TYPE_LABELS, -} from '../constants'; +import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS, TRACKING_CATEGORY_SHOW } from '../constants'; +import { isLabelsWidget } from '../utils'; function isTokenSelectorElement(el) { return ( @@ -127,7 +122,7 @@ export default { return this.$apollo.queries.searchLabels.loading; }, labelsWidget() { - return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS); + return this.workItem?.widgets?.find(isLabelsWidget); }, labels() { return this.labelsWidget?.labels?.nodes || []; diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue index 4180d484357..fe7cb719bbb 100644 --- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue +++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue @@ -1,8 +1,11 @@ <script> +import * as Sentry from '@sentry/browser'; import { STATUS_OPEN } from '~/issues/constants'; -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; import { issuableListTabs } from '~/vue_shared/issuable/list/constants'; +import { STATE_CLOSED } from '../../constants'; +import getWorkItemsQuery from '../queries/get_work_items.query.graphql'; export default { i18n: { @@ -12,26 +15,59 @@ export default { components: { IssuableList, }, + inject: ['fullPath'], data() { return { - issues: [], + error: undefined, searchTokens: [], sortOptions: [], state: STATUS_OPEN, + workItems: [], }; }, + apollo: { + workItems: { + query: getWorkItemsQuery, + variables() { + return { + fullPath: this.fullPath, + }; + }, + update(data) { + return data.group.workItems.nodes ?? []; + }, + error(error) { + this.error = s__( + 'WorkItem|Something went wrong when fetching work items. Please try again.', + ); + Sentry.captureException(error); + }, + }, + }, + methods: { + getStatus(issue) { + return issue.state === STATE_CLOSED ? __('Closed') : undefined; + }, + }, }; </script> <template> <issuable-list :current-tab="state" - :issuables="issues" + :error="error" + :issuables="workItems" namespace="work-items" recent-searches-storage-key="issues" :search-input-placeholder="$options.i18n.searchPlaceholder" :search-tokens="searchTokens" + show-work-item-type-icon :sort-options="sortOptions" :tabs="$options.issuableListTabs" - /> + @dismiss-alert="error = undefined" + > + <template #status="{ issuable }"> + {{ getStatus(issuable) }} + </template> + </issuable-list> </template> diff --git a/app/assets/javascripts/work_items/list/index.js b/app/assets/javascripts/work_items/list/index.js index 5b701893471..5cd38600779 100644 --- a/app/assets/javascripts/work_items/list/index.js +++ b/app/assets/javascripts/work_items/list/index.js @@ -1,5 +1,7 @@ import Vue from 'vue'; -import WorkItemsListApp from '~/work_items/list/components/work_items_list_app.vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import WorkItemsListApp from './components/work_items_list_app.vue'; export const mountWorkItemsListApp = () => { const el = document.querySelector('.js-work-items-list-root'); @@ -8,9 +10,17 @@ export const mountWorkItemsListApp = () => { return null; } + Vue.use(VueApollo); + return new Vue({ el, name: 'WorkItemsListRoot', + apolloProvider: new VueApollo({ + defaultClient: createDefaultClient(), + }), + provide: { + fullPath: el.dataset.fullPath, + }, render: (createComponent) => createComponent(WorkItemsListApp), }); }; diff --git a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql new file mode 100644 index 00000000000..7ada2cf12dd --- /dev/null +++ b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql @@ -0,0 +1,56 @@ +query getWorkItems($fullPath: ID!) { + group(fullPath: $fullPath) { + id + workItems { + nodes { + id + author { + id + avatarUrl + name + username + webUrl + } + closedAt + confidential + createdAt + iid + reference(full: true) + state + title + updatedAt + webUrl + widgets { + ... on WorkItemWidgetAssignees { + assignees { + nodes { + id + avatarUrl + name + username + webUrl + } + } + type + } + ... on WorkItemWidgetLabels { + allowsScopedLabels + labels { + nodes { + id + color + description + title + } + } + type + } + } + workItemType { + id + name + } + } + } + } +} diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js index 81dbe56b2ea..5a882977bc2 100644 --- a/app/assets/javascripts/work_items/utils.js +++ b/app/assets/javascripts/work_items/utils.js @@ -1,4 +1,8 @@ -import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants'; +import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_HIERARCHY, WIDGET_TYPE_LABELS } from './constants'; + +export const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES; + +export const isLabelsWidget = (widget) => widget.type === WIDGET_TYPE_LABELS; export const findHierarchyWidgets = (widgets) => widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY); diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb index 79fb81e7820..945d286a2fd 100644 --- a/app/models/concerns/each_batch.rb +++ b/app/models/concerns/each_batch.rb @@ -219,6 +219,7 @@ module EachBatch new_count, last_value = unscoped .from(inner_query) + .unscope(where: :type) .order(count: :desc) .limit(1) .pick(:count, column) diff --git a/app/models/namespace/detail.rb b/app/models/namespace/detail.rb index 2660d11171e..6ac325dfbf7 100644 --- a/app/models/namespace/detail.rb +++ b/app/models/namespace/detail.rb @@ -4,6 +4,8 @@ class Namespace::Detail < ApplicationRecord include IgnorableColumns ignore_column :free_user_cap_over_limt_notified_at, remove_with: '15.7', remove_after: '2022-11-22' + ignore_column :dashboard_notification_at, remove_with: '16.5', remove_after: '2023-08-22' + ignore_column :dashboard_enforcement_at, remove_with: '16.5', remove_after: '2023-08-22' belongs_to :namespace, inverse_of: :namespace_details validates :namespace, presence: true diff --git a/app/views/groups/work_items/index.html.haml b/app/views/groups/work_items/index.html.haml index 1d508289b21..2e3d3dda941 100644 --- a/app/views/groups/work_items/index.html.haml +++ b/app/views/groups/work_items/index.html.haml @@ -1,3 +1,4 @@ - page_title s_('WorkItem|Work items') +- add_page_specific_style 'page_bundles/issuable_list' -.js-work-items-list-root +.js-work-items-list-root{ data: { full_path: @group.full_path } } diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml index 3e880a36e29..bbcd072c762 100644 --- a/app/views/shared/notes/_comment_button.html.haml +++ b/app/views/shared/notes/_comment_button.html.haml @@ -1,4 +1,5 @@ - noteable_name = @note.noteable.human_class_name .js-comment-type-dropdown.float-left.gl-sm-mr-3{ data: { noteable_name: noteable_name } } - %input.btn.gl-button.btn-confirm.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment'), data: { qa_selector: 'comment_button' } } + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'js-comment-button js-comment-submit-button', value: _('Comment'), data: { qa_selector: 'comment_button' }}) do + = _('Comment') |