Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1fec509ff1
commit
8de0010c4e
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const tableOfContents = (h, node) => h(node.position, 'nav');
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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 || []
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"] -%>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()]),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -7,7 +7,7 @@ export const MOCK_QUERY = {
|
|||
confidential: null,
|
||||
group_id: 1,
|
||||
language: ['C', 'JavaScript'],
|
||||
labels: ['60', '37'],
|
||||
labelName: ['60', '37'],
|
||||
search: '*',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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 |
Loading…
Reference in New Issue