diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-14 15:08:33 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-14 15:08:33 +0300 |
commit | a5c4a731c88de720e6c23be355e44d916c34985f (patch) | |
tree | 35f7c35385e9af8a747fa1b7af7d5fed976dc2c9 /app | |
parent | 3438be0998953aa87854371f42df3c1f47bc2544 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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') } } |