From 8de0010c4e112c27e12da8360e415bd072a68c66 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 29 Jul 2024 18:07:42 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/rules.gitlab-ci.yml | 13 + .../services/markdown_serializer.js | 3 +- .../services/remark_markdown_deserializer.js | 309 ---- app/assets/javascripts/lib/gfm/constants.js | 10 - .../gfm/glfm_extensions/table_of_contents.js | 85 - app/assets/javascripts/lib/gfm/index.js | 122 -- .../glfm_mdast_to_hast_handlers.js | 1 - .../sidebar/components/label_filter/data.js | 14 +- .../sidebar/components/label_filter/index.vue | 29 +- .../javascripts/search/store/actions.js | 8 +- .../javascripts/search/store/constants.js | 4 +- .../javascripts/search/store/getters.js | 18 +- .../mr_widget_how_to_merge_modal.vue | 3 +- .../shared/work_item_link_child_contents.vue | 2 +- .../work_item_links/work_item_links.vue | 4 - app/controllers/groups_controller.rb | 9 +- .../assign_attributes_service.rb | 12 + .../templates/_deprecation_template.md.erb | 2 + .../backup_restore/backup_gitlab.md | 4 +- .../geo/replication/datatypes.md | 4 +- .../packages/container_registry.md | 4 +- doc/administration/packages/index.md | 4 +- doc/api/deployments.md | 2 +- doc/ci/testing/browser_performance_testing.md | 2 +- doc/development/contributing/index.md | 2 +- .../database/namespaces_storage_statistics.md | 2 +- doc/development/database_review.md | 4 +- doc/development/fe_guide/getting_started.md | 4 +- doc/development/rake_tasks.md | 6 +- doc/development/real_time.md | 2 +- doc/development/value_stream_analytics.md | 4 +- doc/gitlab-basics/add-file.md | 2 +- doc/topics/git/git_rebase.md | 2 +- doc/topics/git/how_to_install_git/index.md | 2 +- doc/topics/git/index.md | 5 +- doc/topics/git/undo.md | 2 +- doc/update/deprecations.md | 590 +++++++ doc/user/gitlab_duo_chat/index.md | 2 +- .../authorization_for_merge_requests.md | 2 +- locale/gitlab.pot | 3 + package.json | 1 + scripts/frontend/lib/compile_css.mjs | 2 + scripts/frontend/lib/postcss_color_to_hex.js | 39 + spec/controllers/groups_controller_spec.rb | 9 + spec/dot_gitlab_ci/rules_spec.rb | 2 - .../remark_markdown_processing_spec.js | 1411 ----------------- spec/frontend/lib/gfm/index_spec.js | 309 ---- .../frontend/lib/postcss_color_to_hex_spec.js | 67 + spec/frontend/search/mock_data.js | 2 +- .../sidebar/components/label_filter_spec.js | 4 +- spec/frontend/search/store/actions_spec.js | 4 +- spec/frontend/search/store/getters_spec.js | 6 +- .../assign_attributes_service_spec.rb | 22 + .../internal/badgateway/embed/gdk-lg.svg | 2 +- 54 files changed, 851 insertions(+), 2331 deletions(-) delete mode 100644 app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js delete mode 100644 app/assets/javascripts/lib/gfm/constants.js delete mode 100644 app/assets/javascripts/lib/gfm/glfm_extensions/table_of_contents.js delete mode 100644 app/assets/javascripts/lib/gfm/index.js delete mode 100644 app/assets/javascripts/lib/gfm/mdast_to_hast_handlers/glfm_mdast_to_hast_handlers.js create mode 100644 scripts/frontend/lib/postcss_color_to_hex.js delete mode 100644 spec/frontend/content_editor/remark_markdown_processing_spec.js delete mode 100644 spec/frontend/lib/gfm/index_spec.js create mode 100644 spec/frontend/scripts/frontend/lib/postcss_color_to_hex_spec.js diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index e7effa5f846..ce2d1b8dc51 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -696,6 +696,10 @@ - GITLAB_SHELL_VERSION - GITALY_SERVER_VERSION +.ruby-version-patterns: &ruby-version-patterns + - ".ruby-version" + - ".tool-versions" + .audit-events-patterns: &audit-events-patterns - "{,ee/}config/audit_events/types/*.yml" - "doc/user/compliance/audit_event_types.md" @@ -1005,6 +1009,9 @@ - <<: *if-force-ci when: manual # The rest is included to be consistent with .qa:rules:e2e:test-on-gdk + # Run tests automatically for MRs that touch Ruby version files + - <<: *if-merge-request + changes: *ruby-version-patterns # Run tests automatically for MRs that touch QA files - <<: *if-merge-request changes: *qa-patterns @@ -1599,6 +1606,9 @@ - <<: *if-merge-request-targeting-stable-branch changes: *setup-test-env-patterns allow_failure: true + # Run tests automatically for MRs that touch Ruby version files + - <<: *if-merge-request + changes: *ruby-version-patterns # Run tests automatically for MRs that touch GITALY_SERVER_VERSION - <<: *if-merge-request changes: *gitaly-patterns @@ -1670,6 +1680,9 @@ rules: - !reference [".qa:rules:package-and-test-never-run", rules] - <<: *if-merge-request-labels-run-all-e2e + # Run tests automatically for MRs that touch Ruby version files + - <<: *if-merge-request + changes: *ruby-version-patterns # Run tests automatically for MRs that touch QA files - <<: *if-merge-request changes: *qa-patterns diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index 66bb0edcf85..80a8c7e1a9a 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -157,8 +157,7 @@ export default class MarkdownSerializer { * Markdown from which the node was generated using a Markdown * deserializer. * - * See the Sourcemap metadata extension and the remark_markdown_deserializer - * service for more information. + * See the Sourcemap metadata extension for more information. * * @param {ProseMirror.Node} params.doc ProseMirror document to convert into Markdown * @param {ProseMirror.Node} params.pristineDoc Pristine version of the document that diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js deleted file mode 100644 index ca290efca11..00000000000 --- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js +++ /dev/null @@ -1,309 +0,0 @@ -import { render } from '~/lib/gfm'; -import { isValidAttribute } from '~/lib/dompurify'; -import { SAFE_AUDIO_EXT, SAFE_VIDEO_EXT, DIAGRAM_LANGUAGES } from '../constants'; -import { createProseMirrorDocFromMdastTree } from './hast_to_prosemirror_converter'; - -const ALL_AUDIO_VIDEO_EXT = [...SAFE_AUDIO_EXT, ...SAFE_VIDEO_EXT]; - -const wrappableTags = ['img', 'br', 'code', 'i', 'em', 'b', 'strong', 'a', 'strike', 's', 'del']; - -const isTaskItem = (hastNode) => { - const className = hastNode.properties?.className; - - return ( - hastNode.tagName === 'li' && Array.isArray(className) && className.includes('task-list-item') - ); -}; - -const getTableCellAttrs = (hastNode) => ({ - colspan: parseInt(hastNode.properties.colSpan, 10) || 1, - rowspan: parseInt(hastNode.properties.rowSpan, 10) || 1, -}); - -const getMediaAttrs = (hastNode) => ({ - src: hastNode.properties.src, - canonicalSrc: hastNode.properties.identifier ?? hastNode.properties.src, - isReference: hastNode.properties.isReference === 'true', - title: hastNode.properties.title, - alt: hastNode.properties.alt, -}); - -const isMediaTag = (hastNode) => hastNode.tagName === 'img' && Boolean(hastNode.properties); - -const extractMediaFileExtension = (url) => { - try { - const parsedUrl = new URL(url, window.location.origin); - - return /\.(\w+)$/.exec(parsedUrl.pathname)?.[1] ?? null; - } catch { - return null; - } -}; - -const isCodeBlock = (hastNode) => hastNode.tagName === 'codeblock'; - -const isDiagramCodeBlock = (hastNode) => DIAGRAM_LANGUAGES.includes(hastNode.properties?.language); - -const getCodeBlockAttrs = (hastNode) => ({ language: hastNode.properties.language }); - -const factorySpecs = { - blockquote: { type: 'block', selector: 'blockquote' }, - paragraph: { type: 'block', selector: 'p' }, - listItem: { - type: 'block', - wrapInParagraph: true, - selector: (hastNode) => hastNode.tagName === 'li' && !hastNode.properties?.className, - processText: (text) => text.trimRight(), - }, - orderedList: { - type: 'block', - selector: (hastNode) => hastNode.tagName === 'ol' && !hastNode.properties?.className, - }, - bulletList: { - type: 'block', - selector: (hastNode) => hastNode.tagName === 'ul' && !hastNode.properties?.className, - }, - heading: { - type: 'block', - selector: (hastNode) => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(hastNode.tagName), - getAttrs: (hastNode) => { - const level = parseInt(/(\d)$/.exec(hastNode.tagName)?.[1], 10) || 1; - - return { level }; - }, - }, - codeBlock: { - type: 'block', - selector: (hastNode) => isCodeBlock(hastNode) && !isDiagramCodeBlock(hastNode), - getAttrs: getCodeBlockAttrs, - }, - diagram: { - type: 'block', - selector: (hastNode) => isCodeBlock(hastNode) && isDiagramCodeBlock(hastNode), - getAttrs: getCodeBlockAttrs, - }, - horizontalRule: { - type: 'block', - selector: 'hr', - }, - taskList: { - type: 'block', - selector: (hastNode) => { - const className = hastNode.properties?.className; - - return ( - ['ul', 'ol'].includes(hastNode.tagName) && - Array.isArray(className) && - className.includes('contains-task-list') - ); - }, - getAttrs: (hastNode) => ({ - numeric: hastNode.tagName === 'ol', - }), - }, - taskItem: { - type: 'block', - wrapInParagraph: true, - selector: isTaskItem, - getAttrs: (hastNode) => ({ - checked: hastNode.children[0].properties.checked, - }), - processText: (text) => text.trimLeft(), - }, - taskItemCheckbox: { - type: 'ignore', - selector: (hastNode, ancestors) => - hastNode.tagName === 'input' && isTaskItem(ancestors[ancestors.length - 1]), - }, - div: { - type: 'block', - selector: 'div', - wrapInParagraph: true, - }, - table: { - type: 'block', - selector: 'table', - }, - tableRow: { - type: 'block', - selector: 'tr', - parent: 'table', - }, - tableHeader: { - type: 'block', - selector: 'th', - getAttrs: getTableCellAttrs, - wrapInParagraph: true, - }, - tableCell: { - type: 'block', - selector: 'td', - getAttrs: getTableCellAttrs, - wrapInParagraph: true, - }, - ignoredTableNodes: { - type: 'ignore', - selector: (hastNode) => ['thead', 'tbody', 'tfoot'].includes(hastNode.tagName), - }, - footnoteDefinition: { - type: 'block', - selector: 'footnotedefinition', - getAttrs: (hastNode) => hastNode.properties, - }, - pre: { - type: 'block', - selector: 'pre', - wrapInParagraph: true, - }, - audio: { - type: 'inline', - selector: (hastNode) => - isMediaTag(hastNode) && - SAFE_AUDIO_EXT.includes(extractMediaFileExtension(hastNode.properties.src)), - getAttrs: getMediaAttrs, - }, - image: { - type: 'inline', - selector: (hastNode) => - isMediaTag(hastNode) && - !ALL_AUDIO_VIDEO_EXT.includes(extractMediaFileExtension(hastNode.properties.src)), - getAttrs: getMediaAttrs, - }, - video: { - type: 'inline', - selector: (hastNode) => - isMediaTag(hastNode) && - SAFE_VIDEO_EXT.includes(extractMediaFileExtension(hastNode.properties.src)), - getAttrs: getMediaAttrs, - }, - hardBreak: { - type: 'inline', - selector: 'br', - }, - footnoteReference: { - type: 'inline', - selector: 'footnotereference', - getAttrs: (hastNode) => hastNode.properties, - }, - code: { - type: 'mark', - selector: 'code', - }, - italic: { - type: 'mark', - selector: (hastNode) => ['em', 'i'].includes(hastNode.tagName), - }, - bold: { - type: 'mark', - selector: (hastNode) => ['strong', 'b'].includes(hastNode.tagName), - }, - link: { - type: 'mark', - selector: 'a', - getAttrs: (hastNode) => ({ - canonicalSrc: hastNode.properties.identifier ?? hastNode.properties.href, - href: hastNode.properties.href, - isReference: hastNode.properties.isReference === 'true', - title: hastNode.properties.title, - }), - }, - strike: { - type: 'mark', - selector: (hastNode) => ['strike', 's', 'del'].includes(hastNode.tagName), - }, - /* TODO - * Implement proper editing support for HTML comments in the Content Editor - * https://gitlab.com/gitlab-org/gitlab/-/issues/342173 - */ - comment: { - type: 'ignore', - selector: (hastNode) => hastNode.type === 'comment', - }, - - referenceDefinition: { - type: 'block', - selector: 'referencedefinition', - getAttrs: (hastNode) => ({ - title: hastNode.properties.title, - url: hastNode.properties.url, - identifier: hastNode.properties.identifier, - }), - }, - - frontmatter: { - type: 'block', - selector: 'frontmatter', - getAttrs: (hastNode) => ({ - language: hastNode.properties.language, - }), - }, - - tableOfContents: { - type: 'block', - selector: 'tableofcontents', - }, -}; - -const SANITIZE_ALLOWLIST = ['level', 'identifier', 'numeric', 'language', 'url', 'isReference']; - -const sanitizeAttribute = (attributeName, attributeValue, hastNode) => { - if (!attributeValue || SANITIZE_ALLOWLIST.includes(attributeName)) { - return attributeValue; - } - - /** - * This is a workaround to validate the value of the canonicalSrc - * attribute using DOMPurify without passing the attribute name. canonicalSrc - * is not an allowed attribute in DOMPurify therefore the library will remove - * it regardless of its value. - * - * We want to preserve canonicalSrc, and we also want to make sure that its - * value is sanitized. - */ - const validateAttributeAs = attributeName === 'canonicalSrc' ? 'src' : attributeName; - - if (!isValidAttribute(hastNode.tagName, validateAttributeAs, attributeValue)) { - return null; - } - - return attributeValue; -}; - -const attributeTransformer = { - transform: (attributeName, attributeValue, hastNode) => { - return sanitizeAttribute(attributeName, attributeValue, hastNode); - }, -}; - -export default () => { - return { - deserialize: async ({ schema, markdown }) => { - const document = await render({ - markdown, - renderer: (tree) => - createProseMirrorDocFromMdastTree({ - schema, - factorySpecs, - tree, - wrappableTags, - attributeTransformer, - markdown, - }), - skipRendering: [ - 'footnoteReference', - 'footnoteDefinition', - 'code', - 'definition', - 'linkReference', - 'imageReference', - 'yaml', - 'toml', - 'json', - 'tableOfContents', - ], - }); - - return { document }; - }, - }; -}; diff --git a/app/assets/javascripts/lib/gfm/constants.js b/app/assets/javascripts/lib/gfm/constants.js deleted file mode 100644 index eaabeb2a767..00000000000 --- a/app/assets/javascripts/lib/gfm/constants.js +++ /dev/null @@ -1,10 +0,0 @@ -export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN = '[['; -export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN = 'TOC'; -export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN = ']]'; -export const TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN = '[TOC]'; - -export const MDAST_TEXT_NODE = 'text'; -export const MDAST_EMPHASIS_NODE = 'emphasis'; -export const MDAST_PARAGRAPH_NODE = 'paragraph'; - -export const GLFM_TABLE_OF_CONTENTS_NODE = 'tableOfContents'; diff --git a/app/assets/javascripts/lib/gfm/glfm_extensions/table_of_contents.js b/app/assets/javascripts/lib/gfm/glfm_extensions/table_of_contents.js deleted file mode 100644 index 4d2484a657a..00000000000 --- a/app/assets/javascripts/lib/gfm/glfm_extensions/table_of_contents.js +++ /dev/null @@ -1,85 +0,0 @@ -import { first, last } from 'lodash'; -import { u } from 'unist-builder'; -import { visitParents, SKIP, CONTINUE } from 'unist-util-visit-parents'; -import { - TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN, - TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN, - TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN, - TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN, - MDAST_TEXT_NODE, - MDAST_EMPHASIS_NODE, - MDAST_PARAGRAPH_NODE, - GLFM_TABLE_OF_CONTENTS_NODE, -} from '../constants'; - -const isTOCTextNode = ({ type, value }) => - type === MDAST_TEXT_NODE && value === TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN; - -const isTOCEmphasisNode = ({ type, children }) => - type === MDAST_EMPHASIS_NODE && children.length === 1 && isTOCTextNode(first(children)); - -const isTOCDoubleSquareBracketOpenTokenTextNode = ({ type, value }) => - type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN; - -const isTOCDoubleSquareBracketCloseTokenTextNode = ({ type, value }) => - type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN; - -/* - * Detects table of contents declaration with syntax [[_TOC_]] - */ -const isTableOfContentsDoubleSquareBracketSyntax = ({ children }) => { - if (children.length !== 3) { - return false; - } - - const [firstChild, middleChild, lastChild] = children; - - return ( - isTOCDoubleSquareBracketOpenTokenTextNode(firstChild) && - isTOCEmphasisNode(middleChild) && - isTOCDoubleSquareBracketCloseTokenTextNode(lastChild) - ); -}; - -/* - * Detects table of contents declaration with syntax [TOC] - */ -const isTableOfContentsSingleSquareBracketSyntax = ({ children }) => { - if (children.length !== 1) { - return false; - } - - const [firstChild] = children; - const { type, value } = firstChild; - - return type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN; -}; - -const isTableOfContentsNode = (node) => - node.type === MDAST_PARAGRAPH_NODE && - (isTableOfContentsDoubleSquareBracketSyntax(node) || - isTableOfContentsSingleSquareBracketSyntax(node)); - -export default () => { - return (tree) => { - visitParents(tree, (node, ancestors) => { - const parent = last(ancestors); - - if (!parent) { - return CONTINUE; - } - - if (isTableOfContentsNode(node)) { - const index = parent.children.indexOf(node); - - parent.children[index] = u(GLFM_TABLE_OF_CONTENTS_NODE, { - position: node.position, - }); - } - - return SKIP; - }); - - return tree; - }; -}; diff --git a/app/assets/javascripts/lib/gfm/index.js b/app/assets/javascripts/lib/gfm/index.js deleted file mode 100644 index fad73f93c1a..00000000000 --- a/app/assets/javascripts/lib/gfm/index.js +++ /dev/null @@ -1,122 +0,0 @@ -import { pick } from 'lodash'; -import normalize from 'mdurl/encode'; -import { unified } from 'unified'; -import remarkParse from 'remark-parse'; -import remarkFrontmatter from 'remark-frontmatter'; -import remarkGfm from 'remark-gfm'; -import remarkRehype, { all } from 'remark-rehype'; -import rehypeRaw from 'rehype-raw'; -import glfmTableOfContents from './glfm_extensions/table_of_contents'; -import * as glfmMdastToHastHandlers from './mdast_to_hast_handlers/glfm_mdast_to_hast_handlers'; - -const skipFrontmatterHandler = (language) => (h, node) => - h(node.position, 'frontmatter', { language }, [{ type: 'text', value: node.value }]); - -const skipRenderingHandlers = { - footnoteReference: (h, node) => - h(node.position, 'footnoteReference', { identifier: node.identifier, label: node.label }, []), - footnoteDefinition: (h, node) => - h( - node.position, - 'footnoteDefinition', - { identifier: node.identifier, label: node.label }, - all(h, node), - ), - code: (h, node) => - h(node.position, 'codeBlock', { language: node.lang, meta: node.meta }, [ - { type: 'text', value: node.value }, - ]), - definition: (h, node) => { - const title = node.title ? ` "${node.title}"` : ''; - - return h( - node.position, - 'referenceDefinition', - { identifier: node.identifier, url: node.url, title: node.title }, - [{ type: 'text', value: `[${node.identifier}]: ${node.url}${title}` }], - ); - }, - linkReference: (h, node) => { - const definition = h.definition(node.identifier); - - return h( - node.position, - 'a', - { - href: normalize(definition.url ?? ''), - identifier: node.identifier, - isReference: 'true', - title: definition.title, - }, - all(h, node), - ); - }, - imageReference: (h, node) => { - const definition = h.definition(node.identifier); - - return h( - node.position, - 'img', - { - src: normalize(definition.url ?? ''), - alt: node.alt, - identifier: node.identifier, - isReference: 'true', - title: definition.title, - }, - all(h, node), - ); - }, - tableOfContents: (h, node) => h(node.position, 'tableOfContents'), - toml: skipFrontmatterHandler('toml'), - yaml: skipFrontmatterHandler('yaml'), - json: skipFrontmatterHandler('json'), -}; - -const createParser = ({ skipRendering }) => { - return unified() - .use(remarkParse) - .use(remarkGfm) - .use(remarkFrontmatter, ['yaml', 'toml', { type: 'json', marker: ';' }]) - .use(glfmTableOfContents) - .use(remarkRehype, { - allowDangerousHtml: true, - handlers: { - ...glfmMdastToHastHandlers, - ...pick(skipRenderingHandlers, skipRendering), - }, - }) - .use(rehypeRaw); -}; - -const compilerFactory = (renderer) => - function compiler() { - Object.assign(this, { - Compiler(tree) { - return renderer(tree); - }, - }); - }; - -/** - * Parses a Markdown string and provides the result Abstract - * Syntax Tree (AST) to a renderer function to convert the - * tree in any desired representation - * - * @param {String} params.markdown Markdown to parse - * @param {Function} params.renderer A function that accepts mdast - * AST tree and returns an object of any type that represents the result of - * rendering the tree. See the references below to for more information - * about MDast. - * - * MDastTree documentation https://github.com/syntax-tree/mdast - * @returns {Promise} Returns a promise with the result of rendering - * the MDast tree - */ -export const render = async ({ markdown, renderer, skipRendering = [] }) => { - const { result } = await createParser({ skipRendering }) - .use(compilerFactory(renderer)) - .process(markdown); - - return result; -}; diff --git a/app/assets/javascripts/lib/gfm/mdast_to_hast_handlers/glfm_mdast_to_hast_handlers.js b/app/assets/javascripts/lib/gfm/mdast_to_hast_handlers/glfm_mdast_to_hast_handlers.js deleted file mode 100644 index 91b09e69405..00000000000 --- a/app/assets/javascripts/lib/gfm/mdast_to_hast_handlers/glfm_mdast_to_hast_handlers.js +++ /dev/null @@ -1 +0,0 @@ -export const tableOfContents = (h, node) => h(node.position, 'nav'); diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/data.js b/app/assets/javascripts/search/sidebar/components/label_filter/data.js index 654357da902..b76bc8dc4f3 100644 --- a/app/assets/javascripts/search/sidebar/components/label_filter/data.js +++ b/app/assets/javascripts/search/sidebar/components/label_filter/data.js @@ -8,16 +8,8 @@ export const SEARCH_INPUT_DESCRIPTION = 'label-search-input-description'; export const SEARCH_RESULTS_DESCRIPTION = 'label-search-results-description'; -const header = __('Labels'); +export const LABEL_FILTER_HEADER = __('Labels'); -const scopes = { - ISSUES: 'issues', -}; +export const LABEL_FILTER_PARAM = 'labelName'; -const filterParam = 'labels'; - -export const labelFilterData = { - header, - scopes, - filterParam, -}; +export const LABEL_AGREGATION_NAME = 'labels'; diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue index 1f4bd2e8f6d..983dbc450c8 100644 --- a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue @@ -12,7 +12,7 @@ import { } from '@gitlab/ui'; // eslint-disable-next-line no-restricted-imports import { mapActions, mapState, mapGetters } from 'vuex'; -import { uniq } from 'lodash'; +import { difference, uniq } from 'lodash'; import { rgbFromHex } from '@gitlab/ui/dist/utils/utils'; import { slugify } from '~/lib/utils/text_utility'; @@ -26,7 +26,7 @@ import { SEARCH_BOX_INDEX, SEARCH_RESULTS_DESCRIPTION, SEARCH_INPUT_DESCRIPTION, - labelFilterData, + LABEL_FILTER_PARAM, } from './data'; import { trackSelectCheckbox, trackOpenDropdown } from './tracking'; @@ -50,6 +50,7 @@ export default { return { currentFocusIndex: SEARCH_BOX_INDEX, isFocused: false, + combinedSelectedLabels: [], }; }, i18n: I18N, @@ -83,7 +84,6 @@ export default { combinedSelectedFilters() { const appliedSelectedLabelKeys = this.appliedSelectedLabels.map((label) => label.key); const { labels = [] } = this.query; - return uniq([...appliedSelectedLabelKeys, ...labels]); }, searchLabels: { @@ -96,16 +96,29 @@ export default { }, selectedLabels: { get() { - return this.combinedSelectedFilters; + return this.combinedSelectedLabels; }, set(value) { - this.setQuery({ key: this.$options.labelFilterData?.filterParam, value }); + this.setQuery({ key: this.$options.LABEL_FILTER_PARAM, value }); trackSelectCheckbox(value); }, }, }, + watch: { + combinedSelectedFilters(newLabels, oldLabels) { + const hasDifference = difference(newLabels, oldLabels).length > 0; + if (hasDifference) { + this.combinedSelectedLabels = newLabels; + } + }, + filteredAppliedSelectedLabels(newCount, oldCount) { + if (newCount.length !== oldCount.length) { + this.currentFocusIndex = FIRST_DROPDOWN_INDEX; + } + }, + }, async created() { - if (this.urlQuery?.[labelFilterData.filterParam]?.length > 0) { + if (this.urlQuery?.[LABEL_FILTER_PARAM]?.length > 0) { await this.fetchAllAggregation(); } }, @@ -142,14 +155,14 @@ export default { FIRST_DROPDOWN_INDEX, SEARCH_RESULTS_DESCRIPTION, SEARCH_INPUT_DESCRIPTION, - labelFilterData, + LABEL_FILTER_PARAM, };