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>2022-07-14 15:08:33 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-14 15:08:33 +0300
commita5c4a731c88de720e6c23be355e44d916c34985f (patch)
tree35f7c35385e9af8a747fa1b7af7d5fed976dc2c9 /app
parent3438be0998953aa87854371f42df3c1f47bc2544 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js4
-rw-r--r--app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js6
-rw-r--r--app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js28
-rw-r--r--app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js4
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table.vue1
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue4
-rw-r--r--app/assets/javascripts/pages/projects/jobs/index/index.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue246
-rw-r--r--app/assets/javascripts/work_items/constants.js1
-rw-r--r--app/assets/javascripts/work_items/graphql/provider.js17
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql8
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql8
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss10
-rw-r--r--app/controllers/projects/jobs_controller.rb10
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/services/ci/runners/reconcile_existing_runner_versions_service.rb3
-rw-r--r--app/services/gravatar_service.rb1
-rw-r--r--app/services/issuable/clone/attributes_rewriter.rb126
-rw-r--r--app/services/issuable/clone/base_service.rb32
-rw-r--r--app/services/issues/clone_service.rb17
-rw-r--r--app/services/issues/move_service.rb1
-rw-r--r--app/services/work_items/create_and_link_service.rb6
-rw-r--r--app/services/work_items/create_from_task_service.rb2
-rw-r--r--app/views/projects/jobs/index.html.haml10
26 files changed, 378 insertions, 217 deletions
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index 06757e7a280..867bf0b4d55 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -39,12 +39,12 @@ export class ContentEditor {
this._eventHub.dispose();
}
- deserialize(serializedContent) {
+ deserialize(markdown) {
const { _tiptapEditor: editor, _deserializer: deserializer } = this;
return deserializer.deserialize({
schema: editor.schema,
- content: serializedContent,
+ markdown,
});
}
diff --git a/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
index dcd56e55268..fa46bd9ff81 100644
--- a/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
+++ b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
@@ -16,8 +16,8 @@ export default ({ render }) => {
* document. The dom property contains the HTML generated from the Markdown Source.
*/
return {
- deserialize: async ({ schema, content }) => {
- const html = await render(content);
+ deserialize: async ({ schema, markdown }) => {
+ const html = await render(markdown);
if (!html) return {};
@@ -25,7 +25,7 @@ export default ({ render }) => {
const { body } = parser.parseFromString(html, 'text/html');
// append original source as a comment that nodes can access
- body.append(document.createComment(content));
+ body.append(document.createComment(markdown));
return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body) };
},
diff --git a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
index 5d675fb4851..ad9419699c8 100644
--- a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
+++ b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
@@ -53,7 +53,7 @@ function maybeMerge(a, b) {
* Hast node documentation: https://github.com/syntax-tree/hast
*
* @param {HastNode} hastNode A Hast node
- * @param {String} source Markdown source file
+ * @param {String} markdown Markdown source file
*
* @returns It returns an object with the following attributes:
*
@@ -62,13 +62,13 @@ function maybeMerge(a, b) {
* - sourceMarkdown: A node’s original Markdown source extrated
* from the Markdown source file.
*/
-function createSourceMapAttributes(hastNode, source) {
+function createSourceMapAttributes(hastNode, markdown) {
const { position } = hastNode;
return position && position.end
? {
sourceMapKey: `${position.start.offset}:${position.end.offset}`,
- sourceMarkdown: source.substring(position.start.offset, position.end.offset),
+ sourceMarkdown: markdown.substring(position.start.offset, position.end.offset),
}
: {};
}
@@ -84,16 +84,16 @@ function createSourceMapAttributes(hastNode, source) {
* @param {*} proseMirrorNodeSpec ProseMirror node spec object
* @param {HastNode} hastNode A hast node
* @param {Array<HastNode>} hastParents All the ancestors of the hastNode
- * @param {String} source Markdown source file’s content
+ * @param {String} markdown Markdown source file’s content
*
* @returns An object that contains a ProseMirror node’s attributes
*/
-function getAttrs(proseMirrorNodeSpec, hastNode, hastParents, source) {
+function getAttrs(proseMirrorNodeSpec, hastNode, hastParents, markdown) {
const { getAttrs: specGetAttrs } = proseMirrorNodeSpec;
return {
- ...createSourceMapAttributes(hastNode, source),
- ...(isFunction(specGetAttrs) ? specGetAttrs(hastNode, hastParents, source) : {}),
+ ...createSourceMapAttributes(hastNode, markdown),
+ ...(isFunction(specGetAttrs) ? specGetAttrs(hastNode, hastParents, markdown) : {}),
};
}
@@ -319,11 +319,11 @@ class HastToProseMirrorConverterState {
* @param {model.ProseMirrorSchema} schema A ProseMirror schema used to create the
* ProseMirror nodes and marks.
* @param {Object} proseMirrorFactorySpecs ProseMirror nodes factory specifications.
- * @param {String} source Markdown source file’s content
+ * @param {String} markdown Markdown source file’s content
*
* @returns An object that contains ProseMirror node factories
*/
-const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source) => {
+const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, markdown) => {
const factories = {
root: {
selector: 'root',
@@ -356,7 +356,7 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
const nodeType = schema.nodeType(proseMirrorName);
state.closeUntil(parent);
- state.openNode(nodeType, hastNode, getAttrs(factory, hastNode, parent, source), factory);
+ state.openNode(nodeType, hastNode, getAttrs(factory, hastNode, parent, markdown), factory);
/**
* If a getContent function is provided, we immediately close
@@ -371,14 +371,14 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
const nodeType = schema.nodeType(proseMirrorName);
factory.handle = (state, hastNode, parent) => {
state.closeUntil(parent);
- state.openNode(nodeType, hastNode, getAttrs(factory, hastNode, parent, source), factory);
+ state.openNode(nodeType, hastNode, getAttrs(factory, hastNode, parent, markdown), factory);
// Inline nodes do not have children therefore they are immediately closed
state.closeNode();
};
} else if (factory.type === 'mark') {
const markType = schema.marks[proseMirrorName];
factory.handle = (state, hastNode, parent) => {
- state.openMark(markType, hastNode, getAttrs(factory, hastNode, parent, source), factory);
+ state.openMark(markType, hastNode, getAttrs(factory, hastNode, parent, markdown), factory);
};
} else if (factory.type === 'ignore') {
factory.handle = noop;
@@ -601,9 +601,9 @@ export const createProseMirrorDocFromMdastTree = ({
factorySpecs,
wrappableTags,
tree,
- source,
+ markdown,
}) => {
- const proseMirrorNodeFactories = createProseMirrorNodeFactories(schema, factorySpecs, source);
+ const proseMirrorNodeFactories = createProseMirrorNodeFactories(schema, factorySpecs, markdown);
const state = new HastToProseMirrorConverterState();
visitParents(tree, (hastNode, ancestors) => {
diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
index 8c99dc157e6..9bf130c454e 100644
--- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
+++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
@@ -169,7 +169,7 @@ const factorySpecs = {
export default () => {
return {
- deserialize: async ({ schema, content: markdown }) => {
+ deserialize: async ({ schema, markdown }) => {
const document = await render({
markdown,
renderer: (tree) =>
@@ -178,7 +178,7 @@ export default () => {
factorySpecs,
tree,
wrappableTags,
- source: markdown,
+ markdown,
}),
});
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue
index f513d2090fa..d8c5c292f52 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue
@@ -45,6 +45,7 @@ export default {
:fields="tableFields"
:tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }"
:empty-text="$options.i18n.emptyText"
+ data-testid="jobs-table"
show-empty
stacked="lg"
fixed
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
index 1ac1a2d68e2..b3db5a94ac5 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
@@ -2,7 +2,6 @@
import { GlAlert, GlSkeletonLoader, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import createFlash from '~/flash';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import JobsFilteredSearch from '../filtered_search/jobs_filtered_search.vue';
import eventHub from './event_hub';
import GetJobs from './graphql/queries/get_jobs.query.graphql';
@@ -28,7 +27,6 @@ export default {
GlIntersectionObserver,
GlLoadingIcon,
},
- mixins: [glFeatureFlagMixin()],
inject: {
fullPath: {
default: '',
@@ -93,7 +91,7 @@ export default {
return this.loading && !this.showLoadingSpinner;
},
showFilteredSearch() {
- return this.glFeatures?.jobsTableVueSearch && !this.scope;
+ return !this.scope;
},
jobsCount() {
return this.jobs.count;
diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js
index 75194499a7f..eb3a24f38a8 100644
--- a/app/assets/javascripts/pages/projects/jobs/index/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/index/index.js
@@ -1,23 +1,3 @@
-import Vue from 'vue';
import initJobsTable from '~/jobs/components/table';
-import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-if (gon.features?.jobsTableVue) {
- initJobsTable();
-} else {
- const remainingTimeElements = document.querySelectorAll('.js-remaining-time');
-
- remainingTimeElements.forEach(
- (el) =>
- new Vue({
- el,
- render(h) {
- return h(GlCountdown, {
- props: {
- endDateString: el.dateTime,
- },
- });
- },
- }),
- );
-}
+initJobsTable();
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 c68437b9879..3e0ac236fdf 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
@@ -221,8 +221,11 @@ export default {
formattedHumanAccess() {
return (this.mr.humanAccess || '').toLowerCase();
},
+ hasMergeError() {
+ return this.mr.mergeError && this.state !== 'closed';
+ },
hasAlerts() {
- return this.mr.mergeError || this.showMergePipelineForkWarning;
+ return this.hasMergeError || this.showMergePipelineForkWarning;
},
shouldShowExtension() {
return (
@@ -574,7 +577,12 @@ export default {
/>
<div class="mr-section-container mr-widget-workflow">
<div v-if="hasAlerts" class="gl-overflow-hidden mr-widget-alert-container">
- <mr-widget-alert-message v-if="mr.mergeError" type="danger" dismissible>
+ <mr-widget-alert-message
+ v-if="hasMergeError"
+ type="danger"
+ dismissible
+ data-testid="merge_error"
+ >
<span v-safe-html="mergeError"></span>
</mr-widget-alert-message>
<mr-widget-alert-message
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 4fb4a18c460..138101890bf 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -4,6 +4,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
i18n,
WIDGET_TYPE_ASSIGNEES,
+ WIDGET_TYPE_LABELS,
WIDGET_TYPE_DESCRIPTION,
WIDGET_TYPE_WEIGHT,
} from '../constants';
@@ -14,6 +15,7 @@ import WorkItemState from './work_item_state.vue';
import WorkItemTitle from './work_item_title.vue';
import WorkItemDescription from './work_item_description.vue';
import WorkItemAssignees from './work_item_assignees.vue';
+import WorkItemLabels from './work_item_labels.vue';
import WorkItemWeight from './work_item_weight.vue';
export default {
@@ -25,6 +27,7 @@ export default {
WorkItemAssignees,
WorkItemActions,
WorkItemDescription,
+ WorkItemLabels,
WorkItemTitle,
WorkItemState,
WorkItemWeight,
@@ -99,6 +102,9 @@ export default {
workItemAssignees() {
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES);
},
+ workItemLabels() {
+ return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
+ },
workItemWeight() {
return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
},
@@ -155,6 +161,12 @@ export default {
:allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
@error="error = $event"
/>
+ <work-item-labels
+ v-if="workItemLabels"
+ :work-item-id="workItem.id"
+ :can-update="canUpdate"
+ @error="error = $event"
+ />
<work-item-weight
v-if="workItemWeight"
class="gl-mb-5"
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
new file mode 100644
index 00000000000..78ed67998d7
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -0,0 +1,246 @@
+<script>
+import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import Tracking from '~/tracking';
+import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
+import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
+import workItemQuery from '../graphql/work_item.query.graphql';
+import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+
+import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_LABELS } from '../constants';
+
+function isTokenSelectorElement(el) {
+ return el?.classList.contains('gl-label-close') || el?.classList.contains('dropdown-item');
+}
+
+function addClass(el) {
+ return {
+ ...el,
+ class: 'gl-bg-transparent',
+ };
+}
+
+export default {
+ components: {
+ GlTokenSelector,
+ GlLabel,
+ GlSkeletonLoader,
+ LabelItem,
+ },
+ mixins: [Tracking.mixin()],
+ inject: ['fullPath'],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ canUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isEditing: false,
+ searchStarted: false,
+ localLabels: [],
+ searchKey: '',
+ searchLabels: [],
+ };
+ },
+ apollo: {
+ workItem: {
+ query: workItemQuery,
+ variables() {
+ return {
+ id: this.workItemId,
+ };
+ },
+ skip() {
+ return !this.workItemId;
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
+ searchLabels: {
+ query: labelSearchQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ search: this.searchKey,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace?.labels?.nodes.map((node) => addClass({ ...node, ...node.label }));
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
+ },
+ computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_labels',
+ property: `type_${this.workItem.workItemType?.name}`,
+ };
+ },
+ allowScopedLabels() {
+ return this.labelsWidget.allowScopedLabels;
+ },
+ listEmpty() {
+ return this.labels.length === 0;
+ },
+ containerClass() {
+ return !this.isEditing ? 'gl-shadow-none!' : '';
+ },
+ isLoading() {
+ return this.$apollo.queries.searchLabels.loading;
+ },
+ labelsWidget() {
+ return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
+ },
+ labels() {
+ return this.labelsWidget?.nodes || [];
+ },
+ },
+ watch: {
+ labels(newVal) {
+ if (!this.isEditing) {
+ this.localLabels = newVal.map(addClass);
+ }
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ methods: {
+ getId(id) {
+ return getIdFromGraphQLId(id);
+ },
+ removeLabel({ id }) {
+ this.localLabels = this.localLabels.filter((label) => label.id !== id);
+ },
+ setLabels(event) {
+ this.searchKey = '';
+ if (isTokenSelectorElement(event.relatedTarget) || !this.isEditing) return;
+ this.isEditing = false;
+ this.$apollo
+ .mutate({
+ mutation: localUpdateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ labels: this.localLabels,
+ },
+ },
+ })
+ .catch((e) => {
+ this.$emit('error', e);
+ });
+ this.track('updated_labels');
+ },
+ handleFocus() {
+ this.isEditing = true;
+ this.searchStarted = true;
+ },
+ async focusTokenSelector(labels) {
+ if (this.allowScopedLabels) {
+ const newLabel = labels[labels.length - 1];
+ const existingLabels = labels.slice(0, labels.length - 1);
+
+ const newLabelKey = scopedLabelKey(newLabel);
+
+ const removeLabelsWithSameScope = existingLabels.filter((label) => {
+ const sameKey = newLabelKey === scopedLabelKey(label);
+ return !sameKey;
+ });
+
+ this.localLabels = [...removeLabelsWithSameScope, newLabel];
+ }
+ this.handleFocus();
+ await this.$nextTick();
+ this.$refs.tokenSelector.focusTextInput();
+ },
+ handleMouseOver() {
+ this.timeout = setTimeout(() => {
+ this.searchStarted = true;
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ handleMouseOut() {
+ clearTimeout(this.timeout);
+ },
+ setSearchKey(value) {
+ this.searchKey = value;
+ },
+ scopedLabel(label) {
+ return this.allowScopedLabels && isScopedLabel(label);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="form-row gl-mb-5 work-item-labels gl-relative">
+ <span
+ class="gl-font-weight-bold gl-mt-2 col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
+ data-testid="labels-title"
+ >{{ __('Labels') }}</span
+ >
+ <gl-token-selector
+ ref="tokenSelector"
+ v-model="localLabels"
+ :container-class="containerClass"
+ :dropdown-items="searchLabels"
+ :loading="isLoading"
+ :view-only="!canUpdate"
+ class="gl-flex-grow-1 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2!"
+ @input="focusTokenSelector"
+ @text-input="debouncedSearchKeyUpdate"
+ @focus="handleFocus"
+ @blur="setLabels"
+ @mouseover.native="handleMouseOver"
+ @mouseout.native="handleMouseOut"
+ >
+ <template #empty-placeholder>
+ <div
+ class="add-labels gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-gray-400 gl-pr-4 gl-top-2"
+ data-testid="empty-state"
+ >
+ <span v-if="canUpdate" class="gl-ml-2">{{ __('Select labels') }}</span>
+ <span v-else class="gl-ml-2">{{ __('None') }}</span>
+ </div>
+ </template>
+ <template #token-content="{ token }">
+ <gl-label
+ :data-qa-label-name="token.title"
+ :title="token.title"
+ :description="token.description"
+ :background-color="token.color"
+ :scoped="scopedLabel(token)"
+ :show-close-button="canUpdate"
+ @close="removeLabel(token)"
+ />
+ </template>
+ <template #dropdown-item-content="{ dropdownItem }">
+ <label-item :label="dropdownItem" />
+ </template>
+ <template #loading-content>
+ <gl-skeleton-loader :height="170">
+ <rect width="380" height="20" x="10" y="15" rx="4" />
+ <rect width="280" height="20" x="10" y="50" rx="4" />
+ <rect width="380" height="20" x="10" y="95" rx="4" />
+ <rect width="280" height="20" x="10" y="130" rx="4" />
+ </gl-skeleton-loader>
+ </template>
+ </gl-token-selector>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index c1d2851f0b7..c9ccbd48ba1 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -17,6 +17,7 @@ export const TASK_TYPE_NAME = 'Task';
export const WIDGET_TYPE_ASSIGNEES = 'ASSIGNEES';
export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
+export const WIDGET_TYPE_LABELS = 'LABELS';
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
diff --git a/app/assets/javascripts/work_items/graphql/provider.js b/app/assets/javascripts/work_items/graphql/provider.js
index f1772cae9ac..8788ad21e7b 100644
--- a/app/assets/javascripts/work_items/graphql/provider.js
+++ b/app/assets/javascripts/work_items/graphql/provider.js
@@ -2,7 +2,7 @@ import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_WEIGHT } from '../constants';
+import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_LABELS, WIDGET_TYPE_WEIGHT } from '../constants';
import typeDefs from './typedefs.graphql';
import workItemQuery from './work_item.query.graphql';
@@ -10,7 +10,7 @@ export const temporaryConfig = {
typeDefs,
cacheConfig: {
possibleTypes: {
- LocalWorkItemWidget: ['LocalWorkItemWeight'],
+ LocalWorkItemWidget: ['LocalWorkItemLabels', 'LocalWorkItemWeight'],
},
typePolicies: {
WorkItem: {
@@ -20,6 +20,12 @@ export const temporaryConfig = {
return (
widgets || [
{
+ __typename: 'LocalWorkItemLabels',
+ type: WIDGET_TYPE_LABELS,
+ allowScopedLabels: true,
+ nodes: [],
+ },
+ {
__typename: 'LocalWorkItemWeight',
type: 'WEIGHT',
weight: null,
@@ -56,6 +62,13 @@ export const resolvers = {
);
weightWidget.weight = input.weight;
}
+
+ if (input.labels) {
+ const labelsWidget = draftData.workItem.mockWidgets.find(
+ (widget) => widget.type === WIDGET_TYPE_LABELS,
+ );
+ labelsWidget.nodes = [...input.labels];
+ }
});
cache.writeQuery({
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index 71ac263a02e..48228b15a53 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -1,5 +1,6 @@
enum LocalWidgetType {
ASSIGNEES
+ LABELS
WEIGHT
}
@@ -12,6 +13,12 @@ type LocalWorkItemAssignees implements LocalWorkItemWidget {
nodes: [UserCore]
}
+type LocalWorkItemLabels implements LocalWorkItemWidget {
+ type: LocalWidgetType!
+ allowScopedLabels: Boolean!
+ nodes: [Label!]
+}
+
type LocalWorkItemWeight implements LocalWorkItemWidget {
type: LocalWidgetType!
weight: Int
@@ -24,6 +31,7 @@ extend type WorkItem {
input LocalUpdateWorkItemInput {
id: WorkItemID!
assignees: [UserCore!]
+ labels: [Label]
weight: Int
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
index 724741c9957..61cb8802187 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -1,9 +1,17 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "./work_item.fragment.graphql"
query workItem($id: WorkItemID!) {
workItem(id: $id) {
...WorkItem
mockWidgets @client {
+ ... on LocalWorkItemLabels {
+ type
+ allowScopedLabels
+ nodes {
+ ...Label
+ }
+ }
... on LocalWorkItemWeight {
type
weight
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index e5910dcd5d2..9220fa82b46 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -23,3 +23,13 @@
display: block;
}
}
+
+.work-item-labels {
+ .gl-token {
+ padding-left: $gl-spacing-scale-1;
+ }
+
+ .gl-token-close {
+ display: none;
+ }
+}
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 24685d26fc9..ad59f421c06 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -18,8 +18,6 @@ class Projects::JobsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize
before_action :verify_proxy_request!, only: :proxy_websocket_authorize
- before_action :push_jobs_table_vue, only: [:index]
- before_action :push_jobs_table_vue_search, only: [:index]
before_action :push_job_log_search, only: [:show]
before_action :reject_if_build_artifacts_size_refreshing!, only: [:erase]
@@ -251,14 +249,6 @@ class Projects::JobsController < Projects::ApplicationController
::Gitlab::Workhorse.channel_websocket(service)
end
- def push_jobs_table_vue
- push_frontend_feature_flag(:jobs_table_vue, @project)
- end
-
- def push_jobs_table_vue_search
- push_frontend_feature_flag(:jobs_table_vue_search, @project)
- end
-
def push_job_log_search
push_frontend_feature_flag(:job_log_search, @project)
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index b1fdfed5ae0..ec97ab0ea42 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -174,6 +174,10 @@ class MergeRequest < ApplicationRecord
merge_request.merge_jid = nil
end
+ before_transition any => :closed do |merge_request|
+ merge_request.merge_error = nil
+ end
+
after_transition any => :opened do |merge_request|
merge_request.run_after_commit do
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
diff --git a/app/services/ci/runners/reconcile_existing_runner_versions_service.rb b/app/services/ci/runners/reconcile_existing_runner_versions_service.rb
index e15676faa33..e04079bfe27 100644
--- a/app/services/ci/runners/reconcile_existing_runner_versions_service.rb
+++ b/app/services/ci/runners/reconcile_existing_runner_versions_service.rb
@@ -75,7 +75,8 @@ module Ci
def runner_version_with_updated_status(runner_version)
version = runner_version['version']
- new_status = upgrade_check.check_runner_upgrade_status(version)
+ suggestion = upgrade_check.check_runner_upgrade_status(version)
+ new_status = suggestion.each_key.first
if new_status != :error && new_status != runner_version['status'].to_sym
{
diff --git a/app/services/gravatar_service.rb b/app/services/gravatar_service.rb
index a689b088854..9d5990f2c8a 100644
--- a/app/services/gravatar_service.rb
+++ b/app/services/gravatar_service.rb
@@ -2,6 +2,7 @@
class GravatarService
def execute(email, size = nil, scale = 2, username: nil)
+ return if Gitlab::FIPS.enabled?
return unless Gitlab::CurrentSettings.gravatar_enabled?
identifier = email.presence || username.presence
diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb
deleted file mode 100644
index 279d3051848..00000000000
--- a/app/services/issuable/clone/attributes_rewriter.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-# frozen_string_literal: true
-
-module Issuable
- module Clone
- class AttributesRewriter < ::Issuable::Clone::BaseService
- def initialize(current_user, original_entity, new_entity)
- @current_user = current_user
- @original_entity = original_entity
- @new_entity = new_entity
- end
-
- def execute
- update_attributes = { labels: cloneable_labels }
-
- milestone = matching_milestone(original_entity.milestone&.title)
- update_attributes[:milestone] = milestone if milestone.present?
-
- new_entity.update(update_attributes)
-
- copy_resource_label_events
- copy_resource_milestone_events
- copy_resource_state_events
- end
-
- private
-
- def matching_milestone(title)
- return if title.blank? || !new_entity.supports_milestone?
-
- params = { title: title, project_ids: new_entity.project&.id, group_ids: group&.id }
-
- milestones = MilestonesFinder.new(params).execute
- milestones.first
- end
-
- def cloneable_labels
- params = {
- project_id: new_entity.project&.id,
- group_id: group&.id,
- title: original_entity.labels.select(:title),
- include_ancestor_groups: true
- }
-
- params[:only_group_labels] = true if new_parent.is_a?(Group)
-
- LabelsFinder.new(current_user, params).execute
- end
-
- def copy_resource_label_events
- copy_events(ResourceLabelEvent.table_name, original_entity.resource_label_events) do |event|
- event.attributes
- .except('id', 'reference', 'reference_html')
- .merge(entity_key => new_entity.id, 'action' => ResourceLabelEvent.actions[event.action])
- end
- end
-
- def copy_resource_milestone_events
- return unless milestone_events_supported?
-
- copy_events(ResourceMilestoneEvent.table_name, original_entity.resource_milestone_events) do |event|
- if event.remove?
- event_attributes_with_milestone(event, nil)
- else
- matching_destination_milestone = matching_milestone(event.milestone_title)
-
- event_attributes_with_milestone(event, matching_destination_milestone) if matching_destination_milestone.present?
- end
- end
- end
-
- def copy_resource_state_events
- return unless state_events_supported?
-
- copy_events(ResourceStateEvent.table_name, original_entity.resource_state_events) do |event|
- event.attributes
- .except(*blocked_state_event_attributes)
- .merge(entity_key => new_entity.id,
- 'state' => ResourceStateEvent.states[event.state])
- end
- end
-
- # Overriden on EE::Issuable::Clone::AttributesRewriter
- def blocked_state_event_attributes
- ['id']
- end
-
- def event_attributes_with_milestone(event, milestone)
- event.attributes
- .except('id')
- .merge(entity_key => new_entity.id,
- 'milestone_id' => milestone&.id,
- 'action' => ResourceMilestoneEvent.actions[event.action],
- 'state' => ResourceMilestoneEvent.states[event.state])
- end
-
- def copy_events(table_name, events_to_copy)
- events_to_copy.find_in_batches do |batch|
- events = batch.map do |event|
- yield(event)
- end.compact
-
- ApplicationRecord.legacy_bulk_insert(table_name, events) # rubocop:disable Gitlab/BulkInsert
- end
- end
-
- def entity_key
- new_entity.class.name.underscore.foreign_key
- end
-
- def milestone_events_supported?
- both_respond_to?(:resource_milestone_events)
- end
-
- def state_events_supported?
- both_respond_to?(:resource_state_events)
- end
-
- def both_respond_to?(method)
- original_entity.respond_to?(method) &&
- new_entity.respond_to?(method)
- end
- end
- end
-end
-
-Issuable::Clone::AttributesRewriter.prepend_mod_with('Issuable::Clone::AttributesRewriter')
diff --git a/app/services/issuable/clone/base_service.rb b/app/services/issuable/clone/base_service.rb
index ce9918a4b56..6061a4b6012 100644
--- a/app/services/issuable/clone/base_service.rb
+++ b/app/services/issuable/clone/base_service.rb
@@ -25,19 +25,19 @@ module Issuable
private
- def copy_award_emoji
- AwardEmojis::CopyService.new(original_entity, new_entity).execute
- end
-
- def copy_notes
- Notes::CopyService.new(current_user, original_entity, new_entity).execute
+ def rewritten_old_entity_attributes(include_milestone: true)
+ Gitlab::Issuable::Clone::AttributesRewriter.new(
+ current_user,
+ original_entity,
+ target_project
+ ).execute(include_milestone: include_milestone)
end
def update_new_entity
update_new_entity_description
- update_new_entity_attributes
copy_award_emoji
copy_notes
+ copy_resource_events
end
def update_new_entity_description
@@ -52,8 +52,16 @@ module Issuable
new_entity.update!(update_description_params)
end
- def update_new_entity_attributes
- AttributesRewriter.new(current_user, original_entity, new_entity).execute
+ def copy_award_emoji
+ AwardEmojis::CopyService.new(original_entity, new_entity).execute
+ end
+
+ def copy_notes
+ Notes::CopyService.new(current_user, original_entity, new_entity).execute
+ end
+
+ def copy_resource_events
+ Gitlab::Issuable::Clone::CopyResourceEventsService.new(current_user, original_entity, new_entity).execute
end
def update_old_entity
@@ -74,12 +82,6 @@ module Issuable
new_entity.resource_parent
end
- def group
- if new_entity.project&.group && current_user.can?(:read_group, new_entity.project.group)
- new_entity.project.group
- end
- end
-
def relative_position
return if original_entity.project.root_ancestor.id != target_project.root_ancestor.id
diff --git a/app/services/issues/clone_service.rb b/app/services/issues/clone_service.rb
index c675f957cd7..896b15a14b8 100644
--- a/app/services/issues/clone_service.rb
+++ b/app/services/issues/clone_service.rb
@@ -41,9 +41,12 @@ module Issues
def update_new_entity
# we don't call `super` because we want to be able to decide whether or not to copy all comments over.
update_new_entity_description
- update_new_entity_attributes
copy_award_emoji
- copy_notes if with_notes
+
+ if with_notes
+ copy_notes
+ copy_resource_events
+ end
end
def update_old_entity
@@ -62,14 +65,18 @@ module Issues
}
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
+ new_params = new_params.merge(rewritten_old_entity_attributes)
+ new_params.delete(:created_at)
+ new_params.delete(:updated_at)
# spam checking is not necessary, as no new content is being created. Passing nil for
# spam_params will cause SpamActionService to skip checking and return a success response.
spam_params = nil
- # Skip creation of system notes for existing attributes of the issue. The system notes of the old
- # issue are copied over so we don't want to end up with duplicate notes.
- CreateService.new(project: target_project, current_user: current_user, params: new_params, spam_params: spam_params).execute(skip_system_notes: true)
+ # Skip creation of system notes for existing attributes of the issue when cloning with notes.
+ # The system notes of the old issue are copied over so we don't want to end up with duplicate notes.
+ # When cloning without notes, we want to generate system notes for the attributes that were copied.
+ CreateService.new(project: target_project, current_user: current_user, params: new_params, spam_params: spam_params).execute(skip_system_notes: with_notes)
end
def queue_copy_designs
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index d210ba2a76c..edab62b1fdf 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -76,6 +76,7 @@ module Issues
}
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
+ new_params = new_params.merge(rewritten_old_entity_attributes)
# spam checking is not necessary, as no new content is being created. Passing nil for
# spam_params will cause SpamActionService to skip checking and return a success response.
spam_params = nil
diff --git a/app/services/work_items/create_and_link_service.rb b/app/services/work_items/create_and_link_service.rb
index 534d220a846..6a773a84225 100644
--- a/app/services/work_items/create_and_link_service.rb
+++ b/app/services/work_items/create_and_link_service.rb
@@ -25,7 +25,11 @@ module WorkItems
work_item = create_result[:work_item]
return ::ServiceResponse.success(payload: payload(work_item)) if @link_params.blank?
- result = IssueLinks::CreateService.new(work_item, @current_user, @link_params).execute
+ result = WorkItems::ParentLinks::CreateService.new(
+ @link_params[:parent_work_item],
+ @current_user,
+ { target_issuable: work_item }
+ ).execute
if result[:status] == :success
::ServiceResponse.success(payload: payload(work_item))
diff --git a/app/services/work_items/create_from_task_service.rb b/app/services/work_items/create_from_task_service.rb
index 4203c96e676..d5fa5fca772 100644
--- a/app/services/work_items/create_from_task_service.rb
+++ b/app/services/work_items/create_from_task_service.rb
@@ -17,7 +17,7 @@ module WorkItems
current_user: @current_user,
params: @work_item_params.slice(:title, :work_item_type_id),
spam_params: @spam_params,
- link_params: { target_issuable: @work_item }
+ link_params: { parent_work_item: @work_item }
).execute
if create_and_link_result.error?
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index dfea4db4d07..d39d292fb53 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -2,12 +2,4 @@
- add_page_specific_style 'page_bundles/ci_status'
- admin = local_assigns.fetch(:admin, false)
-- if Feature.enabled?(:jobs_table_vue, @project)
- #js-jobs-table{ data: { admin: admin, full_path: @project.full_path, job_statuses: job_statuses.to_json, pipeline_editor_path: project_ci_pipeline_editor_path(@project), empty_state_svg_path: image_path('jobs-empty-state.svg') } }
-- else
- .top-area
- - build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) }
- = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
-
- .content-list.builds-content-list
- = render "table", builds: @builds, project: @project
+#js-jobs-table{ data: { admin: admin, full_path: @project.full_path, job_statuses: job_statuses.to_json, pipeline_editor_path: project_ci_pipeline_editor_path(@project), empty_state_svg_path: image_path('jobs-empty-state.svg') } }