Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-07-29 18:07:42 +00:00
parent 1fec509ff1
commit 8de0010c4e
54 changed files with 851 additions and 2331 deletions

View File

@ -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

View File

@ -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

View File

@ -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 };
},
};
};

View File

@ -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';

View File

@ -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;
};
};

View File

@ -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;
};

View File

@ -1 +0,0 @@
export const tableOfContents = (h, node) => h(node.position, 'nav');

View File

@ -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';

View File

@ -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,
};
</script>
<template>
<div class="gl-pb-0 gl-md-pt-0 label-filter gl-relative">
<div class="gl-mb-2 gl-font-bold gl-font-sm" data-testid="label-filter-title">
{{ $options.labelFilterData.header }}
{{ $options.LABEL_FILTER_HEADER }}
</div>
<div>
<gl-label

View File

@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import { visitUrl, setUrlParams, getNormalizedURL } from '~/lib/utils/url_utility';
import { logError } from '~/lib/logger';
import { __ } from '~/locale';
import { labelFilterData } from '~/search/sidebar/components/label_filter/data';
import { LABEL_FILTER_PARAM } from '~/search/sidebar/components/label_filter/data';
import { SCOPE_BLOB } from '~/search/sidebar/constants';
import {
GROUPS_LOCAL_STORAGE_KEY,
@ -104,7 +104,6 @@ export const setFrequentProject = ({ state, commit }, item) => {
export const setQuery = ({ state, commit }, { key, value }) => {
commit(types.SET_QUERY, { key, value });
if (SIDEBAR_PARAMS.includes(key)) {
commit(types.SET_SIDEBAR_DIRTY, isSidebarDirty(state.query, state.urlQuery));
}
@ -138,9 +137,8 @@ export const resetQuery = ({ state }) => {
};
export const closeLabel = ({ state, commit }, { key }) => {
const labels = state?.query?.labels.filter((labelKey) => labelKey !== key);
setQuery({ state, commit }, { key: labelFilterData.filterParam, value: labels });
const labels = state?.query?.[LABEL_FILTER_PARAM].filter((labelKey) => labelKey !== key);
setQuery({ state, commit }, { key: LABEL_FILTER_PARAM, value: labels });
};
export const setLabelFilterSearch = ({ commit }, { value }) => {

View File

@ -1,7 +1,7 @@
import { statusFilterData } from '~/search/sidebar/components/status_filter/data';
import { confidentialFilterData } from '~/search/sidebar/components/confidentiality_filter/data';
import { languageFilterData } from '~/search/sidebar/components/language_filter/data';
import { labelFilterData } from '~/search/sidebar/components/label_filter/data';
import { LABEL_FILTER_PARAM } from '~/search/sidebar/components/label_filter/data';
import { archivedFilterData } from '~/search/sidebar/components/archived_filter/data';
import { INCLUDE_FORKED_FILTER_PARAM } from '~/search/sidebar/components/forks_filter/index.vue';
@ -17,7 +17,7 @@ export const SIDEBAR_PARAMS = [
statusFilterData.filterParam,
confidentialFilterData.filterParam,
languageFilterData.filterParam,
labelFilterData.filterParam,
LABEL_FILTER_PARAM,
archivedFilterData.filterParam,
INCLUDE_FORKED_FILTER_PARAM,
];

View File

@ -1,6 +1,9 @@
import { findKey, intersection } from 'lodash';
import { languageFilterData } from '~/search/sidebar/components/language_filter/data';
import { labelFilterData } from '~/search/sidebar/components/label_filter/data';
import {
LABEL_FILTER_PARAM,
LABEL_AGREGATION_NAME,
} from '~/search/sidebar/components/label_filter/data';
import {
formatSearchResultCount,
addCountOverLimit,
@ -10,8 +13,8 @@ import {
import { PROJECT_DATA, SCOPE_BLOB } from '~/search/sidebar/constants';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, ICON_MAP } from './constants';
const queryLabelFilters = (state) => state?.query?.[labelFilterData.filterParam] || [];
const urlQueryLabelFilters = (state) => state?.urlQuery?.[labelFilterData.filterParam] || [];
const queryLabelFilters = (state) => state?.query?.[LABEL_FILTER_PARAM] || [];
const urlQueryLabelFilters = (state) => state?.urlQuery?.[LABEL_FILTER_PARAM] || [];
const appliedSelectedLabelsKeys = (state) =>
intersection(urlQueryLabelFilters(state), queryLabelFilters(state));
@ -20,7 +23,9 @@ const unselectedLabelsKeys = (state) =>
urlQueryLabelFilters(state)?.filter((label) => !queryLabelFilters(state)?.includes(label));
const unappliedNewLabelKeys = (state) =>
state?.query?.labels?.filter((label) => !urlQueryLabelFilters(state)?.includes(label));
state?.query?.[LABEL_FILTER_PARAM]?.filter(
(label) => !urlQueryLabelFilters(state)?.includes(label),
);
export const queryLanguageFilters = (state) => state.query[languageFilterData.filterParam] || [];
@ -42,9 +47,8 @@ export const languageAggregationBuckets = (state) => {
export const labelAggregationBuckets = (state) => {
return (
state?.aggregations?.data?.find(
(aggregation) => aggregation.name === labelFilterData.filterParam,
)?.buckets || []
state?.aggregations?.data?.find((aggregation) => aggregation.name === LABEL_AGREGATION_NAME)
?.buckets || []
);
};

View File

@ -121,8 +121,9 @@ export default {
<gl-modal
ref="modal"
modal-id="modal-merge-info"
no-focus-on-show
:title="$options.i18n.title"
:no-enforce-focus="true"
no-focus-on-show
no-fade
hide-footer
>

View File

@ -175,7 +175,7 @@ export default {
<gl-link
:href="childItem.webUrl"
class="gl-break-words gl-hyphens-auto gl-font-semibold"
@click="$emit('click', $event)"
@click.exact="$emit('click', $event)"
@mouseover="$emit('mouseover')"
@mouseout="$emit('mouseout')"
>

View File

@ -13,7 +13,6 @@ import { createAlert } from '~/alert';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_ISSUE, TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import { isMetaKey } from '~/lib/utils/common_utils';
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
@ -185,9 +184,6 @@ export default {
this.isShownAddForm = false;
},
openChild({ event, child }) {
if (isMetaKey(event)) {
return;
}
event.preventDefault();
this.activeChild = child;
this.$refs.modal.show();

View File

@ -154,9 +154,14 @@ class GroupsController < Groups::ApplicationController
def update
if Groups::UpdateService.new(@group, current_user, group_params).execute
notice = "Group '#{@group.name}' was successfully updated."
redirect_to edit_group_origin_location, notice: notice
if @group.namespace_settings.errors.present?
flash[:alert] = group.namespace_settings.errors.full_messages.to_sentence
else
flash[:notice] = "Group '#{@group.name}' was successfully updated."
end
redirect_to edit_group_origin_location
else
@group.reset
render action: "edit"

View File

@ -40,6 +40,7 @@ module NamespaceSettings
user_policy: :update_git_access_protocol
)
handle_default_branch_name
handle_default_branch_protection unless settings_params[:default_branch_protection].blank?
handle_early_access_program_participation
@ -52,6 +53,17 @@ module NamespaceSettings
private
def handle_default_branch_name
default_branch_key = :default_branch_name
return if settings_params[default_branch_key].blank?
unless Gitlab::GitRefValidator.validate(settings_params[default_branch_key])
settings_params.delete(default_branch_key)
group.namespace_settings.errors.add(default_branch_key, _('is invalid.'))
end
end
def handle_default_branch_protection
# We are migrating default_branch_protection from an integer
# column to a jsonb column. While completing the rest of the

View File

@ -60,6 +60,7 @@ For deprecation reviewers (Technical Writers only):
### <%= deprecation["title"] %>
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone"><%= deprecation["announcement_milestone"]%></span>
<% if deprecation["end_of_support_milestone"] -%>
- End of Support in GitLab <span class="milestone"><%= deprecation["end_of_support_milestone"]%></span>
@ -70,6 +71,7 @@ For deprecation reviewers (Technical Writers only):
<% if deprecation["issue_url"] -%>
- To discuss this change or learn more, see the [deprecation issue](<%= deprecation["issue_url"]%>).
<%- end -%>
</div>
<%= deprecation["body"] -%>

View File

@ -455,8 +455,8 @@ Depending on your installation type, slightly different components can be skippe
- `terraform_state` (Terraform states)
- `registry` (Container registry images)
- `packages` (Packages)
- `ci_secure_files` (Project-level Secure Files)
- `external_diffs` (External Merge Request diffs)
- `ci_secure_files` (Project-level secure files)
- `external_diffs` (External merge request diffs)
:::TabTitle Helm chart (Kubernetes)

View File

@ -54,8 +54,8 @@ verification methods:
| Blobs | Terraform Module Registry _(object storage)_ | Geo with API/Managed <sup>2</sup> | SHA256 checksum <sup>3</sup> |
| Blobs | Versioned Terraform State _(file system)_ | Geo with API | SHA256 checksum |
| Blobs | Versioned Terraform State _(object storage)_ | Geo with API/Managed <sup>2</sup> | SHA256 checksum <sup>3</sup> |
| Blobs | External Merge Request Diffs _(file system)_ | Geo with API | SHA256 checksum |
| Blobs | External Merge Request Diffs _(object storage)_ | Geo with API/Managed <sup>2</sup> | SHA256 checksum <sup>3</sup> |
| Blobs | External merge request diffs _(file system)_ | Geo with API | SHA256 checksum |
| Blobs | External merge request diffs _(object storage)_ | Geo with API/Managed <sup>2</sup> | SHA256 checksum <sup>3</sup> |
| Blobs | Pipeline artifacts _(file system)_ | Geo with API | SHA256 checksum |
| Blobs | Pipeline artifacts _(object storage)_ | Geo with API/Managed <sup>2</sup> | SHA256 checksum <sup>3</sup> |
| Blobs | Pages _(file system)_ | Geo with API | SHA256 checksum |

View File

@ -1094,6 +1094,8 @@ DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** Self-managed
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/423459) in GitLab 17.3.
The metadata database enables many new registry features, including
online garbage collection, and increases the efficiency of many registry operations.
See the [Container registry metadata database](container_registry_metadata_database.md) page for details.
@ -1288,8 +1290,6 @@ blobs start being deleted is anything permanent done.
### Continuous zero-downtime garbage collection
DETAILS:
You can run garbage collection in the background without the need to schedule it or require read-only mode,
if you migrate to the [metadata database](container_registry_metadata_database.md).

View File

@ -43,14 +43,14 @@ guides you through the process.
| CocoaPods | [#36890](https://gitlab.com/gitlab-org/gitlab/-/issues/36890) |
| Conda | [#36891](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) |
| CRAN | [#36892](https://gitlab.com/gitlab-org/gitlab/-/issues/36892) |
| Debian | [Draft: Merge Request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50438) |
| Debian | [Draft: Merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50438) |
| Opkg | [#36894](https://gitlab.com/gitlab-org/gitlab/-/issues/36894) |
| P2 | [#36895](https://gitlab.com/gitlab-org/gitlab/-/issues/36895) |
| Puppet | [#36897](https://gitlab.com/gitlab-org/gitlab/-/issues/36897) |
| RPM | [#5932](https://gitlab.com/gitlab-org/gitlab/-/issues/5932) |
| RubyGems | [#803](https://gitlab.com/gitlab-org/gitlab/-/issues/803) |
| SBT | [#36898](https://gitlab.com/gitlab-org/gitlab/-/issues/36898) |
| Terraform | [Draft: Merge Request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18834) |
| Terraform | [Draft: Merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18834) |
| Vagrant | [#36899](https://gitlab.com/gitlab-org/gitlab/-/issues/36899) |
<!-- vale gitlab.Spelling = YES -->

View File

@ -500,7 +500,7 @@ This API retrieves the list of merge requests shipped with a given deployment:
GET /projects/:id/deployments/:deployment_id/merge_requests
```
It supports the same parameters as the [Merge Requests API](merge_requests.md#list-merge-requests) and returns a response using the same format:
It supports the same parameters as the [Merge requests API](merge_requests.md#list-merge-requests) and returns a response using the same format:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments/42/merge_requests"

View File

@ -23,7 +23,7 @@ GitLab uses [Sitespeed.io](https://www.sitespeed.io), a free and open source
tool, for measuring the rendering performance of web sites. The
[Sitespeed plugin](https://gitlab.com/gitlab-org/gl-performance) that GitLab built outputs
the performance score for each page analyzed in a file called `browser-performance.json`
this data can be shown on Merge Requests.
this data can be shown on merge requests.
## Use cases

View File

@ -108,7 +108,7 @@ Follow [Configure GDK-in-a-box](first_contribution/configure-dev-env-gdk-in-a-bo
### How community merge requests are triaged
When you create a merge request, a Merge Request Coach will assign relevant reviewers or
When you create a merge request, a merge request coach will assign relevant reviewers or
guide you through the review themselves if possible.
The goal is to have a merge request reviewed within a week after a reviewer is assigned.

View File

@ -187,7 +187,7 @@ performant approach of aggregating the root namespaces.
All the details regarding this use case can be found on:
- <https://gitlab.com/gitlab-org/gitlab-foss/-/issues/62214>
- Merge Request with the implementation: <https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28996>
- Merge request with the implementation: <https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28996>
Performance of the namespace storage statistics were measured in staging and production (GitLab.com). All results were posted
on <https://gitlab.com/gitlab-org/gitlab-foss/-/issues/64092>: No problem has been reported so far.

View File

@ -236,10 +236,10 @@ Using `update`, `upsert`, `delete`, `update_all`, `upsert_all`, `delete_all` or
ActiveRecord methods requires extra care because they modify data and can perform poorly, or they
can destroy data if improperly scoped. These methods are also
[incompatible with Common Table Expression (CTE) statements](sql.md#when-to-use-common-table-expressions).
Danger will comment on a Merge Request Diff when these methods are used.
Danger will comment on a merge request diff when these methods are used.
Follow documentation for [preparation when adding or modifying queries](#preparation-when-adding-or-modifying-queries)
to add the raw SQL query and query plan to the Merge Request description, and request a database review.
to add the raw SQL query and query plan to the merge request description, and request a database review.
### How to review for database

View File

@ -6,7 +6,7 @@ info: Any user with at least the Maintainer role can merge updates to this conte
# Getting started
This page will guide you through the Frontend development process and show you what a normal Merge Request cycle looks like. You can find more about the organization of the frontend team in the [handbook](https://handbook.gitlab.com/handbook/engineering/frontend/).
This page will guide you through the Frontend development process and show you what a normal merge request cycle looks like. You can find more about the organization of the frontend team in the [handbook](https://handbook.gitlab.com/handbook/engineering/frontend/).
There are a lot of things to consider for a first merge request and it can feel overwhelming. The [Frontend onboarding course](onboarding_course/index.md) provides a 6-week structured curriculum to learn how to contribute to the GitLab frontend.
@ -37,7 +37,7 @@ If all of these questions have an answer, then you can safely move on to writing
Make sure to communicate with your team as you progress or if you are unable to work on a planned issue for a long period of time.
If you require assistance, make sure to push your branch and share your Merge Request either directly to a teammate or in the Slack channel `#frontend` to get advice on how to move forward. You can [mark your Merge Request as a draft](../../user/project/merge_requests/drafts.md), which will clearly communicate that it is not ready for a full on review. Always remember to have a [low level of shame](https://handbook.gitlab.com/handbook/values/#low-level-of-shame) and **ask for help when you need it**.
If you require assistance, make sure to push your branch and share your merge request either directly to a teammate or in the Slack channel `#frontend` to get advice on how to move forward. You can [mark your merge request as a draft](../../user/project/merge_requests/drafts.md), which will clearly communicate that it is not ready for a full on review. Always remember to have a [low level of shame](https://handbook.gitlab.com/handbook/values/#low-level-of-shame) and **ask for help when you need it**.
As you write code, make sure to test your change thoroughly. It is the author's responsibility to test their code, ensure that it works as expected, and ensure that it did not break existing behaviours. Reviewers may help in that regard, but **do not expect it**. Make sure to check different browsers, mobile viewports and unexpected user flows.

View File

@ -283,9 +283,9 @@ To run several tests inside one directory:
- `bin/rspec spec/requests/api/` for the RSpec tests if you want to test API only
### Run RSpec tests which failed in Merge Request pipeline on your machine
### Run RSpec tests which failed in merge request pipeline on your machine
If your Merge Request pipeline failed with RSpec test failures,
If your merge request pipeline failed with RSpec test failures,
you can run all the failed tests on your machine with the following Rake task:
```shell
@ -294,7 +294,7 @@ bin/rake spec:merge_request_rspec_failure
There are a few caveats for this Rake task:
- You need to be on the same branch on your machine as the source branch of the Merge Request.
- You need to be on the same branch on your machine as the source branch of the merge request.
- The pipeline must have been completed.
- You may need to wait for the test report to be parsed and retry again.

View File

@ -330,7 +330,7 @@ out using the instructions below.
You can work on the frontend and backend at the same time, because updates over WebSockets
are difficult to simulate without the necessary backend code in place.
However, it is safer to send changes in separate Merge Requests and deploy the backend changes first.
However, it is safer to send changes in separate merge requests and deploy the backend changes first.
This ensures that when the frontend starts subscribing to events, the backend is already prepared
to service them.

View File

@ -15,13 +15,13 @@ expressions and runs various aggregations on the data.
For example:
- Duration between the Merge Request creation time and Merge Request merge time.
- Duration between the merge request creation time and merge request merge time.
- Duration between the Issue creation time and Issue close time.
This duration is exposed in various ways:
- Aggregation: median, average
- Listing: list the duration for individual Merge Request and Issue records
- Listing: list the duration for individual merge request and issue records
Apart from the durations, we expose the record count within a stage.

View File

@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated
description: "Add, commit, and push a file to your Git repository using the command line."
---
# Add files and make changes by using Git
# Add files to your branch
You can use the Git command line to add files, make changes to existing files, and stash changes you don't need yet.

View File

@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated
description: "Introduction to Git rebase and force push, methods to resolve merge conflicts through the command line."
---
# Git rebase and force push
# Rebase to address merge conflicts
In Git, a rebase updates your branch with the contents of another branch.
A rebase confirms that changes in your branch don't conflict with

View File

@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
description: "How to install Git on your local machine."
---
# Install and configure Git
# Install Git
To begin contributing to GitLab projects, you must download and install the Git client on your computer.

View File

@ -20,9 +20,10 @@ platform for software development. GitLab adds many powerful
- [Tutorial: Create your first commit](../../tutorials/make_first_git_commit/index.md)
- [Clone a repository to your local machine](clone.md)
- [Create a branch for your changes](branch.md)
- [Add files and make changes](../../gitlab-basics/add-file.md)
- [Add files to your branch](../../gitlab-basics/add-file.md)
- [Stash changes for later](stash.md)
- [Undo changes](undo.md)
- [Tutorial: Update Git commit messages](../../tutorials/update_commit_messages/index.md)
- [Rebase to address merge conflicts](git_rebase.md)
- [Common Git commands](../../gitlab-basics/start-using-git.md)
- [Rebase and force-push](git_rebase.md)
- [Troubleshooting](troubleshooting_git.md)

View File

@ -4,7 +4,7 @@ group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Undo changes by using Git
# Undo changes
Git provides options for undoing changes. The method for undoing a change depends on whether the change is unstaged, staged, committed, or pushed.

File diff suppressed because it is too large Load Diff

View File

@ -55,7 +55,7 @@ In the GitLab UI, GitLab Duo Chat knows about these areas:
|---------------|------------------------------------------------------------------------------------------------------------------|
| Epics | From the epic, ask about `this epic`, `this`, or the URL. From any UI area, ask about the URL. |
| Issues | From the issue, ask about `this issue`, `this`, or the URL. From any UI area, ask about the URL. |
| Merge Request | From the merge request, ask about `this merge request`, `this`, or the URL. From any UI area, ask about the URL. |
| Merge request | From the merge request, ask about `this merge request`, `this`, or the URL. From any UI area, ask about the URL. |
| Code files | From the single file, ask about `this code` or `this file`. From any UI area, ask about the URL. |
In the IDEs, GitLab Duo Chat knows about these areas:

View File

@ -59,7 +59,7 @@ To set up a protected branch flow:
1. To ensure that the code is reviewed and checked by the right people in your team, use:
- [Code Owners](../codeowners/index.md)
- [Merge Request approval rules](approvals/rules.md)
- [Merge request approval rules](approvals/rules.md)
Also available in the Ultimate tier:

View File

@ -63181,6 +63181,9 @@ msgstr ""
msgid "is invalid because there is upstream lock"
msgstr ""
msgid "is invalid."
msgstr ""
msgid "is linked to an account pending deletion."
msgstr ""

View File

@ -131,6 +131,7 @@
"cache-loader": "^4.1.0",
"canvas-confetti": "^1.4.0",
"clipboard": "^2.0.8",
"colord": "^2.9.3",
"compression-webpack-plugin": "^5.0.2",
"copy-webpack-plugin": "^6.4.1",
"core-js": "^3.37.1",

View File

@ -12,6 +12,7 @@ import tailwindcss from 'tailwindcss/lib/plugin.js';
import tailwindConfig from '../../../config/tailwind.config.js';
import IS_EE from '../../../config/helpers/is_ee_env.js';
import IS_JH from '../../../config/helpers/is_jh_env.js';
import { postCssColorToHex } from './postcss_color_to_hex.js';
/* eslint-enable import/extensions */
// Note, in node > 21.2 we could replace the below with import.meta.dirname
@ -193,6 +194,7 @@ function createPostCSSProcessors() {
files: [path.join(ROOT_PATH, 'node_modules/@gitlab/ui/src/tokens/build/css/tokens.css')],
}),
postcssCustomProperties({ preserve: false }),
postCssColorToHex(),
autoprefixer(),
]),
default: postcss([autoprefixer()]),

View File

@ -0,0 +1,39 @@
const { colord, extend } = require('colord');
const namesPlugin = require('colord/plugins/names');
const minifyPlugin = require('colord/plugins/minify');
extend([namesPlugin, minifyPlugin]);
const hexify = (value) => {
const newValue = colord(value);
if (newValue.isValid()) {
// value has an alpha: return as rgba
if (newValue.alpha() < 1) {
return newValue.minify({
rgb: true,
transparent: true,
hsl: false,
hex: false,
});
}
// value has no alpha, return as hex
return newValue.toHex();
}
// Not a valid color, just return the input.
return value;
};
const postCssColorToHex = () => {
return {
postcssPlugin: 'postcss-color-to-hex',
Declaration(decl) {
// eslint-disable-next-line
decl.value = hexify(decl.value);
},
};
};
postCssColorToHex.postcss = true;
module.exports = {
postCssColorToHex,
};

View File

@ -1420,6 +1420,15 @@ RSpec.describe GroupsController, factory_default: :keep, feature_category: :code
expect { subject }.not_to change { group.reload.name }
end
end
context 'when default branch name is invalid' do
subject { put :update, params: { id: group.to_param, group: { default_branch_name: "***" } } }
it 'renders an error message' do
expect { subject }.not_to change { group.reload.name }
expect(flash[:alert]).to eq('Default branch name is invalid.')
end
end
end
describe 'DELETE #destroy' do

View File

@ -218,11 +218,9 @@ RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', feature_category: :tooling do
'.prettierignore',
'.projections.json.example',
'.rubocop_revert_ignores.txt',
'.ruby-version',
'.solargraph.yml.example',
'.solargraph.yml',
'.test_license_encryption_key.pub',
'.tool-versions',
'.vale.ini',
'.vscode/extensions.json',
'ee/lib/ee/gitlab/background_migration/.rubocop.yml',

File diff suppressed because it is too large Load Diff

View File

@ -1,309 +0,0 @@
import { render } from '~/lib/gfm';
describe('gfm', () => {
const markdownToAST = async (markdown, skipRendering = []) => {
let result;
await render({
markdown,
skipRendering,
renderer: (tree) => {
result = tree;
},
});
return result;
};
const expectInRoot = (result, ...nodes) => {
expect(result).toEqual(
expect.objectContaining({
children: expect.arrayContaining(nodes),
}),
);
};
describe('render', () => {
it('transforms raw HTML into individual nodes in the AST', async () => {
const result = await markdownToAST('<strong>This is bold text</strong>');
expectInRoot(
result,
expect.objectContaining({
children: expect.arrayContaining([
expect.objectContaining({
type: 'element',
tagName: 'strong',
}),
]),
}),
);
});
describe('with custom renderer', () => {
it('processes Commonmark and provides an ast to the renderer function', async () => {
const result = await markdownToAST('This is text');
expect(result.type).toBe('root');
});
it('returns the result of executing the renderer function', async () => {
const rendered = { value: 'rendered tree' };
const result = await render({
markdown: '<strong>This is bold text</strong>',
renderer: () => {
return rendered;
},
});
expect(result).toEqual(rendered);
});
});
describe('footnote references and footnote definitions', () => {
describe('when skipping the rendering of footnote reference and definition nodes', () => {
it('transforms footnotes into footnotedefinition and footnotereference tags', async () => {
const result = await markdownToAST(
`footnote reference [^footnote]
[^footnote]: Footnote definition`,
['footnoteReference', 'footnoteDefinition'],
);
expectInRoot(
result,
expect.objectContaining({
children: expect.arrayContaining([
expect.objectContaining({
type: 'element',
tagName: 'footnotereference',
properties: {
identifier: 'footnote',
label: 'footnote',
},
}),
]),
}),
);
expectInRoot(
result,
expect.objectContaining({
tagName: 'footnotedefinition',
properties: {
identifier: 'footnote',
label: 'footnote',
},
}),
);
});
});
});
describe('code blocks', () => {
describe('when skipping the rendering of code blocks', () => {
it('transforms code nodes into codeblock html tags', async () => {
const result = await markdownToAST(
`
\`\`\`javascript
console.log('Hola');
\`\`\`\
`,
['code'],
);
expectInRoot(
result,
expect.objectContaining({
tagName: 'codeblock',
properties: {
language: 'javascript',
},
}),
);
});
});
});
describe('reference definitions', () => {
describe('when skipping the rendering of reference definitions', () => {
it('transforms code nodes into codeblock html tags', async () => {
const result = await markdownToAST(
`
[gitlab][gitlab]
[gitlab]: https://gitlab.com "GitLab"
`,
['definition'],
);
expectInRoot(
result,
expect.objectContaining({
type: 'element',
tagName: 'referencedefinition',
properties: {
identifier: 'gitlab',
title: 'GitLab',
url: 'https://gitlab.com',
},
children: [
{
type: 'text',
value: '[gitlab]: https://gitlab.com "GitLab"',
},
],
}),
);
});
});
});
describe('link and image references', () => {
describe('when skipping the rendering of link and image references', () => {
it('transforms linkReference and imageReference nodes into html tags', async () => {
const result = await markdownToAST(
`
[gitlab][gitlab] and ![GitLab Logo][gitlab-logo]
[gitlab]: https://gitlab.com "GitLab"
[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"
`,
['linkReference', 'imageReference'],
);
expectInRoot(
result,
expect.objectContaining({
tagName: 'p',
children: expect.arrayContaining([
expect.objectContaining({
type: 'element',
tagName: 'a',
properties: expect.objectContaining({
href: 'https://gitlab.com',
isReference: 'true',
identifier: 'gitlab',
title: 'GitLab',
}),
}),
expect.objectContaining({
type: 'element',
tagName: 'img',
properties: expect.objectContaining({
src: 'https://gitlab.com/gitlab-logo.png',
isReference: 'true',
identifier: 'gitlab-logo',
title: 'GitLab Logo',
alt: 'GitLab Logo',
}),
}),
]),
}),
);
});
it('normalizes the urls extracted from the reference definitions', async () => {
const result = await markdownToAST(
`
[gitlab][gitlab] and ![GitLab Logo][gitlab]
[gitlab]: /url\\bar*baz
`,
['linkReference', 'imageReference'],
);
expectInRoot(
result,
expect.objectContaining({
tagName: 'p',
children: expect.arrayContaining([
expect.objectContaining({
type: 'element',
tagName: 'a',
properties: expect.objectContaining({
href: '/url%5Cbar*baz',
}),
}),
expect.objectContaining({
type: 'element',
tagName: 'img',
properties: expect.objectContaining({
src: '/url%5Cbar*baz',
}),
}),
]),
}),
);
});
});
});
describe('frontmatter', () => {
describe('when skipping the rendering of frontmatter types', () => {
it.each`
type | input
${'yaml'} | ${'---\ntitle: page\n---'}
${'toml'} | ${'+++\ntitle: page\n+++'}
${'json'} | ${';;;\ntitle: page\n;;;'}
`('transforms $type nodes into frontmatter html tags', async ({ input, type }) => {
const result = await markdownToAST(input, [type]);
expectInRoot(
result,
expect.objectContaining({
type: 'element',
tagName: 'frontmatter',
properties: {
language: type,
},
children: [
{
type: 'text',
value: 'title: page',
},
],
}),
);
});
});
});
describe('table of contents', () => {
it.each`
markdown
${'[[_TOC_]]'}
${' [[_TOC_]]'}
${'[[_TOC_]] '}
${'[TOC]'}
${' [TOC]'}
${'[TOC] '}
`('parses $markdown and produces a table of contents section', async ({ markdown }) => {
const result = await markdownToAST(markdown);
expectInRoot(
result,
expect.objectContaining({
type: 'element',
tagName: 'nav',
}),
);
});
});
describe('when skipping the rendering of table of contents', () => {
it('transforms table of contents nodes into html tableofcontents tags', async () => {
const result = await markdownToAST('[[_TOC_]]', ['tableOfContents']);
expectInRoot(
result,
expect.objectContaining({
type: 'element',
tagName: 'tableofcontents',
}),
);
});
});
});
});

View File

@ -0,0 +1,67 @@
import postcss from 'postcss';
import { postCssColorToHex } from '../../../../../scripts/frontend/lib/postcss_color_to_hex';
describe('postcssColorToHex', () => {
function convert(css) {
return postcss(postCssColorToHex()).process(css).toString();
}
it('converts non-transparent colors to hex', () => {
expect(
convert(`.foo {
--example: #fff;
color: rgba(255,0,0);
color: rgba(0 255 0);
color: #abcdef;
color: white;
color: hsl(90, 100%, 100%);
}`),
).toBe(`.foo {
--example: #ffffff;
color: #ff0000;
color: #00ff00;
color: #abcdef;
color: #ffffff;
color: #ffffff;
}`);
});
it('returns `transparent` for alpha = 0', () => {
expect(
convert(`.foo {
color: transparent;
color: rgba(0,0,0,0);
}`),
).toBe(`.foo {
color: transparent;
color: transparent;
}`);
});
it('converts colors with alpha to rgba', () => {
expect(
convert(`.foo {
--example: #fff7;
color: rgba(255,0,0,0.05);
color: #abcdef7f;
color: hsl(90 100% 100% / 60%);
}`),
).toBe(`.foo {
--example: rgba(255,255,255,.47);
color: rgba(255,0,0,.05);
color: rgba(171,205,239,.5);
color: rgba(255,255,255,.6);
}`);
});
it('ignores non-color values', () => {
const input = `.foo {
--example: 1rem;
margin: 5px;
padding: 5%;
z-index: clamp(1,2,3);
}`;
expect(convert(input)).toBe(input);
});
});

View File

@ -7,7 +7,7 @@ export const MOCK_QUERY = {
confidential: null,
group_id: 1,
language: ['C', 'JavaScript'],
labels: ['60', '37'],
labelName: ['60', '37'],
search: '*',
};

View File

@ -34,7 +34,7 @@ import {
TRACKING_ACTION_SHOW,
} from '~/search/sidebar/components/label_filter/tracking';
import { labelFilterData } from '~/search/sidebar/components/label_filter/data';
import { LABEL_FILTER_PARAM } from '~/search/sidebar/components/label_filter/data';
import {
RECEIVE_AGGREGATIONS_SUCCESS,
@ -350,7 +350,7 @@ describe('GlobalSearchSidebarLabelFilter', () => {
it('trigger event', () => {
expect(actionSpies.setQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ key: labelFilterData?.filterParam, value: 6 }),
expect.objectContaining({ key: LABEL_FILTER_PARAM, value: 6 }),
);
});

View File

@ -207,7 +207,7 @@ describe('Global Search Store Actions', () => {
it('calls visitUrl and setParams with the state.query', async () => {
await testAction(actions.applyQuery, null, state, [], []);
expect(urlUtils.visitUrl).toHaveBeenCalledWith(
'https://test/?scope=issues&state=all&group_id=1&language%5B%5D=C&language%5B%5D=JavaScript&labels%5B%5D=60&labels%5B%5D=37&search=*',
'https://test/?scope=issues&state=all&group_id=1&language%5B%5D=C&language%5B%5D=JavaScript&labelName%5B%5D=60&labelName%5B%5D=37&search=*',
);
});
});
@ -408,7 +408,7 @@ describe('Global Search Store Actions', () => {
const expectedResult = [
{
payload: {
key: 'labels',
key: 'labelName',
value: ['37'],
},
type: 'SET_QUERY',

View File

@ -143,7 +143,7 @@ describe('Global Search Store Getters', () => {
describe('unselectedLabels', () => {
it('returns all labels that are not selected', () => {
state.query.labels = ['60'];
state.query.labelName = ['60'];
expect(getters.unselectedLabels(state)).toStrictEqual([MOCK_LABEL_SEARCH_RESULT]);
});
});
@ -151,9 +151,9 @@ describe('Global Search Store Getters', () => {
describe('unappliedNewLabels', () => {
it('returns all labels that are selected but not applied', () => {
// Applied labels
state.urlQuery.labels = ['37', '60'];
state.urlQuery.labelName = ['37', '60'];
// Applied and selected labels
state.query.labels = ['37', '6', '73', '60'];
state.query.labelName = ['37', '6', '73', '60'];
// Selected but unapplied labels
// expect(getters.unappliedNewLabels(state)).toStrictEqual(MOCK_FILTERED_UNSELECTED_LABELS);
expect(getters.unappliedNewLabels(state).map(({ key }) => key)).toStrictEqual(['6', '73']);

View File

@ -43,6 +43,28 @@ RSpec.describe NamespaceSettings::AssignAttributesService, feature_category: :gr
.to change { group.namespace_settings.default_branch_name }
.from(nil).to(example_branch_name)
end
context 'when default branch name is invalid' do
let(:settings) { { default_branch_name: '****' } }
it "does not update the default branch" do
expect { service.execute }.not_to change { group.namespace_settings.default_branch_name }
expect(group.namespace_settings.errors[:default_branch_name]).to include('is invalid.')
end
end
context 'when default branch name is changed to empty' do
before do
group.namespace_settings.update!(default_branch_name: 'main')
end
let(:settings) { { default_branch_name: '' } }
it 'updates the default branch' do
expect { service.execute }.to change { group.namespace_settings.default_branch_name }.from('main').to('')
end
end
end
context 'when default_branch_protection is updated' do

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB