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-07-31 18:11:19 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-07-31 18:11:19 +0300
commit7f98cf51aa49426815fe943a5a8dae2a96b59c01 (patch)
tree89047dcbc3bdddcc28895c1ac950cf080b363d1b /app
parent74ecf758e30be848144df1672b5080a29fafbc0a (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/blob/line_highlighter.js2
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue25
-rw-r--r--app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql8
-rw-r--r--app/assets/javascripts/issues/dashboard/queries/get_issues_counts.query.graphql16
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue25
-rw-r--r--app/assets/javascripts/issues/list/constants.js32
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues.query.graphql12
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql28
-rw-r--r--app/assets/javascripts/issues/list/utils.js4
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue158
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/date_token.vue73
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue28
-rw-r--r--app/assets/javascripts/work_items/components/item_state.vue71
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue1
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue81
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_badge.vue41
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue (renamed from app/assets/javascripts/work_items/components/work_item_state.vue)71
-rw-r--r--app/helpers/issues_helper.rb3
-rw-r--r--app/models/ci/bridge.rb15
-rw-r--r--app/presenters/packages/npm/package_presenter.rb27
-rw-r--r--app/services/ci/retry_job_service.rb2
-rw-r--r--app/services/deployments/create_for_job_service.rb (renamed from app/services/deployments/create_for_build_service.rb)8
-rw-r--r--app/services/environments/create_for_job_service.rb (renamed from app/services/environments/create_for_build_service.rb)6
-rw-r--r--app/workers/ci/initial_pipeline_process_worker.rb2
30 files changed, 540 insertions, 260 deletions
diff --git a/app/assets/javascripts/blob/line_highlighter.js b/app/assets/javascripts/blob/line_highlighter.js
index 1ec204b4034..4258d16b69f 100644
--- a/app/assets/javascripts/blob/line_highlighter.js
+++ b/app/assets/javascripts/blob/line_highlighter.js
@@ -153,7 +153,7 @@ LineHighlighter.prototype.highlightRange = function (range) {
const results = [];
const ref = range[0] <= range[1] ? range : range.reverse();
- for (let lineNumber = ref[0]; lineNumber <= ref[1]; lineNumber += 1) {
+ for (let lineNumber = range[0]; lineNumber <= ref[1]; lineNumber += 1) {
results.push(this.highlightLine(lineNumber));
}
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index eb73f8e0182..9febebf7e55 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -36,6 +36,7 @@ import { getParameterByName } from '~/lib/utils/url_utility';
import {
OPERATORS_IS,
OPERATORS_IS_NOT_OR,
+ OPERATORS_AFTER_BEFORE,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_CONFIDENTIAL,
@@ -44,6 +45,8 @@ import {
TOKEN_TITLE_MY_REACTION,
TOKEN_TITLE_SEARCH_WITHIN,
TOKEN_TITLE_TYPE,
+ TOKEN_TITLE_CREATED,
+ TOKEN_TITLE_CLOSED,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@@ -52,6 +55,8 @@ import {
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_SEARCH_WITHIN,
TOKEN_TYPE_TYPE,
+ TOKEN_TYPE_CREATED,
+ TOKEN_TYPE_CLOSED,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
@@ -63,6 +68,7 @@ const EmojiToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
const LabelToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
+const DateToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/date_token.vue');
const MilestoneToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
@@ -89,6 +95,7 @@ export default {
'emptyStateWithoutFilterSvgPath',
'hasBlockedIssuesFeature',
'hasIssuableHealthStatusFeature',
+ 'hasIssueDateFilterFeature',
'hasIssueWeightsFeature',
'hasScopedLabelsFeature',
'initialSort',
@@ -318,6 +325,24 @@ export default {
fetchEmojis: this.fetchEmojis,
recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-my_reaction',
});
+
+ if (this.hasIssueDateFilterFeature) {
+ tokens.push({
+ type: TOKEN_TYPE_CREATED,
+ title: TOKEN_TITLE_CREATED,
+ icon: 'history',
+ token: DateToken,
+ operators: OPERATORS_AFTER_BEFORE,
+ });
+
+ tokens.push({
+ type: TOKEN_TYPE_CLOSED,
+ title: TOKEN_TITLE_CLOSED,
+ icon: 'history',
+ token: DateToken,
+ operators: OPERATORS_AFTER_BEFORE,
+ });
+ }
}
tokens.sort((a, b) => a.title.localeCompare(b.title));
diff --git a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
index 5c331fe95e2..51e38d44c85 100644
--- a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
@@ -23,6 +23,10 @@ query getDashboardIssues(
$beforeCursor: String
$firstPageSize: Int
$lastPageSize: Int
+ $createdAfter: Time
+ $createdBefore: Time
+ $closedAfter: Time
+ $closedBefore: Time
) {
issues(
search: $search
@@ -44,6 +48,10 @@ query getDashboardIssues(
before: $beforeCursor
first: $firstPageSize
last: $lastPageSize
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) @persist {
nodes {
__persist
diff --git a/app/assets/javascripts/issues/dashboard/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues/dashboard/queries/get_issues_counts.query.graphql
index b36f546e4ab..a91f15f0c04 100644
--- a/app/assets/javascripts/issues/dashboard/queries/get_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues/dashboard/queries/get_issues_counts.query.graphql
@@ -12,6 +12,10 @@ query getDashboardIssuesCount(
$in: [IssuableSearchableField!]
$not: NegatedIssueFilterInput
$or: UnionedIssueFilterInput
+ $createdAfter: Time
+ $createdBefore: Time
+ $closedAfter: Time
+ $closedBefore: Time
) {
openedIssues: issues(
state: opened
@@ -28,6 +32,10 @@ query getDashboardIssuesCount(
in: $in
not: $not
or: $or
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) {
count
}
@@ -46,6 +54,10 @@ query getDashboardIssuesCount(
in: $in
not: $not
or: $or
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) {
count
}
@@ -64,6 +76,10 @@ query getDashboardIssuesCount(
in: $in
not: $not
or: $or
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) {
count
}
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index db3008c17a4..c4e906be94f 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -40,6 +40,7 @@ import {
OPERATORS_IS,
OPERATORS_IS_NOT,
OPERATORS_IS_NOT_OR,
+ OPERATORS_AFTER_BEFORE,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_CONFIDENTIAL,
@@ -51,6 +52,8 @@ import {
TOKEN_TITLE_RELEASE,
TOKEN_TITLE_SEARCH_WITHIN,
TOKEN_TITLE_TYPE,
+ TOKEN_TITLE_CREATED,
+ TOKEN_TITLE_CLOSED,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@@ -62,6 +65,8 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_SEARCH_WITHIN,
TOKEN_TYPE_TYPE,
+ TOKEN_TYPE_CREATED,
+ TOKEN_TYPE_CLOSED,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
@@ -125,6 +130,7 @@ const CrmContactToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue');
const CrmOrganizationToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue');
+const DateToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/date_token.vue');
export default {
i18n,
@@ -166,6 +172,7 @@ export default {
'hasAnyProjects',
'hasBlockedIssuesFeature',
'hasIssuableHealthStatusFeature',
+ 'hasIssueDateFilterFeature',
'hasIssueWeightsFeature',
'hasScopedLabelsFeature',
'initialEmail',
@@ -460,6 +467,24 @@ export default {
{ icon: 'eye', value: 'no', title: this.$options.i18n.confidentialNo },
],
});
+
+ if (this.hasIssueDateFilterFeature) {
+ tokens.push({
+ type: TOKEN_TYPE_CREATED,
+ title: TOKEN_TITLE_CREATED,
+ icon: 'history',
+ token: DateToken,
+ operators: OPERATORS_AFTER_BEFORE,
+ });
+
+ tokens.push({
+ type: TOKEN_TYPE_CLOSED,
+ title: TOKEN_TITLE_CLOSED,
+ icon: 'history',
+ token: DateToken,
+ operators: OPERATORS_AFTER_BEFORE,
+ });
+ }
}
if (this.canReadCrmContact) {
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index a7933803ed4..85e300b6474 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -9,6 +9,8 @@ import {
OPERATOR_IS,
OPERATOR_NOT,
OPERATOR_OR,
+ OPERATOR_AFTER,
+ OPERATOR_BEFORE,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@@ -24,6 +26,8 @@ import {
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
TOKEN_TYPE_SEARCH_WITHIN,
+ TOKEN_TYPE_CREATED,
+ TOKEN_TYPE_CLOSED,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
WORK_ITEM_TYPE_ENUM_INCIDENT,
@@ -416,4 +420,32 @@ export const filtersMap = {
},
},
},
+ [TOKEN_TYPE_CREATED]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'createdBefore',
+ [ALTERNATIVE_FILTER]: 'createdAfter',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_AFTER]: {
+ [ALTERNATIVE_FILTER]: 'created_after',
+ },
+ [OPERATOR_BEFORE]: {
+ [NORMAL_FILTER]: 'created_before',
+ },
+ },
+ },
+ [TOKEN_TYPE_CLOSED]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'closedBefore',
+ [ALTERNATIVE_FILTER]: 'closedAfter',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_AFTER]: {
+ [ALTERNATIVE_FILTER]: 'closed_after',
+ },
+ [OPERATOR_BEFORE]: {
+ [NORMAL_FILTER]: 'closed_before',
+ },
+ },
+ },
};
diff --git a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
index 1018848fb53..23410ea0f81 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
@@ -30,6 +30,10 @@ query getIssues(
$afterCursor: String
$firstPageSize: Int
$lastPageSize: Int
+ $createdAfter: Time
+ $createdBefore: Time
+ $closedAfter: Time
+ $closedBefore: Time
) {
group(fullPath: $fullPath) @skip(if: $isProject) @persist {
id
@@ -57,6 +61,10 @@ query getIssues(
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) {
__persist
pageInfo {
@@ -96,6 +104,10 @@ query getIssues(
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) {
__persist
pageInfo {
diff --git a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
index fdb0eeb5970..7953dc423b6 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
@@ -18,6 +18,10 @@ query getIssuesCount(
$crmOrganizationId: String
$not: NegatedIssueFilterInput
$or: UnionedIssueFilterInput
+ $createdAfter: Time
+ $createdBefore: Time
+ $closedAfter: Time
+ $closedBefore: Time
) {
group(fullPath: $fullPath) @skip(if: $isProject) {
id
@@ -39,6 +43,10 @@ query getIssuesCount(
crmOrganizationId: $crmOrganizationId
not: $not
or: $or
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) {
count
}
@@ -60,6 +68,10 @@ query getIssuesCount(
crmOrganizationId: $crmOrganizationId
not: $not
or: $or
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) {
count
}
@@ -81,6 +93,10 @@ query getIssuesCount(
crmOrganizationId: $crmOrganizationId
not: $not
or: $or
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) {
count
}
@@ -106,6 +122,10 @@ query getIssuesCount(
crmOrganizationId: $crmOrganizationId
not: $not
or: $or
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) {
count
}
@@ -128,6 +148,10 @@ query getIssuesCount(
crmOrganizationId: $crmOrganizationId
not: $not
or: $or
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) {
count
}
@@ -150,6 +174,10 @@ query getIssuesCount(
crmOrganizationId: $crmOrganizationId
not: $not
or: $or
+ createdAfter: $createdAfter
+ createdBefore: $createdBefore
+ closedAfter: $closedAfter
+ closedBefore: $closedBefore
) {
count
}
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index 04ce9a4969b..37df0c8f9ff 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -6,6 +6,7 @@ import {
FILTERED_SEARCH_TERM,
OPERATOR_NOT,
OPERATOR_OR,
+ OPERATOR_AFTER,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@@ -246,8 +247,9 @@ export const isSpecialFilter = (type, data) => {
const getFilterType = ({ type, value: { data, operator } }) => {
const isUnionedAuthor = type === TOKEN_TYPE_AUTHOR && operator === OPERATOR_OR;
const isUnionedLabel = type === TOKEN_TYPE_LABEL && operator === OPERATOR_OR;
+ const isAfter = operator === OPERATOR_AFTER;
- if (isUnionedAuthor || isUnionedLabel) {
+ if (isUnionedAuthor || isUnionedLabel || isAfter) {
return ALTERNATIVE_FILTER;
}
if (isSpecialFilter(type, data)) {
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index 37a6ab0122b..4e26750efc2 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -43,6 +43,7 @@ export default {
isExpanded: Boolean(this.expanded || this.item.is_active),
isMouseOverSection: false,
isMouseOverFlyout: false,
+ keepFlyoutClosed: false,
};
},
computed: {
@@ -77,12 +78,17 @@ export default {
watch: {
isExpanded(newIsExpanded) {
this.$emit('collapse-toggle', newIsExpanded);
+ this.keepFlyoutClosed = !this.newIsExpanded;
},
},
methods: {
handlePointerover(e) {
this.isMouseOverSection = e.pointerType === 'mouse';
},
+ handlePointerleave() {
+ this.isMouseOverSection = false;
+ this.keepFlyoutClosed = false;
+ },
},
};
</script>
@@ -99,7 +105,7 @@ export default {
v-bind="buttonProps"
@click="isExpanded = !isExpanded"
@pointerover="handlePointerover"
- @pointerleave="isMouseOverSection = false"
+ @pointerleave="handlePointerleave"
>
<span
:class="[isActive ? 'gl-bg-blue-500' : 'gl-bg-transparent']"
@@ -124,7 +130,7 @@ export default {
<flyout-menu
v-if="hasFlyout"
- v-show="isMouseOver && !isExpanded"
+ v-show="isMouseOver && !isExpanded && !keepFlyoutClosed"
:target-id="`menu-section-button-${itemId}`"
:items="item.items"
@mouseover="isMouseOverFlyout = true"
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
new file mode 100644
index 00000000000..a57034b5bb7
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
@@ -0,0 +1,158 @@
+<script>
+import * as Sentry from '@sentry/browser';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import axios from '~/lib/utils/axios_utils';
+import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
+import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
+import { i18n, codeQualityPrefixes } from './constants';
+
+const translations = i18n;
+
+export default {
+ name: 'WidgetCodeQuality',
+ components: {
+ MrWidget,
+ },
+ i18n: translations,
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ pollingFinished: false,
+ hasError: false,
+ collapsedData: {},
+ poll: null,
+ };
+ },
+ computed: {
+ summary() {
+ const { new_errors, resolved_errors } = this.collapsedData;
+
+ if (!this.pollingFinished) {
+ return { title: i18n.loading };
+ } else if (this.hasError) {
+ return { title: i18n.error };
+ } else if (
+ this.collapsedData?.new_errors?.length >= 1 &&
+ this.collapsedData?.resolved_errors?.length >= 1
+ ) {
+ return {
+ title: i18n.improvementAndDegradationCopy(
+ i18n.findings(resolved_errors, codeQualityPrefixes.fixed),
+ i18n.findings(new_errors, codeQualityPrefixes.new),
+ ),
+ };
+ } else if (this.collapsedData?.resolved_errors?.length >= 1) {
+ return {
+ title: i18n.singularCopy(i18n.findings(resolved_errors, codeQualityPrefixes.fixed)),
+ };
+ } else if (this.collapsedData?.new_errors?.length >= 1) {
+ return { title: i18n.singularCopy(i18n.findings(new_errors, codeQualityPrefixes.new)) };
+ }
+ return { title: i18n.noChanges };
+ },
+ expandedData() {
+ const fullData = [];
+ this.collapsedData?.new_errors?.forEach((e) => {
+ fullData.push({
+ text: e.check_name
+ ? `${capitalizeFirstCharacter(e.severity)} - ${e.check_name} - ${e.description}`
+ : `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
+ link: {
+ href: e.web_url,
+ text: `${i18n.prependText} ${e.file_path}:${e.line}`,
+ },
+ icon: {
+ name: SEVERITY_ICONS_MR_WIDGET[e.severity],
+ },
+ });
+ });
+
+ this.collapsedData?.resolved_errors?.forEach((e) => {
+ fullData.push({
+ text: e.check_name
+ ? `${capitalizeFirstCharacter(e.severity)} - ${e.check_name} - ${e.description}`
+ : `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
+ supportingText: `${i18n.prependText} ${e.file_path}:${e.line}`,
+ icon: {
+ name: SEVERITY_ICONS_MR_WIDGET[e.severity],
+ },
+ badge: {
+ variant: 'neutral',
+ text: i18n.fixed,
+ },
+ });
+ });
+
+ return fullData;
+ },
+ statusIcon() {
+ if (this.collapsedData?.new_errors?.length >= 1) {
+ return EXTENSION_ICONS.warning;
+ } else if (this.collapsedData?.resolved_errors?.length >= 1) {
+ return EXTENSION_ICONS.success;
+ }
+ return EXTENSION_ICONS.neutral;
+ },
+ shouldCollapse() {
+ const { new_errors: newErrors, resolved_errors: resolvedErrors } = this.collapsedData;
+
+ if ((newErrors?.length === 0 && resolvedErrors?.length === 0) || this.hasError) {
+ return false;
+ }
+ return true;
+ },
+ apiCodeQualityPath() {
+ return this.mr.codequalityReportsPath;
+ },
+ },
+ methods: {
+ setCollapsedError(err) {
+ this.hasError = true;
+
+ Sentry.captureException(err);
+ },
+ fetchCodeQuality() {
+ return axios
+ .get(this.apiCodeQualityPath)
+ .then(({ data, headers = {}, status }) => {
+ if (status === HTTP_STATUS_OK) {
+ this.pollingFinished = true;
+ }
+ if (data) {
+ this.collapsedData = data;
+ }
+ return {
+ headers,
+ status,
+ data,
+ };
+ })
+ .catch((e) => {
+ return this.setCollapsedError(e);
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <mr-widget
+ :fetch-collapsed-data="fetchCodeQuality"
+ :error-text="$options.i18n.error"
+ :has-error="hasError"
+ :content="expandedData"
+ :loading-text="$options.i18n.loading"
+ data-testid="new-cq-widget"
+ :summary="summary"
+ :widget-name="$options.name"
+ :status-icon-name="statusIcon"
+ :is-collapsible="shouldCollapse"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 52a2f42f8ec..db48e68e8f6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -56,7 +56,6 @@ import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variab
import getStateQuery from './queries/get_state.query.graphql';
import getStateSubscription from './queries/get_state.subscription.graphql';
import accessibilityExtension from './extensions/accessibility';
-import codeQualityExtension from './extensions/code_quality';
import testReportExtension from './extensions/test_report';
import ReportWidgetContainer from './components/report_widget_container.vue';
import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
@@ -215,9 +214,6 @@ export default {
return !hasCI && mergeRequestAddCiConfigPath && !isDismissedSuggestPipeline;
},
- shouldRenderCodeQuality() {
- return this.mr?.codequalityReportsPath;
- },
shouldRenderCollaborationStatus() {
return this.mr.allowCollaboration && this.mr.isOpen;
},
@@ -280,11 +276,6 @@ export default {
this.initPostMergeDeploymentsPolling();
}
},
- shouldRenderCodeQuality(newVal) {
- if (newVal) {
- this.registerCodeQualityExtension();
- }
- },
shouldShowAccessibilityReport(newVal) {
if (newVal) {
this.registerAccessibilityExtension();
@@ -534,11 +525,6 @@ export default {
registerExtension(accessibilityExtension);
}
},
- registerCodeQualityExtension() {
- if (this.shouldRenderCodeQuality) {
- registerExtension(codeQualityExtension);
- }
- },
registerTestReportExtension() {
if (this.shouldRenderTestReport) {
registerExtension(testReportExtension);
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 5b98af8c732..39fd3d62c3b 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -17,12 +17,19 @@ export const OPERATOR_NOT = '!=';
export const OPERATOR_NOT_TEXT = __('is not one of');
export const OPERATOR_OR = '||';
export const OPERATOR_OR_TEXT = __('is one of');
+export const OPERATOR_AFTER = '≥';
+export const OPERATOR_AFTER_TEXT = __('on or after');
+export const OPERATOR_BEFORE = '<';
+export const OPERATOR_BEFORE_TEXT = __('before');
export const OPERATORS_IS = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }];
export const OPERATORS_NOT = [{ value: OPERATOR_NOT, description: OPERATOR_NOT_TEXT }];
export const OPERATORS_OR = [{ value: OPERATOR_OR, description: OPERATOR_OR_TEXT }];
+export const OPERATORS_AFTER = [{ value: OPERATOR_AFTER, description: OPERATOR_AFTER_TEXT }];
+export const OPERATORS_BEFORE = [{ value: OPERATOR_BEFORE, description: OPERATOR_BEFORE_TEXT }];
export const OPERATORS_IS_NOT = [...OPERATORS_IS, ...OPERATORS_NOT];
export const OPERATORS_IS_NOT_OR = [...OPERATORS_IS, ...OPERATORS_NOT, ...OPERATORS_OR];
+export const OPERATORS_AFTER_BEFORE = [...OPERATORS_AFTER, ...OPERATORS_BEFORE];
export const OPTION_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
export const OPTION_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
@@ -62,6 +69,8 @@ export const TOKEN_TITLE_STATUS = __('Status');
export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch');
export const TOKEN_TITLE_TYPE = __('Type');
export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within');
+export const TOKEN_TITLE_CREATED = __('Created date');
+export const TOKEN_TITLE_CLOSED = __('Closed date');
export const TOKEN_TYPE_APPROVED_BY = 'approved-by';
export const TOKEN_TYPE_ASSIGNEE = 'assignee';
@@ -88,3 +97,5 @@ export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch';
export const TOKEN_TYPE_TYPE = 'type';
export const TOKEN_TYPE_WEIGHT = 'weight';
export const TOKEN_TYPE_SEARCH_WITHIN = 'in';
+export const TOKEN_TYPE_CREATED = 'created';
+export const TOKEN_TYPE_CLOSED = 'closed';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/date_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/date_token.vue
new file mode 100644
index 00000000000..4446886dc88
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/date_token.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlDatepicker, GlFilteredSearchToken } from '@gitlab/ui';
+import { formatDate } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ GlDatepicker,
+ GlFilteredSearchToken,
+ },
+ props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ selectedDate: null,
+ };
+ },
+ methods: {
+ selectValue(value) {
+ this.selectedDate = formatDate(value, 'yyyy-mm-dd');
+ },
+ close(submitValue) {
+ if (this.selectedDate == null) {
+ return;
+ }
+
+ submitValue(this.selectedDate);
+ },
+ handle() {
+ const listeners = { ...this.$listeners };
+ // If we don't remove this, clicking the month/year in the datepicker will deactivate
+ delete listeners.deactivate;
+ return listeners;
+ },
+ },
+ dataSegmentInputAttributes: {
+ id: 'glfs-datepicker',
+ placeholder: 'YYYY-MM-DD',
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ :value="value"
+ :active="active"
+ :data-segment-input-attributes="$options.dataSegmentInputAttributes"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="handle()"
+ >
+ <template #before-data-segment-input="{ submitValue }">
+ <gl-datepicker
+ class="gl-display-none!"
+ target="#glfs-datepicker"
+ :container="null"
+ @input="selectValue($event)"
+ @close="close(submitValue)"
+ />
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 756d9b42e99..79d14b5f2d0 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -9,6 +9,7 @@ import {
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
+import Tracking from '~/tracking';
import ConfirmForkModal from '~/vue_shared/components/web_ide/confirm_fork_modal.vue';
import { KEY_EDIT, KEY_WEB_IDE, KEY_GITPOD, KEY_PIPELINE_EDITOR } from './constants';
@@ -28,6 +29,8 @@ export const i18n = {
toggleText: __('Edit'),
};
+const TRACKING_ACTION_NAME = 'click_consolidated_edit';
+
export default {
name: 'CEWebIdeLink',
components: {
@@ -40,6 +43,7 @@ export default {
ConfirmForkModal,
},
i18n,
+ mixins: [Tracking.mixin()],
props: {
isFork: {
type: Boolean,
@@ -181,9 +185,9 @@ export default {
key: KEY_EDIT,
text: __('Edit single file'),
secondaryText: __('Edit this file only.'),
- attrs: {
- 'data-track-action': 'click_consolidated_edit',
- 'data-track-label': 'edit',
+ tracking: {
+ action: TRACKING_ACTION_NAME,
+ label: 'single_file',
},
...handleOptions,
};
@@ -223,9 +227,9 @@ export default {
key: KEY_WEB_IDE,
text: this.webIdeActionText,
secondaryText: this.$options.i18n.webIdeText,
- attrs: {
- 'data-track-action': 'click_consolidated_edit_ide',
- 'data-track-label': 'web_ide',
+ tracking: {
+ action: TRACKING_ACTION_NAME,
+ label: 'web_ide',
},
...handleOptions,
};
@@ -253,9 +257,9 @@ export default {
text: __('Edit in pipeline editor'),
secondaryText,
href: this.pipelineEditorUrl,
- attrs: {
- 'data-track-action': 'click_consolidated_pipeline_editor',
- 'data-track-label': 'pipeline_editor',
+ tracking: {
+ action: TRACKING_ACTION_NAME,
+ label: 'pipeline_editor',
},
};
},
@@ -277,6 +281,10 @@ export default {
key: KEY_GITPOD,
text: this.gitpodActionText,
secondaryText,
+ tracking: {
+ action: TRACKING_ACTION_NAME,
+ label: 'gitpod',
+ },
...handleOptions,
};
},
@@ -311,6 +319,7 @@ export default {
this[dataKey] = true;
},
executeAction(action) {
+ this.track(action.tracking.action, { label: action.tracking.label });
action.handle?.();
},
},
@@ -335,7 +344,6 @@ export default {
<gl-disclosure-dropdown-item
v-for="action in actions"
:key="action.key"
- v-bind="action.attrs"
:item="action"
:data-qa-selector="`${action.key}_menu_item`"
@action="executeAction(action)"
diff --git a/app/assets/javascripts/work_items/components/item_state.vue b/app/assets/javascripts/work_items/components/item_state.vue
deleted file mode 100644
index 2100cc67c8c..00000000000
--- a/app/assets/javascripts/work_items/components/item_state.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<script>
-import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
-import { __ } from '~/locale';
-import { STATE_OPEN, STATE_CLOSED } from '../constants';
-
-export default {
- i18n: {
- status: __('Status'),
- },
- states: [
- {
- value: STATE_OPEN,
- text: __('Open'),
- },
- {
- value: STATE_CLOSED,
- text: __('Closed'),
- },
- ],
- components: {
- GlFormGroup,
- GlFormSelect,
- },
- props: {
- state: {
- type: String,
- required: true,
- },
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- currentState() {
- return this.$options.states[this.state];
- },
- },
- methods: {
- setState(newState) {
- if (newState !== this.state) {
- this.$emit('changed', newState);
- }
- },
- },
- labelId: 'work-item-state-select',
-};
-</script>
-
-<template>
- <gl-form-group
- :label="$options.i18n.status"
- :label-for="$options.labelId"
- label-cols="3"
- label-cols-lg="2"
- label-class="gl-pb-0! gl-overflow-wrap-break work-item-field-label"
- class="gl-align-items-center"
- >
- <gl-form-select
- :id="$options.labelId"
- :value="state"
- :options="$options.states"
- :disabled="disabled"
- data-testid="work-item-state-select"
- class="hide-unfocused-input-decoration work-item-field-value gl-w-auto gl-pl-4 gl-my-1"
- :class="{ 'gl-bg-transparent! gl-cursor-text!': disabled }"
- @change="setState"
- />
- </gl-form-group>
-</template>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index c330eccb186..66ad3d50287 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -265,6 +265,7 @@ export default {
:comment-button-text="commentButtonText"
@submitForm="updateWorkItem"
@cancelEditing="cancelEditing"
+ @error="$emit('error', $event)"
/>
<textarea
v-else
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index c317ec48732..b143c529014 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -1,22 +1,13 @@
<script>
import { GlButton, GlFormCheckbox, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__, __, sprintf } from '~/locale';
+import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
-import {
- I18N_WORK_ITEM_ERROR_UPDATING,
- sprintfWorkItem,
- STATE_OPEN,
- STATE_EVENT_REOPEN,
- STATE_EVENT_CLOSE,
- TRACKING_CATEGORY_SHOW,
- i18n,
-} from '~/work_items/constants';
+import { STATE_OPEN, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
+import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
export default {
i18n: {
@@ -25,6 +16,7 @@ export default {
'Notes|Internal notes are only visible to members with the role of Reporter or higher',
),
addInternalNote: __('Add internal note'),
+ cancelButtonText: __('Cancel'),
},
constantOptions: {
markdownDocsPath: helpPagePath('user/markdown'),
@@ -34,6 +26,7 @@ export default {
MarkdownEditor,
GlFormCheckbox,
GlIcon,
+ WorkItemStateToggleButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -123,14 +116,6 @@ export default {
isWorkItemOpen() {
return this.workItemState === STATE_OPEN;
},
- toggleWorkItemStateText() {
- return this.isWorkItemOpen
- ? sprintf(__('Close %{workItemType}'), { workItemType: this.workItemType.toLowerCase() })
- : sprintf(__('Reopen %{workItemType}'), { workItemType: this.workItemType.toLowerCase() });
- },
- cancelButtonText() {
- return this.isNewDiscussion ? this.toggleWorkItemStateText : __('Cancel');
- },
commentButtonTextComputed() {
return this.isNoteInternal ? this.$options.i18n.addInternalNote : this.commentButtonText;
},
@@ -166,48 +151,6 @@ export default {
this.$emit('cancelEditing');
clearDraft(this.autosaveKey);
},
- async toggleWorkItemState() {
- const input = {
- id: this.workItemId,
- stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
- };
-
- this.updateInProgress = true;
-
- try {
- this.track('updated_state');
-
- const { mutation, variables } = getUpdateWorkItemMutation({
- workItemParentId: this.workItemParentId,
- input,
- });
-
- const { data } = await this.$apollo.mutate({
- mutation,
- variables,
- });
-
- const errors = data.workItemUpdate?.errors;
-
- if (errors?.length) {
- this.$emit('error', i18n.updateError);
- }
- } catch (error) {
- const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
-
- this.$emit('error', msg);
- Sentry.captureException(error);
- }
-
- this.updateInProgress = false;
- },
- cancelButtonAction() {
- if (this.isNewDiscussion) {
- this.toggleWorkItemState();
- } else {
- this.cancelEditing();
- }
- },
},
};
</script>
@@ -257,13 +200,23 @@ export default {
@click="$emit('submitForm', { commentText, isNoteInternal })"
>{{ commentButtonTextComputed }}
</gl-button>
+ <work-item-state-toggle-button
+ v-if="isNewDiscussion"
+ class="gl-ml-3"
+ :work-item-id="workItemId"
+ :work-item-state="workItemState"
+ :work-item-type="workItemType"
+ can-update
+ @error="$emit('error', $event)"
+ />
<gl-button
+ v-else
data-testid="cancel-button"
category="primary"
class="gl-ml-3"
:loading="updateInProgress"
- @click="cancelButtonAction"
- >{{ cancelButtonText }}
+ @click="cancelEditing"
+ >{{ $options.i18n.cancelButtonText }}
</gl-button>
</form>
</div>
diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
index c727075eaac..139f0f7919c 100644
--- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -11,7 +11,6 @@ import {
WIDGET_TYPE_START_AND_DUE_DATE,
WIDGET_TYPE_WEIGHT,
} from '../constants';
-import WorkItemState from './work_item_state.vue';
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
@@ -23,7 +22,6 @@ export default {
WorkItemMilestone,
WorkItemAssignees,
WorkItemDueDate,
- WorkItemState,
WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'),
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
@@ -97,12 +95,6 @@ export default {
<template>
<div class="work-item-attributes-wrapper">
- <work-item-state
- :work-item="workItem"
- :work-item-parent-id="workItemParentId"
- :can-update="canUpdate"
- @error="$emit('error', $event)"
- />
<work-item-assignees
v-if="workItemAssignees"
:can-update="canUpdate"
diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
index 78a86aa49a4..af5293ebe91 100644
--- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue
+++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
@@ -2,6 +2,7 @@
import { GlAvatarLink, GlSprintf } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
export default {
@@ -9,6 +10,7 @@ export default {
GlAvatarLink,
GlSprintf,
TimeAgoTooltip,
+ WorkItemStateBadge,
},
inject: ['fullPath'],
props: {
@@ -31,6 +33,9 @@ export default {
authorId() {
return getIdFromGraphQLId(this.author.id);
},
+ workItemState() {
+ return this.workItem?.state;
+ },
},
apollo: {
workItem: {
@@ -54,7 +59,8 @@ export default {
<template>
<div class="gl-mb-3">
- <span data-testid="work-item-created">
+ <work-item-state-badge v-if="workItemState" :work-item-state="workItemState" />
+ <span data-testid="work-item-created" class="gl-vertical-align-middle">
<gl-sprintf v-if="author.name" :message="__('Created %{timeAgo} by %{author}')">
<template #timeAgo>
<time-ago-tooltip :time="createdAt" />
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index dc4065f9812..238df236b6b 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -49,6 +49,7 @@ import WorkItemDescription from './work_item_description.vue';
import WorkItemNotes from './work_item_notes.vue';
import WorkItemDetailModal from './work_item_detail_modal.vue';
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
+import WorkItemStateToggleButton from './work_item_state_toggle_button.vue';
export default {
i18n,
@@ -57,6 +58,7 @@ export default {
},
isLoggedIn: isLoggedIn(),
components: {
+ WorkItemStateToggleButton,
GlAlert,
GlBadge,
GlButton,
@@ -445,6 +447,14 @@ export default {
class="gl-mr-3 gl-cursor-help"
>{{ __('Confidential') }}</gl-badge
>
+ <work-item-state-toggle-button
+ v-if="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-state="workItem.state"
+ :work-item-parent-id="workItemParentId"
+ :work-item-type="workItemType"
+ @error="updateError = $event"
+ />
<work-item-todos
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
diff --git a/app/assets/javascripts/work_items/components/work_item_state_badge.vue b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
new file mode 100644
index 00000000000..1d1bc7352b1
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlBadge } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { STATE_OPEN } from '../constants';
+
+export default {
+ components: {
+ GlBadge,
+ },
+ props: {
+ workItemState: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isWorkItemOpen() {
+ return this.workItemState === STATE_OPEN;
+ },
+ stateText() {
+ return this.isWorkItemOpen ? __('Open') : __('Closed');
+ },
+ workItemStateIcon() {
+ return this.isWorkItemOpen ? 'issue-open-m' : 'issue-close';
+ },
+ workItemStateVariant() {
+ return this.isWorkItemOpen ? 'success' : 'info';
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-badge
+ :icon="workItemStateIcon"
+ :variant="workItemStateVariant"
+ class="gl-mr-2 gl-vertical-align-middle"
+ >
+ {{ stateText }}
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_state.vue b/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue
index 3880ae25c8c..0ea30845466 100644
--- a/app/assets/javascripts/work_items/components/work_item_state.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue
@@ -1,26 +1,35 @@
<script>
+import { GlButton } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import Tracking from '~/tracking';
+import { __, sprintf } from '~/locale';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_UPDATING,
STATE_OPEN,
- STATE_CLOSED,
STATE_EVENT_CLOSE,
STATE_EVENT_REOPEN,
TRACKING_CATEGORY_SHOW,
} from '../constants';
-import { getUpdateWorkItemMutation } from './update_work_item';
-import ItemState from './item_state.vue';
export default {
components: {
- ItemState,
+ GlButton,
},
mixins: [Tracking.mixin()],
props: {
- workItem: {
- type: Object,
+ workItemState: {
+ type: String,
+ required: true,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemType: {
+ type: String,
required: true,
},
workItemParentId: {
@@ -28,11 +37,6 @@ export default {
required: false,
default: null,
},
- canUpdate: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
@@ -40,8 +44,16 @@ export default {
};
},
computed: {
- workItemType() {
- return this.workItem.workItemType?.name;
+ isWorkItemOpen() {
+ return this.workItemState === STATE_OPEN;
+ },
+ toggleWorkItemStateText() {
+ const baseText = this.isWorkItemOpen
+ ? __('Close %{workItemType}')
+ : __('Reopen %{workItemType}');
+ return capitalizeFirstCharacter(
+ sprintf(baseText, { workItemType: this.workItemType.toLowerCase() }),
+ );
},
tracking() {
return {
@@ -52,25 +64,10 @@ export default {
},
},
methods: {
- updateWorkItemState(newState) {
- const stateEventMap = {
- [STATE_OPEN]: STATE_EVENT_REOPEN,
- [STATE_CLOSED]: STATE_EVENT_CLOSE,
- };
-
- const stateEvent = stateEventMap[newState];
-
- this.updateWorkItem(stateEvent);
- },
-
- async updateWorkItem(updatedState) {
- if (!updatedState) {
- return;
- }
-
+ async updateWorkItem() {
const input = {
- id: this.workItem.id,
- stateEvent: updatedState,
+ id: this.workItemId,
+ stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
};
this.updateInProgress = true;
@@ -107,10 +104,10 @@ export default {
</script>
<template>
- <item-state
- v-if="workItem.state"
- :state="workItem.state"
- :disabled="updateInProgress || !canUpdate"
- @changed="updateWorkItemState"
- />
+ <gl-button
+ :loading="updateInProgress"
+ data-testid="work-item-state-toggle"
+ @click="updateWorkItem"
+ >{{ toggleWorkItemStateText }}</gl-button
+ >
</template>
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index bbd8d2dd0a9..6a58a453a19 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -195,7 +195,8 @@ module IssuesHelper
is_signed_in: current_user.present?.to_s,
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
rss_path: url_for(safe_params.merge(rss_url_options)),
- sign_in_path: new_user_session_path
+ sign_in_path: new_user_session_path,
+ has_issue_date_filter_feature: Feature.enabled?(:issue_date_filter, namespace)
}
end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 77fb7238298..895ff937abe 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -4,6 +4,7 @@ module Ci
class Bridge < Ci::Processable
include Ci::Contextable
include Ci::Metadatable
+ include Ci::Deployable
include Importable
include AfterCommitQueue
include Ci::HasRef
@@ -180,20 +181,6 @@ module Ci
false
end
- def outdated_deployment?
- false
- end
-
- def expanded_environment_name
- end
-
- def persisted_environment
- end
-
- def deployment_job?
- false
- end
-
def execute_hooks
raise NotImplementedError
end
diff --git a/app/presenters/packages/npm/package_presenter.rb b/app/presenters/packages/npm/package_presenter.rb
deleted file mode 100644
index 42f61182ab8..00000000000
--- a/app/presenters/packages/npm/package_presenter.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-module Packages
- module Npm
- class PackagePresenter
- def initialize(metadata)
- @metadata = metadata
- end
-
- def name
- metadata[:name]
- end
-
- def versions
- metadata[:versions]
- end
-
- def dist_tags
- metadata[:dist_tags]
- end
-
- private
-
- attr_reader :metadata
- end
- end
-end
diff --git a/app/services/ci/retry_job_service.rb b/app/services/ci/retry_job_service.rb
index e3cbba6de23..14ea09f17a0 100644
--- a/app/services/ci/retry_job_service.rb
+++ b/app/services/ci/retry_job_service.rb
@@ -39,7 +39,7 @@ module Ci
::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job)
- ::Deployments::CreateForBuildService.new.execute(new_job)
+ ::Deployments::CreateForJobService.new.execute(new_job)
::MergeRequests::AddTodoWhenBuildFailsService
.new(project: project)
diff --git a/app/services/deployments/create_for_build_service.rb b/app/services/deployments/create_for_job_service.rb
index b58aa50a66f..fe632794d99 100644
--- a/app/services/deployments/create_for_build_service.rb
+++ b/app/services/deployments/create_for_job_service.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
module Deployments
- # This class creates a deployment record for a build (a pipeline job).
- class CreateForBuildService
+ # This class creates a deployment record for a pipeline job.
+ class CreateForJobService
DeploymentCreationError = Class.new(StandardError)
def execute(build)
- return unless build.instance_of?(::Ci::Build) && build.persisted_environment.present?
+ return unless build.is_a?(::Ci::Processable) && build.persisted_environment.present?
environment = build.actual_persisted_environment
@@ -37,7 +37,7 @@ module Deployments
# non-environment job.
return unless deployment.valid? && deployment.environment.persisted?
- if cluster = deployment.environment.deployment_platform&.cluster
+ if cluster = deployment.environment.deployment_platform&.cluster # rubocop: disable Lint/AssignmentInCondition
# double write cluster_id until 12.9: https://gitlab.com/gitlab-org/gitlab/issues/202628
deployment.cluster_id = cluster.id
deployment.deployment_cluster = ::DeploymentCluster.new(
diff --git a/app/services/environments/create_for_build_service.rb b/app/services/environments/create_for_job_service.rb
index ff4da212002..2a225c1ac91 100644
--- a/app/services/environments/create_for_build_service.rb
+++ b/app/services/environments/create_for_job_service.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
module Environments
- # This class creates an environment record for a build (a pipeline job).
- class CreateForBuildService
+ # This class creates an environment record for a pipeline job.
+ class CreateForJobService
def execute(build)
- return unless build.instance_of?(::Ci::Build) && build.has_environment_keyword?
+ return unless build.is_a?(::Ci::Processable) && build.has_environment_keyword?
environment = to_resource(build)
diff --git a/app/workers/ci/initial_pipeline_process_worker.rb b/app/workers/ci/initial_pipeline_process_worker.rb
index 52a4f075cf0..067dbb7492f 100644
--- a/app/workers/ci/initial_pipeline_process_worker.rb
+++ b/app/workers/ci/initial_pipeline_process_worker.rb
@@ -32,7 +32,7 @@ module Ci
end
def create_deployment(build)
- ::Deployments::CreateForBuildService.new.execute(build)
+ ::Deployments::CreateForJobService.new.execute(build)
end
end
end