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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-09 03:08:46 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-09 03:08:46 +0300
commit4596c2f5a5aef62aee84c24c26d9dc8db538ef3e (patch)
treec8d6979bc588b9f7c8553c9ed1603a550a9d46a2 /app
parent929b0ad5007d1b9a006b8b9b477f01702f9a780f (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue17
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue39
-rw-r--r--app/assets/javascripts/deprecated_notes.js4
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/constants.js1
-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/mount_milestone_sidebar.js1
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js2
-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
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue42
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue11
-rw-r--r--app/assets/javascripts/work_items/list/components/work_items_list_app.vue44
-rw-r--r--app/assets/javascripts/work_items/list/index.js12
-rw-r--r--app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql56
-rw-r--r--app/assets/javascripts/work_items/utils.js6
-rw-r--r--app/models/concerns/each_batch.rb1
-rw-r--r--app/models/namespace/detail.rb2
-rw-r--r--app/views/groups/work_items/index.html.haml3
-rw-r--r--app/views/shared/notes/_comment_button.html.haml3
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')