diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index bc7a5663ac9..d93164b35b9 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -73,6 +73,9 @@ .if-merge-request-labels-skip-undercoverage: &if-merge-request-labels-skip-undercoverage if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:skip-undercoverage/' +.if-merge-request-labels-community-contribution: &if-merge-request-labels-community-contribution + if: '$CI_MERGE_REQUEST_LABELS =~ /Community contribution/' + .if-merge-request-labels-jh-contribution: &if-merge-request-labels-jh-contribution if: '$CI_MERGE_REQUEST_LABELS =~ /JiHu contribution/' @@ -1664,6 +1667,8 @@ rules: - <<: *if-not-canonical-namespace when: never + - <<: *if-merge-request-labels-community-contribution + when: never - <<: *if-merge-request ############### diff --git a/.gitlab/issue_templates/Deprecations.md b/.gitlab/issue_templates/Deprecations.md index 2e48c272316..3dfed1a1fc1 100644 --- a/.gitlab/issue_templates/Deprecations.md +++ b/.gitlab/issue_templates/Deprecations.md @@ -47,7 +47,7 @@ Please add links to the relevant merge requests. - As soon as possible, but no later than the third milestone preceding the major release (for example, given the following release schedule: `14.8, 14.9, 14.10, 15.0` – `14.8` is the third milestone preceding the major release): - [ ] A [deprecation entry](https://about.gitlab.com/handbook/marketing/blog/release-posts/#creating-a-deprecation-entry) has been created so the deprecation will appear in release posts and on the [general deprecation page](https://docs.gitlab.com/ee/update/deprecations). - - [ ] Documentation has been updated to add a note about the [end-of-life](https://docs.gitlab.com/ee/development/documentation/styleguide/#end-of-life-for-features-or-products) and to mark the feature as [deprecated](https://docs.gitlab.com/ee/development/documentation/styleguide/#deprecated-features). + - [ ] Documentation has been updated to mark the feature as [deprecated](https://docs.gitlab.com/ee/development/documentation/versions.html#deprecations-and-removals). - [ ] On or before the major milestone: A [removal entry](https://about.gitlab.com/handbook/marketing/blog/release-posts/#removals) has been created so the removal will appear on the [removals by milestones](https://docs.gitlab.com/ee/update/removals) page and be announced in the release post. - On the major milestone: - [ ] The deprecated item has been removed. diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue index 3af89dc4a2c..557a8d6b5ba 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue @@ -369,7 +369,7 @@ export default { :href="awsTipLearnLink" target="_blank" category="secondary" - variant="info" + variant="confirm" class="gl-overflow-wrap-break" >{{ __('Learn more about deploying to AWS') }} @@ -416,6 +416,7 @@ export default { :disabled="!canSubmit" variant="confirm" category="primary" + data-testid="ciUpdateOrAddVariableBtn" data-qa-selector="ci_variable_save_button" @click="updateOrAddVariable" >{{ modalActionText }} 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 dce33889c48..f38e4514393 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 @@ -20,7 +20,7 @@ */ import { Mark } from 'prosemirror-model'; -import { visitParents } from 'unist-util-visit-parents'; +import { visitParents, SKIP } from 'unist-util-visit-parents'; import { toString } from 'hast-util-to-string'; import { isFunction, isString, noop } from 'lodash'; @@ -143,6 +143,20 @@ class HastToProseMirrorConverterState { return this.stack.length === 0; } + findInStack(fn) { + const last = this.stack.length - 1; + + for (let i = last; i >= 0; i -= 1) { + const item = this.stack[i]; + + if (fn(item) === true) { + return item; + } + } + + return null; + } + /** * Creates a text node and adds it to * the top node in the stack. @@ -254,34 +268,20 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source) const factories = { root: { selector: 'root', - handle: (state, hastNode) => - state.openNode( - schema.topNodeType, - hastNode, - {}, - { - wrapTextInParagraph: true, - }, - ), + wrapInParagraph: true, + handle: (state, hastNode) => state.openNode(schema.topNodeType, hastNode, {}, {}), }, text: { selector: 'text', - handle: (state, hastNode, parent) => { - const { factorySpec } = state.top; - const { processText, wrapTextInParagraph } = factorySpec; + handle: (state, hastNode) => { + const found = state.findInStack((node) => isFunction(node.factorySpec.processText)); const { value: text } = hastNode; if (/^\s+$/.test(text)) { return; } - if (wrapTextInParagraph === true) { - state.openNode(schema.nodeType('paragraph'), hastNode, getAttrs({}, parent, [], source)); - state.addText(schema, isFunction(processText) ? processText(text) : text); - state.closeNode(); - } else { - state.addText(schema, text); - } + state.addText(schema, found ? found.factorySpec.processText(text) : text); }, }, }; @@ -291,6 +291,7 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source) skipChildren: factorySpec.skipChildren, processText: factorySpec.processText, parent: factorySpec.parent, + wrapInParagraph: factorySpec.wrapInParagraph, }; if (factorySpec.type === 'block') { @@ -370,6 +371,75 @@ const findParent = (ancestors, parent) => { return ancestors[ancestors.length - 1]; }; +const calcTextNodePosition = (textNode) => { + const { position, value, type } = textNode; + + if (type !== 'text' || (!position.start && !position.end) || (position.start && position.end)) { + return textNode.position; + } + + const span = value.length - 1; + + if (position.start && !position.end) { + const { start } = position; + + return { + start, + end: { + row: start.row, + column: start.column + span, + offset: start.offset + span, + }, + }; + } + + const { end } = position; + + return { + start: { + row: end.row, + column: end.column - span, + offset: end.offset - span, + }, + end, + }; +}; + +const removeEmptyTextNodes = (nodes) => + nodes.filter( + (node) => node.type !== 'text' || (node.type === 'text' && !/^\s+$/.test(node.value)), + ); + +const wrapInlineElements = (nodes, wrappableTags) => + nodes.reduce((children, child) => { + const previous = children[children.length - 1]; + + if (child.type !== 'text' && !wrappableTags.includes(child.tagName)) { + return [...children, child]; + } + + const wrapperExists = previous?.properties.wrapper; + + if (wrapperExists) { + const wrapper = previous; + + wrapper.position.end = child.position.end; + wrapper.children.push(child); + + return children; + } + + const wrapper = { + type: 'element', + tagName: 'p', + position: calcTextNodePosition(child), + children: [child], + properties: { wrapper: true }, + }; + + return [...children, wrapper]; + }, []); + /** * Converts a Hast AST to a ProseMirror document based on a series * of specifications that describe how to map all the nodes of the former @@ -445,10 +515,11 @@ const findParent = (ancestors, parent) => { * 2. hasParents: All the hast node’s ancestors up to the root node * 3. source: Markdown source file’s content * - * **wrapTextInParagraph** + * **wrapInParagraph** * - * This property only applies to block nodes. If a block node contains text, - * it will wrap that text in a paragraph. This is useful for ProseMirror block + * This property only applies to block nodes. If a block node contains inline + * elements like text, images, links, etc, the converter will wrap those inline + * elements in a paragraph. This is useful for ProseMirror block * nodes that don’t allow text directly such as list items and tables. * * **processText** @@ -485,7 +556,13 @@ const findParent = (ancestors, parent) => { * * @returns A ProseMirror document */ -export const createProseMirrorDocFromMdastTree = ({ schema, factorySpecs, tree, source }) => { +export const createProseMirrorDocFromMdastTree = ({ + schema, + factorySpecs, + wrappableTags, + tree, + source, +}) => { const proseMirrorNodeFactories = createProseMirrorNodeFactories(schema, factorySpecs, source); const state = new HastToProseMirrorConverterState(); @@ -502,9 +579,23 @@ export const createProseMirrorDocFromMdastTree = ({ schema, factorySpecs, tree, const parent = findParent(ancestors, factory.parent); + if (factory.wrapInParagraph) { + /** + * Modifying parameters is a bad practice. For performance reasons, + * the author of the unist-util-visit-parents function recommends + * modifying nodes in place to avoid traversing the Abstract Syntax + * Tree more than once + */ + // eslint-disable-next-line no-param-reassign + hastNode.children = wrapInlineElements( + removeEmptyTextNodes(hastNode.children), + wrappableTags, + ); + } + factory.handle(state, hastNode, parent); - return factory.skipChildren === true ? 'skip' : true; + return factory.skipChildren === true ? SKIP : true; }); let doc; 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 9fb0d520848..12b3f288e22 100644 --- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js +++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js @@ -2,6 +2,8 @@ import { isString } from 'lodash'; import { render } from '~/lib/gfm'; import { createProseMirrorDocFromMdastTree } from './hast_to_prosemirror_converter'; +const wrappableTags = ['img', 'br', 'code', 'i', 'em', 'b', 'strong', 'a', 'strike', 's', 'del']; + const isTaskItem = (hastNode) => { const { className } = hastNode.properties; @@ -20,9 +22,9 @@ const factorySpecs = { paragraph: { type: 'block', selector: 'p' }, listItem: { type: 'block', - wrapTextInParagraph: true, - processText: (text) => text.trim(), + wrapInParagraph: true, selector: (hastNode) => hastNode.tagName === 'li' && !hastNode.properties.className, + processText: (text) => text.trimRight(), }, orderedList: { type: 'block', @@ -74,12 +76,12 @@ const factorySpecs = { }, taskItem: { type: 'block', - wrapTextInParagraph: true, - processText: (text) => text.trim(), + wrapInParagraph: true, selector: isTaskItem, getAttrs: (hastNode) => ({ checked: hastNode.children[0].properties.checked, }), + processText: (text) => text.trimLeft(), }, taskItemCheckbox: { type: 'ignore', @@ -99,13 +101,13 @@ const factorySpecs = { type: 'block', selector: 'th', getAttrs: getTableCellAttrs, - wrapTextInParagraph: true, + wrapInParagraph: true, }, tableCell: { type: 'block', selector: 'td', getAttrs: getTableCellAttrs, - wrapTextInParagraph: true, + wrapInParagraph: true, }, ignoredTableNodes: { type: 'ignore', @@ -160,6 +162,7 @@ export default () => { schema, factorySpecs, tree, + wrappableTags, source: markdown, }), }); diff --git a/app/assets/javascripts/feature_flags/components/form.vue b/app/assets/javascripts/feature_flags/components/form.vue index 26da0d56f9a..ec5a241bb16 100644 --- a/app/assets/javascripts/feature_flags/components/form.vue +++ b/app/assets/javascripts/feature_flags/components/form.vue @@ -211,16 +211,16 @@ export default { -
+
{{ submitText }} - + {{ __('Cancel') }}
diff --git a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue index 5575c6567b5..98982920121 100644 --- a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue +++ b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue @@ -72,7 +72,7 @@ export default { {{ $options.translations.addEnvironmentsLabel }} - +
- + {{ job.name }} export default { i18n, IssuableListTabs, + IssuableTypes: [IssuableTypes.Issue, IssuableTypes.Incident, IssuableTypes.TestCase], components: { CsvImportExportButtons, GlButton, @@ -168,7 +173,9 @@ export default { issues: { query: getIssuesQuery, variables() { - return this.queryVariables; + const { types } = this.queryVariables; + + return { ...this.queryVariables, types: types ? [types] : this.$options.IssuableTypes }; }, update(data) { return data[this.namespace]?.issues.nodes ?? []; @@ -192,7 +199,9 @@ export default { issuesCounts: { query: getIssuesCountsQuery, variables() { - return this.queryVariables; + const { types } = this.queryVariables; + + return { ...this.queryVariables, types: types ? [types] : this.$options.IssuableTypes }; }, update(data) { return data[this.namespace] ?? {}; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue index 7544382aa4d..43e31037c36 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue @@ -127,12 +127,13 @@ export default { >
[{ query: getStatesQuery }], + refetchQueries: () => [ + { + query: getStatesQuery, + variables: { + projectPath: this.projectPath, + }, + }, + ], awaitRefetchQueries: true, notifyOnNetworkStatusChange: true, }) diff --git a/app/assets/javascripts/terraform/components/terraform_list.vue b/app/assets/javascripts/terraform/components/terraform_list.vue index cc8538ec2e8..f098b447d10 100644 --- a/app/assets/javascripts/terraform/components/terraform_list.vue +++ b/app/assets/javascripts/terraform/components/terraform_list.vue @@ -31,15 +31,12 @@ export default { GlTabs, StatesTable, }, + inject: ['projectPath'], props: { emptyStateImage: { required: true, type: String, }, - projectPath: { - required: true, - type: String, - }, terraformAdmin: { required: false, type: Boolean, diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js index 34261f3c4db..571177986d2 100644 --- a/app/assets/javascripts/terraform/index.js +++ b/app/assets/javascripts/terraform/index.js @@ -30,6 +30,7 @@ export default () => { el, apolloProvider: new VueApollo({ defaultClient }), provide: { + projectPath, accessTokensPath, terraformApiUrl, username, @@ -38,7 +39,6 @@ export default () => { return createElement(TerraformList, { props: { emptyStateImage, - projectPath, terraformAdmin: el.hasAttribute('data-terraform-admin'), }, }); diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index c70f028a876..8bffc2479a1 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -45,12 +45,12 @@ export default { return validSizes.includes(value); }, }, - borderless: { + isActive: { type: Boolean, required: false, default: false, }, - isActive: { + isBorderless: { type: Boolean, required: false, default: false, @@ -72,14 +72,17 @@ export default { return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status} gl-rounded-full gl-justify-content-center`; }, icon() { - return this.borderless ? `${this.status.icon}_borderless` : this.status.icon; + return this.isBorderless ? `${this.status.icon}_borderless` : this.status.icon; }, }, };