Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6723a4288d
commit
d79bf171e4
2
Gemfile
2
Gemfile
|
@ -2,7 +2,7 @@
|
|||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'rails', '~> 6.1.4.7'
|
||||
gem 'rails', '~> 6.1.6.1'
|
||||
|
||||
gem 'bootsnap', '~> 1.12.0', require: false
|
||||
|
||||
|
|
112
Gemfile.lock
112
Gemfile.lock
|
@ -45,63 +45,63 @@ GEM
|
|||
RedCloth (4.3.2)
|
||||
acme-client (2.0.9)
|
||||
faraday (>= 0.17, < 2.0.0)
|
||||
actioncable (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
actioncable (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
actionmailbox (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
activejob (= 6.1.6.1)
|
||||
activerecord (= 6.1.6.1)
|
||||
activestorage (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
actionmailer (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
actionview (= 6.1.6.1)
|
||||
activejob (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
actionpack (6.1.6.1)
|
||||
actionview (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
actiontext (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
activerecord (= 6.1.6.1)
|
||||
activestorage (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
actionview (6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
activejob (6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
activerecord (6.1.4.7)
|
||||
activemodel (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
activemodel (6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
activerecord (6.1.6.1)
|
||||
activemodel (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
activerecord-explain-analyze (0.1.0)
|
||||
activerecord (>= 4)
|
||||
pg
|
||||
activestorage (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
marcel (~> 1.0.0)
|
||||
activestorage (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
activejob (= 6.1.6.1)
|
||||
activerecord (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.4.7)
|
||||
activesupport (6.1.6.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
|
@ -1031,20 +1031,20 @@ GEM
|
|||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.6.0)
|
||||
rails (6.1.4.7)
|
||||
actioncable (= 6.1.4.7)
|
||||
actionmailbox (= 6.1.4.7)
|
||||
actionmailer (= 6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
actiontext (= 6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activemodel (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
rails (6.1.6.1)
|
||||
actioncable (= 6.1.6.1)
|
||||
actionmailbox (= 6.1.6.1)
|
||||
actionmailer (= 6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
actiontext (= 6.1.6.1)
|
||||
actionview (= 6.1.6.1)
|
||||
activejob (= 6.1.6.1)
|
||||
activemodel (= 6.1.6.1)
|
||||
activerecord (= 6.1.6.1)
|
||||
activestorage (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.4.7)
|
||||
railties (= 6.1.6.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
|
@ -1058,11 +1058,11 @@ GEM
|
|||
rails-i18n (7.0.3)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
railties (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
railties (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
method_source
|
||||
rake (>= 0.13)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
|
@ -1678,7 +1678,7 @@ DEPENDENCIES
|
|||
rack-oauth2 (~> 1.21.2)
|
||||
rack-proxy (~> 0.7.2)
|
||||
rack-timeout (~> 0.6.0)
|
||||
rails (~> 6.1.4.7)
|
||||
rails (~> 6.1.6.1)
|
||||
rails-controller-testing
|
||||
rails-i18n (~> 7.0)
|
||||
rainbow (~> 3.0)
|
||||
|
|
|
@ -2,16 +2,35 @@
|
|||
export default () => ({
|
||||
name: 'strike',
|
||||
schema: {
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'del',
|
||||
attrs: {
|
||||
strike: {
|
||||
default: false,
|
||||
},
|
||||
inapplicable: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
parseDOM: [
|
||||
{ tag: 'li.inapplicable > s', attrs: { inapplicable: true } },
|
||||
{ tag: 'li.inapplicable > p:first-of-type > s', attrs: { inapplicable: true } },
|
||||
{ tag: 's', attrs: { strike: true } },
|
||||
{ tag: 'del' },
|
||||
],
|
||||
toDOM: () => ['s', 0],
|
||||
},
|
||||
toMarkdown: {
|
||||
open: '~~',
|
||||
close: '~~',
|
||||
open(_, mark) {
|
||||
if (mark.attrs.strike) {
|
||||
return '<s>';
|
||||
}
|
||||
return mark.attrs.inapplicable ? '' : '~~';
|
||||
},
|
||||
close(_, mark) {
|
||||
if (mark.attrs.strike) {
|
||||
return '</s>';
|
||||
}
|
||||
return mark.attrs.inapplicable ? '' : '~~';
|
||||
},
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
|
|
|
@ -5,8 +5,8 @@ export default () => ({
|
|||
name: 'task_list_item',
|
||||
schema: {
|
||||
attrs: {
|
||||
done: {
|
||||
default: false,
|
||||
state: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
defining: true,
|
||||
|
@ -18,21 +18,53 @@ export default () => ({
|
|||
tag: 'li.task-list-item',
|
||||
getAttrs: (el) => {
|
||||
const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox');
|
||||
return { done: checkbox && checkbox.checked };
|
||||
if (checkbox?.matches('[data-inapplicable]')) {
|
||||
return { state: 'inapplicable' };
|
||||
} else if (checkbox?.checked) {
|
||||
return { state: 'done' };
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
},
|
||||
],
|
||||
toDOM(node) {
|
||||
return [
|
||||
'li',
|
||||
{ class: 'task-list-item' },
|
||||
['input', { type: 'checkbox', class: 'task-list-item-checkbox', checked: node.attrs.done }],
|
||||
{
|
||||
class: () => {
|
||||
if (node.attrs.state === 'inapplicable') {
|
||||
return 'task-list-item inapplicable';
|
||||
}
|
||||
|
||||
return 'task-list-item';
|
||||
},
|
||||
},
|
||||
[
|
||||
'input',
|
||||
{
|
||||
type: 'checkbox',
|
||||
class: 'task-list-item-checkbox',
|
||||
checked: node.attrs.state === 'done',
|
||||
'data-inapplicable': node.attrs.state === 'inapplicable',
|
||||
},
|
||||
],
|
||||
['div', { class: 'todo-content' }, 0],
|
||||
];
|
||||
},
|
||||
},
|
||||
toMarkdown(state, node) {
|
||||
state.write(`[${node.attrs.done ? 'x' : ' '}] `);
|
||||
switch (node.attrs.state) {
|
||||
case 'done':
|
||||
state.write('[x] ');
|
||||
break;
|
||||
case 'inapplicable':
|
||||
state.write('[~] ');
|
||||
break;
|
||||
default:
|
||||
state.write('[ ] ');
|
||||
break;
|
||||
}
|
||||
state.renderContent(node);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -28,7 +28,6 @@ function getErrorMessage(res) {
|
|||
export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
|
||||
const divHover = '<div class="div-dropzone-hover"></div>';
|
||||
const iconPaperclip = spriteIcon('paperclip', 'div-dropzone-icon s24');
|
||||
const $attachButton = form.find('.button-attach-file');
|
||||
const $attachingFileMessage = form.find('.attaching-file-message');
|
||||
const $cancelButton = form.find('.button-cancel-uploading-files');
|
||||
const $retryLink = form.find('.retry-uploading-link');
|
||||
|
@ -89,8 +88,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
|
|||
const shouldPad = processingFileCount >= 1;
|
||||
|
||||
pasteText(response.link.markdown, shouldPad);
|
||||
// Show 'Attach a file' link only when all files have been uploaded.
|
||||
if (!processingFileCount) $attachButton.removeClass('hide');
|
||||
addFileToForm(response.link.url);
|
||||
},
|
||||
error: (file, errorMessage = __('Attaching the file failed.'), xhr) => {
|
||||
|
@ -104,7 +101,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
|
|||
|
||||
$uploadingErrorContainer.removeClass('hide');
|
||||
$uploadingErrorMessage.html(message);
|
||||
$attachButton.addClass('hide');
|
||||
$cancelButton.addClass('hide');
|
||||
},
|
||||
totaluploadprogress(totalUploadProgress) {
|
||||
|
@ -115,13 +111,11 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
|
|||
// DOM elements already exist.
|
||||
// Instead of dynamically generating them,
|
||||
// we just either hide or show them.
|
||||
$attachButton.addClass('hide');
|
||||
$uploadingErrorContainer.addClass('hide');
|
||||
$uploadingProgressContainer.removeClass('hide');
|
||||
$cancelButton.removeClass('hide');
|
||||
},
|
||||
removedfile: () => {
|
||||
$attachButton.removeClass('hide');
|
||||
$cancelButton.addClass('hide');
|
||||
$uploadingProgressContainer.addClass('hide');
|
||||
$uploadingErrorContainer.addClass('hide');
|
||||
|
@ -282,11 +276,18 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
|
|||
messageContainer.text(`${attachingMessage} -`);
|
||||
};
|
||||
|
||||
form.find('.markdown-selector').click(function onMarkdownClick(e) {
|
||||
function handleAttachFile(e) {
|
||||
e.preventDefault();
|
||||
$(this).closest('.gfm-form').find('.div-dropzone').click();
|
||||
formTextarea.focus();
|
||||
});
|
||||
}
|
||||
|
||||
form.find('.markdown-selector').click(handleAttachFile);
|
||||
|
||||
const $attachFileButton = form.find('.js-attach-file-button');
|
||||
if ($attachFileButton.length) {
|
||||
$attachFileButton.get(0).addEventListener('click', handleAttachFile);
|
||||
}
|
||||
|
||||
return $formDropzone.get(0) ? Dropzone.forElement($formDropzone.get(0)) : null;
|
||||
}
|
||||
|
|
|
@ -135,6 +135,7 @@ export default {
|
|||
>
|
||||
<gl-button
|
||||
v-if="shouldShowExternalUrlButton"
|
||||
v-gl-tooltip.hover
|
||||
data-testid="metrics-button"
|
||||
:href="metricsPath"
|
||||
:title="$options.i18n.metricsButtonTitle"
|
||||
|
|
|
@ -134,6 +134,7 @@
|
|||
"WorkItemWidgetAssignees",
|
||||
"WorkItemWidgetDescription",
|
||||
"WorkItemWidgetHierarchy",
|
||||
"WorkItemWidgetStartAndDueDate",
|
||||
"WorkItemWidgetWeight"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -315,7 +315,7 @@ export default {
|
|||
}
|
||||
|
||||
this.taskButtons = [];
|
||||
const taskListFields = this.$el.querySelectorAll('.task-list-item');
|
||||
const taskListFields = this.$el.querySelectorAll('.task-list-item:not(.inapplicable)');
|
||||
|
||||
taskListFields.forEach((item, index) => {
|
||||
const taskLink = item.querySelector('.gfm-issue');
|
||||
|
|
|
@ -32,6 +32,22 @@ const removeUnsafeHref = (node, attr) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends 'noopener' & 'noreferrer' to rel
|
||||
* attr values to prevent reverse tabnabbing.
|
||||
*
|
||||
* @param {String} rel
|
||||
* @returns {String}
|
||||
*/
|
||||
const appendSecureRelValue = (rel) => {
|
||||
const attributes = new Set(rel ? rel.toLowerCase().split(' ') : []);
|
||||
|
||||
attributes.add('noopener');
|
||||
attributes.add('noreferrer');
|
||||
|
||||
return Array.from(attributes).join(' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Sanitize icons' <use> tag attributes, to safely include
|
||||
* svgs such as in:
|
||||
|
@ -57,4 +73,23 @@ addHook('afterSanitizeAttributes', (node) => {
|
|||
}
|
||||
});
|
||||
|
||||
const TEMPORARY_ATTRIBUTE = 'data-temp-href-target';
|
||||
|
||||
addHook('beforeSanitizeAttributes', (node) => {
|
||||
if (node.tagName === 'A' && node.hasAttribute('target')) {
|
||||
node.setAttribute(TEMPORARY_ATTRIBUTE, node.getAttribute('target'));
|
||||
}
|
||||
});
|
||||
|
||||
addHook('afterSanitizeAttributes', (node) => {
|
||||
if (node.tagName === 'A' && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
|
||||
node.setAttribute('target', node.getAttribute(TEMPORARY_ATTRIBUTE));
|
||||
node.removeAttribute(TEMPORARY_ATTRIBUTE);
|
||||
if (node.getAttribute('target') === '_blank') {
|
||||
const rel = node.getAttribute('rel');
|
||||
node.setAttribute('rel', appendSecureRelValue(rel));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const sanitize = (val, config) => dompurifySanitize(val, { ...defaultConfig, ...config });
|
||||
|
|
|
@ -9,7 +9,7 @@ const LINK_TAG_PATTERN = '[{text}](url)';
|
|||
// a bullet point character (*+-) and an optional checkbox ([ ] [x])
|
||||
// OR a number with a . after it and an optional checkbox ([ ] [x])
|
||||
// followed by one or more whitespace characters
|
||||
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([xX\s])\])?\s)(?<content>.)?/;
|
||||
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([xX~\s])\])?\s)(?<content>.)?/;
|
||||
|
||||
// detect a horizontal rule that might be mistaken for a list item (not full pattern for an <hr>)
|
||||
const HR_PATTERN = /^((\s{0,3}-+\s*-+\s*-+\s*[\s-]*)|(\s{0,3}\*+\s*\*+\s*\*+\s*[\s*]*))$/;
|
||||
|
@ -399,7 +399,7 @@ function handleContinueList(e, textArea) {
|
|||
itemToInsert = `${indent}${leader}`;
|
||||
}
|
||||
|
||||
itemToInsert = itemToInsert.replace(/\[x\]/i, '[ ]');
|
||||
itemToInsert = itemToInsert.replace(/\[[x~]\]/i, '[ ]');
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
|
|
|
@ -62,13 +62,21 @@ export default class TaskList {
|
|||
.prop('disabled', true);
|
||||
}
|
||||
|
||||
updateInapplicableTaskListItems(e) {
|
||||
this.getTaskListTarget(e)
|
||||
.find('.task-list-item-checkbox[data-inapplicable]')
|
||||
.prop('disabled', true);
|
||||
}
|
||||
|
||||
disableTaskListItems(e) {
|
||||
this.getTaskListTarget(e).taskList('disable');
|
||||
this.updateInapplicableTaskListItems();
|
||||
}
|
||||
|
||||
enableTaskListItems(e) {
|
||||
this.getTaskListTarget(e).taskList('enable');
|
||||
this.disableNonMarkdownTaskListItems(e);
|
||||
this.updateInapplicableTaskListItems(e);
|
||||
}
|
||||
|
||||
enable() {
|
||||
|
|
|
@ -156,6 +156,14 @@ export default {
|
|||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
handleAttachFile(e) {
|
||||
e.preventDefault();
|
||||
const $gfmForm = $(this.$el).closest('.gfm-form');
|
||||
const $gfmTextarea = $gfmForm.find('.js-gfm-input');
|
||||
|
||||
$gfmForm.find('.div-dropzone').click();
|
||||
$gfmTextarea.focus();
|
||||
},
|
||||
},
|
||||
shortcuts: {
|
||||
bold: keysFor(BOLD_TEXT),
|
||||
|
@ -324,6 +332,14 @@ export default {
|
|||
:button-title="__('Add a table')"
|
||||
icon="table"
|
||||
/>
|
||||
<toolbar-button
|
||||
v-if="!restrictedToolBarItems.includes('attach-file')"
|
||||
data-testid="button-attach-file"
|
||||
:prepend="true"
|
||||
:button-title="__('Attach a file or image')"
|
||||
icon="paperclip"
|
||||
@click="handleAttachFile"
|
||||
/>
|
||||
<toolbar-button
|
||||
v-if="!restrictedToolBarItems.includes('full-screen')"
|
||||
class="js-zen-enter"
|
||||
|
|
|
@ -74,7 +74,7 @@ export default {
|
|||
</div>
|
||||
<span v-if="canAttachFile" class="uploading-container">
|
||||
<span class="uploading-progress-container hide">
|
||||
<gl-icon name="media" />
|
||||
<gl-icon name="paperclip" />
|
||||
<span class="attaching-file-message"></span>
|
||||
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
|
||||
<span class="uploading-progress">0%</span>
|
||||
|
@ -82,7 +82,7 @@ export default {
|
|||
</span>
|
||||
<span class="uploading-error-container hide">
|
||||
<span class="uploading-error-icon">
|
||||
<gl-icon name="media" />
|
||||
<gl-icon name="paperclip" />
|
||||
</span>
|
||||
<span class="uploading-error-message"></span>
|
||||
|
||||
|
@ -113,14 +113,6 @@ export default {
|
|||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
<gl-button
|
||||
icon="media"
|
||||
variant="link"
|
||||
category="primary"
|
||||
class="markdown-selector button-attach-file gl-vertical-align-text-bottom"
|
||||
>
|
||||
{{ __('Attach a file') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
variant="link"
|
||||
category="primary"
|
||||
|
|
|
@ -88,6 +88,6 @@ export default {
|
|||
category="tertiary"
|
||||
class="js-md"
|
||||
data-container="body"
|
||||
@click="() => $emit('click')"
|
||||
@click="$emit('click', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -435,6 +435,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
li.inapplicable {
|
||||
// for a single line list item, no paragraph (tight list)
|
||||
> s {
|
||||
color: $gl-text-color-disabled;
|
||||
}
|
||||
|
||||
// additional blocks, other than paragraphs
|
||||
> div {
|
||||
text-decoration: line-through;
|
||||
color: $gl-text-color-disabled;
|
||||
}
|
||||
|
||||
// because of the embedded checkbox, putting line-through on the entire
|
||||
// paragraph causes the space between the checkbox and the text to have the
|
||||
// line-through. Targeting just the `s` fixes this
|
||||
> p:first-of-type > s {
|
||||
color: $gl-text-color-disabled;
|
||||
}
|
||||
|
||||
> p:not(:first-of-type) {
|
||||
text-decoration: line-through;
|
||||
color: $gl-text-color-disabled;
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
a.with-attachment-icon,
|
||||
a[href*='/uploads/'],
|
||||
a[href*='storage.googleapis.com/google-code-attachments/'] {
|
||||
|
|
|
@ -13,7 +13,8 @@ module Types
|
|||
ORPHAN_TYPES = [
|
||||
::Types::WorkItems::Widgets::DescriptionType,
|
||||
::Types::WorkItems::Widgets::HierarchyType,
|
||||
::Types::WorkItems::Widgets::AssigneesType
|
||||
::Types::WorkItems::Widgets::AssigneesType,
|
||||
::Types::WorkItems::Widgets::StartAndDueDateType
|
||||
].freeze
|
||||
|
||||
def self.ce_orphan_types
|
||||
|
@ -28,6 +29,8 @@ module Types
|
|||
::Types::WorkItems::Widgets::HierarchyType
|
||||
when ::WorkItems::Widgets::Assignees
|
||||
::Types::WorkItems::Widgets::AssigneesType
|
||||
when ::WorkItems::Widgets::StartAndDueDate
|
||||
::Types::WorkItems::Widgets::StartAndDueDateType
|
||||
else
|
||||
raise "Unknown GraphQL type for widget #{object}"
|
||||
end
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module WorkItems
|
||||
module Widgets
|
||||
# Disabling widget level authorization as it might be too granular
|
||||
# and we already authorize the parent work item
|
||||
# rubocop:disable Graphql/AuthorizeTypes
|
||||
class StartAndDueDateType < BaseObject
|
||||
graphql_name 'WorkItemWidgetStartAndDueDate'
|
||||
description 'Represents a start and due date widget'
|
||||
|
||||
implements Types::WorkItems::WidgetInterface
|
||||
|
||||
field :due_date, Types::DateType, null: true,
|
||||
description: 'Due date of the work item.'
|
||||
field :start_date, Types::DateType, null: true,
|
||||
description: 'Start date of the work item.'
|
||||
end
|
||||
# rubocop:enable Graphql/AuthorizeTypes
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,7 +7,9 @@ module GitlabScriptTagHelper
|
|||
# The helper also makes sure the `nonce` attribute is included in every script when the content security
|
||||
# policy is enabled.
|
||||
def javascript_include_tag(*sources)
|
||||
super(*sources, defer: true, nonce: true)
|
||||
options = { defer: true }.merge(sources.extract_options!)
|
||||
options[:nonce] = true
|
||||
super(*sources, **options)
|
||||
end
|
||||
|
||||
# The helper makes sure the `nonce` attribute is included in every script when the content security
|
||||
|
|
|
@ -147,7 +147,7 @@ module IssuablesHelper
|
|||
end
|
||||
|
||||
def issuable_meta_author_status(author)
|
||||
return "" unless show_status_emoji?(author&.status) && status = user_status(author)
|
||||
return "" unless author&.status&.customized? && status = user_status(author)
|
||||
|
||||
"#{status}".html_safe
|
||||
end
|
||||
|
|
|
@ -266,9 +266,10 @@ module MarkupHelper
|
|||
|
||||
def markdown_toolbar_button(options = {})
|
||||
data = options[:data].merge({ container: 'body' })
|
||||
css_classes = %w[gl-button btn btn-default-tertiary btn-icon js-md has-tooltip] << options[:css_class].to_s
|
||||
content_tag :button,
|
||||
type: 'button',
|
||||
class: 'gl-button btn btn-default-tertiary btn-icon js-md has-tooltip',
|
||||
class: css_classes.join(' '),
|
||||
data: data,
|
||||
title: options[:title],
|
||||
aria: { label: options[:title] } do
|
||||
|
|
|
@ -31,10 +31,6 @@ module ProfilesHelper
|
|||
Types::AvailabilityEnum.enum
|
||||
end
|
||||
|
||||
def user_status_set_to_busy?(status)
|
||||
status&.availability == availability_values[:busy]
|
||||
end
|
||||
|
||||
def middle_dot_divider_classes(stacking, breakpoint)
|
||||
['gl-mb-3'].tap do |classes|
|
||||
if stacking
|
||||
|
|
|
@ -67,12 +67,6 @@ module UsersHelper
|
|||
"access:#{max_project_member_access(project)}"
|
||||
end
|
||||
|
||||
def show_status_emoji?(status)
|
||||
return false unless status
|
||||
|
||||
status.message.present? || status.emoji != UserStatus::DEFAULT_EMOJI
|
||||
end
|
||||
|
||||
def user_status(user)
|
||||
return unless user
|
||||
|
||||
|
|
|
@ -32,6 +32,10 @@ class UserStatus < ApplicationRecord
|
|||
def clear_status_after=(value)
|
||||
self.clear_status_at = CLEAR_STATUS_QUICK_OPTIONS[value]&.from_now
|
||||
end
|
||||
|
||||
def customized?
|
||||
message.present? || emoji != UserStatus::DEFAULT_EMOJI
|
||||
end
|
||||
end
|
||||
|
||||
UserStatus.prepend_mod_with('UserStatus')
|
||||
|
|
|
@ -21,11 +21,11 @@ module WorkItems
|
|||
}.freeze
|
||||
|
||||
WIDGETS_FOR_TYPE = {
|
||||
issue: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy],
|
||||
issue: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate],
|
||||
incident: [Widgets::Description, Widgets::Hierarchy],
|
||||
test_case: [Widgets::Description],
|
||||
requirement: [Widgets::Description],
|
||||
task: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy]
|
||||
task: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate]
|
||||
}.freeze
|
||||
|
||||
cache_markdown_field :description, pipeline: :single_line
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WorkItems
|
||||
module Widgets
|
||||
class StartAndDueDate < Base
|
||||
delegate :start_date, :due_date, to: :work_item
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,7 +13,7 @@ module UserStatusTooltip
|
|||
end
|
||||
|
||||
expose :show_status do |user|
|
||||
status_loaded? && show_status_emoji?(user.status)
|
||||
status_loaded? && !!user.status&.customized?
|
||||
end
|
||||
|
||||
expose :availability, if: -> (*) { status_loaded? } do |user|
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
- if can?(current_user, :update_user_status, current_user)
|
||||
%li
|
||||
%button.gl-button.btn.btn-link.menu-item.js-set-status-modal-trigger{ type: 'button' }
|
||||
- if show_status_emoji?(current_user.status) || user_status_set_to_busy?(current_user.status)
|
||||
- if current_user.status&.busy? || current_user.status&.customized?
|
||||
= s_('SetStatusModal|Edit status')
|
||||
- else
|
||||
= s_('SetStatusModal|Set status')
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
.gl-font-weight-bold
|
||||
= current_user.name
|
||||
- if current_user&.status && user_status_set_to_busy?(current_user.status)
|
||||
- if current_user.status&.busy?
|
||||
%span.gl-font-weight-normal.gl-text-gray-500= s_("UserProfile|(Busy)")
|
||||
= current_user.to_reference
|
||||
- if current_user.status
|
||||
.user-status.d-flex.align-items-center.gl-mt-2.gl-mr-0.gl-font-sm.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } }
|
||||
- if show_status_emoji?(current_user.status)
|
||||
- if current_user.status.customized?
|
||||
.user-status-emoji.d-flex.align-items-center
|
||||
= emoji_icon current_user.status.emoji
|
||||
%span.user-status-message.str-truncated
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
|
||||
- availability = availability_values
|
||||
- custom_emoji = show_status_emoji?(@user.status)
|
||||
- custom_emoji = @user.status&.customized?
|
||||
|
||||
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
|
||||
.row.js-search-settings-section
|
||||
|
|
|
@ -27,6 +27,10 @@
|
|||
data: { "md-tag" => "<details><summary>Click to expand</summary>\n{text}\n</details>", "md-prepend" => true, "md-select" => "Click to expand" },
|
||||
title: _("Add a collapsible section") })
|
||||
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
|
||||
= markdown_toolbar_button({ icon: "paperclip",
|
||||
data: { "md-tag" => "", "md-prepend" => true, "testid" => "button-attach-file" },
|
||||
css_class: 'js-attach-file-button markdown-selector',
|
||||
title: _("Attach a file or image") })
|
||||
- if show_fullscreen_button
|
||||
%button.gl-button.btn.btn-default-tertiary.btn-icon.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
|
||||
= sprite_icon("maximize")
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
- if supports_file_upload
|
||||
%span.uploading-container
|
||||
%span.uploading-progress-container.hide
|
||||
= sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
|
||||
= sprite_icon('paperclip', css_class: 'gl-icon gl-vertical-align-text-bottom')
|
||||
%span.attaching-file-message
|
||||
-# Populated by app/assets/javascripts/dropzone_input.js
|
||||
%span.uploading-progress 0%
|
||||
|
@ -19,7 +19,7 @@
|
|||
|
||||
%span.uploading-error-container.hide
|
||||
%span.uploading-error-icon
|
||||
= sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
|
||||
= sprite_icon('paperclip', css_class: 'gl-icon gl-vertical-align-text-bottom')
|
||||
%span.uploading-error-message
|
||||
-# Populated by app/assets/javascripts/dropzone_input.js
|
||||
%button.btn.gl-button.btn-link.gl-vertical-align-baseline.retry-uploading-link
|
||||
|
@ -31,11 +31,6 @@
|
|||
= _("attach a new file")
|
||||
= _(".")
|
||||
|
||||
%button.btn.gl-button.btn-link.button-attach-file.markdown-selector.button-attach-file.gl-vertical-align-text-bottom
|
||||
= sprite_icon('media')
|
||||
%span.gl-button-text
|
||||
= _("Attach a file")
|
||||
|
||||
%button.btn.gl-button.btn-link.button-cancel-uploading-files.gl-vertical-align-baseline.hide
|
||||
%span.gl-button-text
|
||||
= _("Cancel")
|
||||
|
|
|
@ -65,14 +65,14 @@
|
|||
- if @user.pronouns.present?
|
||||
%span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle
|
||||
= "(#{@user.pronouns})"
|
||||
- if @user&.status && user_status_set_to_busy?(@user.status)
|
||||
- if @user.status&.busy?
|
||||
%span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle= s_("UserProfile|(Busy)")
|
||||
|
||||
- if @user.pronunciation.present?
|
||||
.gl-align-items-center
|
||||
%p.gl-mb-4.gl-text-gray-500= s_("UserProfile|Pronounced as: %{pronunciation}") % { pronunciation: @user.pronunciation }
|
||||
|
||||
- if show_status_emoji?(@user.status)
|
||||
- if @user.status&.customized?
|
||||
.cover-status.gl-display-inline-flex.gl-align-items-center
|
||||
= emoji_icon(@user.status.emoji, class: 'gl-mr-2')
|
||||
= markdown_field(@user.status, :message)
|
||||
|
|
|
@ -47,7 +47,8 @@ class ProjectCacheWorker
|
|||
|
||||
Projects::UpdateStatisticsService.new(project, nil, statistics: statistics).execute
|
||||
|
||||
UpdateProjectStatisticsWorker.perform_in(LEASE_TIMEOUT, project.id, statistics)
|
||||
lease_key = project_cache_worker_key(project.id, statistics)
|
||||
UpdateProjectStatisticsWorker.perform_in(LEASE_TIMEOUT, lease_key, project.id, statistics)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -10,10 +10,15 @@ class UpdateProjectStatisticsWorker # rubocop:disable Scalability/IdempotentWork
|
|||
|
||||
feature_category :source_code_management
|
||||
|
||||
# project_id - The ID of the project for which to flush the cache.
|
||||
# statistics - An Array containing columns from ProjectStatistics to
|
||||
# refresh, if empty all columns will be refreshed
|
||||
def perform(project_id, statistics = [])
|
||||
# lease_key - The exclusive lease key to take
|
||||
# project_id - The ID of the project for which to flush the cache.
|
||||
# statistics - An Array containing columns from ProjectStatistics to
|
||||
# refresh, if empty all columns will be refreshed
|
||||
def perform(lease_key, project_id, statistics = [])
|
||||
return unless Gitlab::ExclusiveLease
|
||||
.new(lease_key, timeout: ProjectCacheWorker::LEASE_TIMEOUT)
|
||||
.try_obtain
|
||||
|
||||
project = Project.find_by_id(project_id)
|
||||
|
||||
Projects::UpdateStatisticsService.new(project, nil, statistics: statistics).execute
|
||||
|
|
|
@ -532,6 +532,21 @@ module Gitlab
|
|||
|
||||
# DO NOT PLACE ANY INITIALIZERS AFTER THIS.
|
||||
config.after_initialize do
|
||||
config.active_record.yaml_column_permitted_classes = [
|
||||
Symbol, Date, Time,
|
||||
Gitlab::Diff::Position,
|
||||
# Used in:
|
||||
# app/models/concerns/diff_positionable_note.rb
|
||||
# app/models/legacy_diff_note.rb: serialize :st_diff
|
||||
ActiveSupport::HashWithIndifferentAccess,
|
||||
# Used in ee/lib/ee/api/helpers.rb: send_git_archive
|
||||
DeployToken,
|
||||
ActiveModel::Attribute.const_get(:FromDatabase, false), # https://gitlab.com/gitlab-org/gitlab/-/issues/368072
|
||||
# Used in app/services/web_hooks/log_execution_service.rb: log_execution
|
||||
ActiveSupport::TimeWithZone,
|
||||
ActiveSupport::TimeZone
|
||||
]
|
||||
|
||||
# on_master_start yields immediately in unclustered environments and runs
|
||||
# when the primary process is done initializing otherwise.
|
||||
Gitlab::Cluster::LifecycleEvents.on_master_start do
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
# rubocop:disable Database/MultipleDatabases
|
||||
|
||||
raise 'This patch should be dropped after upgrading Rails v6.1.6.1' if ActiveRecord::VERSION::STRING != "6.1.6.1"
|
||||
|
||||
module ActiveRecord
|
||||
module Coders # :nodoc:
|
||||
class YAMLColumn # :nodoc:
|
||||
private
|
||||
|
||||
def yaml_load(payload)
|
||||
return legacy_yaml_load(payload) if ActiveRecord::Base.use_yaml_unsafe_load
|
||||
|
||||
YAML.safe_load(payload, permitted_classes: ActiveRecord::Base.yaml_column_permitted_classes, aliases: true)
|
||||
rescue Psych::DisallowedClass => e
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
||||
|
||||
legacy_yaml_load(payload)
|
||||
end
|
||||
|
||||
def legacy_yaml_load(payload)
|
||||
if YAML.respond_to?(:unsafe_load)
|
||||
YAML.unsafe_load(payload)
|
||||
else
|
||||
YAML.load(payload) # rubocop:disable Security/YAMLLoad
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Database/MultipleDatabases
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ScheduleDisableLegacyOpenSourceLicenseForNoIssuesNoRepoProjects < Gitlab::Database::Migration[2.0]
|
||||
MIGRATION = 'DisableLegacyOpenSourceLicenseForNoIssuesNoRepoProjects'
|
||||
INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 5_000
|
||||
MAX_BATCH_SIZE = 10_000
|
||||
SUB_BATCH_SIZE = 200
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:projects,
|
||||
:id,
|
||||
job_interval: INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
max_batch_size: MAX_BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, :projects, :id, [])
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
b189304b940d01a527bba4ad8b0865ae44de1e3af2ef1b711d95993821106b6b
|
|
@ -18660,6 +18660,18 @@ Represents a hierarchy widget.
|
|||
| <a id="workitemwidgethierarchyparent"></a>`parent` | [`WorkItem`](#workitem) | Parent work item. |
|
||||
| <a id="workitemwidgethierarchytype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
|
||||
|
||||
### `WorkItemWidgetStartAndDueDate`
|
||||
|
||||
Represents a start and due date widget.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="workitemwidgetstartandduedateduedate"></a>`dueDate` | [`Date`](#date) | Due date of the work item. |
|
||||
| <a id="workitemwidgetstartandduedatestartdate"></a>`startDate` | [`Date`](#date) | Start date of the work item. |
|
||||
| <a id="workitemwidgetstartandduedatetype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
|
||||
|
||||
### `WorkItemWidgetWeight`
|
||||
|
||||
Represents a weight widget.
|
||||
|
@ -20523,6 +20535,7 @@ Type of a work item widget.
|
|||
| <a id="workitemwidgettypeassignees"></a>`ASSIGNEES` | Assignees widget. |
|
||||
| <a id="workitemwidgettypedescription"></a>`DESCRIPTION` | Description widget. |
|
||||
| <a id="workitemwidgettypehierarchy"></a>`HIERARCHY` | Hierarchy widget. |
|
||||
| <a id="workitemwidgettypestart_and_due_date"></a>`START_AND_DUE_DATE` | Start And Due Date widget. |
|
||||
| <a id="workitemwidgettypeweight"></a>`WEIGHT` | Weight widget. |
|
||||
|
||||
## Scalar types
|
||||
|
@ -21751,6 +21764,7 @@ Implementations:
|
|||
- [`WorkItemWidgetAssignees`](#workitemwidgetassignees)
|
||||
- [`WorkItemWidgetDescription`](#workitemwidgetdescription)
|
||||
- [`WorkItemWidgetHierarchy`](#workitemwidgethierarchy)
|
||||
- [`WorkItemWidgetStartAndDueDate`](#workitemwidgetstartandduedate)
|
||||
- [`WorkItemWidgetWeight`](#workitemwidgetweight)
|
||||
|
||||
##### Fields
|
||||
|
@ -22274,4 +22288,4 @@ A time-frame defined as a closed inclusive range of two dates.
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="workitemwidgetweightinputweight"></a>`weight` | [`Int!`](#int) | Weight of the work item. |
|
||||
| <a id="workitemwidgetweightinputweight"></a>`weight` | [`Int`](#int) | Weight of the work item. |
|
||||
|
|
|
@ -151,6 +151,8 @@ or [**Rebase** option](../../user/project/merge_requests/methods/index.md#rebasi
|
|||
|
||||
Prerequisites:
|
||||
|
||||
- The parent project's [CI/CD configuration file](../yaml/index.md) must be configured to
|
||||
[run jobs in merge request pipelines](#prerequisites).
|
||||
- You must be a member of the parent project and have at least the [Developer role](../../user/permissions.md).
|
||||
- The fork project must be [visible](../../user/public_access.md) to the
|
||||
user running the pipeline. Otherwise, the **Pipelines** tab does not display
|
||||
|
|
|
@ -950,6 +950,16 @@ For example:
|
|||
1. Optional. Enter a description for the job.
|
||||
```
|
||||
|
||||
### Recommended steps
|
||||
|
||||
If a step is recommended, start the step with the word `Recommended` followed by a period.
|
||||
|
||||
For example:
|
||||
|
||||
```markdown
|
||||
1. Recommended. Enter a description for the job.
|
||||
```
|
||||
|
||||
### Documenting multiple fields at once
|
||||
|
||||
If the UI text sufficiently explains the fields in a section, do not include a task step for every field.
|
||||
|
|
|
@ -33,10 +33,10 @@ To subscribe to GitLab SaaS:
|
|||
and decide which tier you want.
|
||||
1. Create a user account for yourself by using the
|
||||
[sign up page](https://gitlab.com/users/sign_up).
|
||||
1. Create a [group](../../user/group/index.md#create-a-group). Your license tier applies to the top-level group, its subgroups, and projects.
|
||||
1. Create a [group](../../user/group/index.md#create-a-group). Your subscription tier applies to the top-level group, its subgroups, and projects.
|
||||
1. Create additional users and
|
||||
[add them to the group](../../user/group/index.md#add-users-to-a-group). The users in this group, its subgroups, and projects can use
|
||||
the features of your license tier, and they consume a seat in your subscription.
|
||||
the features of your subscription tier, and they consume a seat in your subscription.
|
||||
1. On the left sidebar, select **Billing** and choose a tier.
|
||||
1. Fill out the form to complete your purchase.
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
|
@ -376,6 +376,8 @@ the [Asciidoctor user manual](https://asciidoctor.org/docs/user-manual/#activati
|
|||
|
||||
### Task lists
|
||||
|
||||
> Inapplicable checkboxes [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85982) in GitLab 15.3.
|
||||
|
||||
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#task-lists).
|
||||
|
||||
You can add task lists anywhere Markdown is supported.
|
||||
|
@ -384,22 +386,28 @@ You can add task lists anywhere Markdown is supported.
|
|||
- In all other places, you cannot select the boxes. You must edit the Markdown manually
|
||||
by adding or removing an `x` in the brackets.
|
||||
|
||||
Besides complete and incomplete, tasks can also be **inapplicable**. Selecting an inapplicable checkbox
|
||||
in an issue, merge request, or comment has no effect.
|
||||
|
||||
To create a task list, follow the format of an ordered or unordered list:
|
||||
|
||||
```markdown
|
||||
- [x] Completed task
|
||||
- [~] Inapplicable task
|
||||
- [ ] Incomplete task
|
||||
- [ ] Sub-task 1
|
||||
- [x] Sub-task 2
|
||||
- [x] Sub-task 1
|
||||
- [~] Sub-task 2
|
||||
- [ ] Sub-task 3
|
||||
|
||||
1. [x] Completed task
|
||||
1. [~] Inapplicable task
|
||||
1. [ ] Incomplete task
|
||||
1. [ ] Sub-task 1
|
||||
1. [x] Sub-task 2
|
||||
1. [x] Sub-task 1
|
||||
1. [~] Sub-task 2
|
||||
1. [ ] Sub-task 3
|
||||
```
|
||||
|
||||
![Task list as rendered by GitLab](img/completed_tasks_v13_3.png)
|
||||
![Task list as rendered by GitLab](img/completed_tasks_v15_3.png)
|
||||
|
||||
### Table of contents
|
||||
|
||||
|
|
|
@ -2015,3 +2015,15 @@
|
|||
07_01__gitlab_specific_markdown__footnotes__001:
|
||||
spec_txt_example_position: 674
|
||||
source_specification: gitlab
|
||||
07_02__gitlab_specific_markdown__task_list_items__001:
|
||||
spec_txt_example_position: 675
|
||||
source_specification: gitlab
|
||||
07_02__gitlab_specific_markdown__task_list_items__002:
|
||||
spec_txt_example_position: 676
|
||||
source_specification: gitlab
|
||||
07_02__gitlab_specific_markdown__task_list_items__003:
|
||||
spec_txt_example_position: 677
|
||||
source_specification: gitlab
|
||||
07_02__gitlab_specific_markdown__task_list_items__004:
|
||||
spec_txt_example_position: 678
|
||||
source_specification: gitlab
|
||||
|
|
|
@ -7588,3 +7588,75 @@
|
|||
wysiwyg: |-
|
||||
<p>footnote reference tag <sup identifier="fortytwo">fortytwo</sup></p>
|
||||
<div node="footnoteDefinition(paragraph("footnote text"))" htmlattributes="[object Object]"><p>footnote text</p></div>
|
||||
07_02__gitlab_specific_markdown__task_list_items__001:
|
||||
canonical: |
|
||||
<ul>
|
||||
<li>
|
||||
<task-button/>
|
||||
<input type="checkbox" disabled/>
|
||||
incomplete
|
||||
</li>
|
||||
</ul>
|
||||
static: |-
|
||||
<ul data-sourcepos="1:1-1:16" class="task-list" dir="auto">
|
||||
<li data-sourcepos="1:1-1:16" class="task-list-item">
|
||||
<task-button></task-button><input type="checkbox" class="task-list-item-checkbox" disabled> incomplete</li>
|
||||
</ul>
|
||||
wysiwyg: |-
|
||||
<ul start="1" parens="false" data-type="taskList"><li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p>incomplete</p></div></li></ul>
|
||||
07_02__gitlab_specific_markdown__task_list_items__002:
|
||||
canonical: |
|
||||
<ul>
|
||||
<li>
|
||||
<task-button/>
|
||||
<input type="checkbox" checked disabled/>
|
||||
completed
|
||||
</li>
|
||||
</ul>
|
||||
static: |-
|
||||
<ul data-sourcepos="1:1-1:15" class="task-list" dir="auto">
|
||||
<li data-sourcepos="1:1-1:15" class="task-list-item">
|
||||
<task-button></task-button><input type="checkbox" class="task-list-item-checkbox" checked disabled> completed</li>
|
||||
</ul>
|
||||
wysiwyg: |-
|
||||
<ul start="1" parens="false" data-type="taskList"><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p>completed</p></div></li></ul>
|
||||
07_02__gitlab_specific_markdown__task_list_items__003:
|
||||
canonical: |
|
||||
<ul>
|
||||
<li>
|
||||
<task-button/>
|
||||
<input type="checkbox" data-inapplicable disabled>
|
||||
<s>
|
||||
inapplicable
|
||||
</s>
|
||||
</li>
|
||||
</ul>
|
||||
static: |-
|
||||
<ul data-sourcepos="1:1-1:18" class="task-list" dir="auto">
|
||||
<li data-sourcepos="1:1-1:18" class="task-list-item inapplicable">
|
||||
<task-button></task-button><input type="checkbox" class="task-list-item-checkbox" data-inapplicable disabled> <s>inapplicable</s>
|
||||
</li>
|
||||
</ul>
|
||||
07_02__gitlab_specific_markdown__task_list_items__004:
|
||||
canonical: |
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<task-button/>
|
||||
<input type="checkbox" data-inapplicable disabled>
|
||||
<s>
|
||||
inapplicable
|
||||
</s>
|
||||
</p>
|
||||
<p>
|
||||
text in loose list
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
static: |-
|
||||
<ul data-sourcepos="1:1-3:20" class="task-list" dir="auto">
|
||||
<li data-sourcepos="1:1-3:20" class="task-list-item inapplicable">
|
||||
<p data-sourcepos="1:3-1:18"><task-button></task-button><input type="checkbox" class="task-list-item-checkbox" data-inapplicable disabled> <s>inapplicable</s></p>
|
||||
<p data-sourcepos="3:3-3:20">text in loose list</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -2193,3 +2193,13 @@
|
|||
footnote reference tag [^fortytwo]
|
||||
|
||||
[^fortytwo]: footnote text
|
||||
07_02__gitlab_specific_markdown__task_list_items__001: |
|
||||
- [ ] incomplete
|
||||
07_02__gitlab_specific_markdown__task_list_items__002: |
|
||||
- [x] completed
|
||||
07_02__gitlab_specific_markdown__task_list_items__003: |
|
||||
- [~] inapplicable
|
||||
07_02__gitlab_specific_markdown__task_list_items__004: |
|
||||
- [~] inapplicable
|
||||
|
||||
text in loose list
|
||||
|
|
|
@ -19244,3 +19244,73 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
07_02__gitlab_specific_markdown__task_list_items__001: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "taskList",
|
||||
"attrs": {
|
||||
"numeric": false,
|
||||
"start": 1,
|
||||
"parens": false
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "taskItem",
|
||||
"attrs": {
|
||||
"checked": false
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "incomplete"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
07_02__gitlab_specific_markdown__task_list_items__002: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "taskList",
|
||||
"attrs": {
|
||||
"numeric": false,
|
||||
"start": 1,
|
||||
"parens": false
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "taskItem",
|
||||
"attrs": {
|
||||
"checked": true
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "completed"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
07_02__gitlab_specific_markdown__task_list_items__003: |-
|
||||
Inapplicable task list items not yet implemented for WYSYWIG
|
||||
07_02__gitlab_specific_markdown__task_list_items__004: |-
|
||||
Inapplicable task list items not yet implemented for WYSYWIG
|
||||
|
|
|
@ -38,3 +38,85 @@ footnote text
|
|||
</ol>
|
||||
</section>
|
||||
````````````````````````````````
|
||||
|
||||
## Task list items
|
||||
|
||||
See
|
||||
[Task lists](https://docs.gitlab.com/ee/user/markdown.html#task-lists) in the GitLab Flavored Markdown documentation.
|
||||
|
||||
Task list items (checkboxes) are defined as a GitHub Flavored Markdown extension in a section above.
|
||||
GitLab extends the behavior of task list items to support additional features.
|
||||
Some of these features are in-progress, and should not yet be considered part of the official
|
||||
GitLab Flavored Markdown specification.
|
||||
|
||||
Some of the behavior of task list items is implemented as client-side JavaScript/CSS.
|
||||
|
||||
The following are some basic examples; more examples may be added in the future.
|
||||
|
||||
Incomplete task:
|
||||
|
||||
```````````````````````````````` example gitlab tasklist
|
||||
- [ ] incomplete
|
||||
.
|
||||
<ul>
|
||||
<li>
|
||||
<task-button/>
|
||||
<input type="checkbox" disabled/>
|
||||
incomplete
|
||||
</li>
|
||||
</ul>
|
||||
````````````````````````````````
|
||||
|
||||
Completed task:
|
||||
|
||||
```````````````````````````````` example gitlab tasklist
|
||||
- [x] completed
|
||||
.
|
||||
<ul>
|
||||
<li>
|
||||
<task-button/>
|
||||
<input type="checkbox" checked disabled/>
|
||||
completed
|
||||
</li>
|
||||
</ul>
|
||||
````````````````````````````````
|
||||
|
||||
Inapplicable task:
|
||||
|
||||
```````````````````````````````` example gitlab tasklist
|
||||
- [~] inapplicable
|
||||
.
|
||||
<ul>
|
||||
<li>
|
||||
<task-button/>
|
||||
<input type="checkbox" data-inapplicable disabled>
|
||||
<s>
|
||||
inapplicable
|
||||
</s>
|
||||
</li>
|
||||
</ul>
|
||||
````````````````````````````````
|
||||
|
||||
Inapplicable task in a "loose" list. Note that the `<del>` tag is not applied to the
|
||||
loose text; it has strikethrough applied with CSS.
|
||||
|
||||
```````````````````````````````` example gitlab tasklist
|
||||
- [~] inapplicable
|
||||
|
||||
text in loose list
|
||||
.
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<task-button/>
|
||||
<input type="checkbox" data-inapplicable disabled>
|
||||
<s>
|
||||
inapplicable
|
||||
</s>
|
||||
</p>
|
||||
<p>
|
||||
text in loose list
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
````````````````````````````````
|
||||
|
|
|
@ -12,3 +12,15 @@
|
|||
skip_running_snapshot_static_html_tests: false # NOT YET SUPPORTED
|
||||
skip_running_snapshot_wysiwyg_html_tests: false
|
||||
skip_running_snapshot_prosemirror_json_tests: false
|
||||
07_02__gitlab_specific_markdown__task_list_items__003:
|
||||
skip_update_example_snapshot_html_wysiwyg: Inapplicable task list items not yet implemented for WYSYWIG
|
||||
skip_update_example_snapshot_prosemirror_json: Inapplicable task list items not yet implemented for WYSYWIG
|
||||
skip_running_conformance_wysiwyg_tests: Inapplicable task list items not yet implemented for WYSYWIG
|
||||
skip_running_snapshot_wysiwyg_html_tests: Inapplicable task list items not yet implemented for WYSYWIG
|
||||
skip_running_snapshot_prosemirror_json_tests: Inapplicable task list items not yet implemented for WYSYWIG
|
||||
07_02__gitlab_specific_markdown__task_list_items__004:
|
||||
skip_update_example_snapshot_html_wysiwyg: Inapplicable task list items not yet implemented for WYSYWIG
|
||||
skip_update_example_snapshot_prosemirror_json: Inapplicable task list items not yet implemented for WYSYWIG
|
||||
skip_running_conformance_wysiwyg_tests: Inapplicable task list items not yet implemented for WYSYWIG
|
||||
skip_running_snapshot_wysiwyg_html_tests: Inapplicable task list items not yet implemented for WYSYWIG
|
||||
skip_running_snapshot_prosemirror_json_tests: Inapplicable task list items not yet implemented for WYSYWIG
|
||||
|
|
|
@ -9641,6 +9641,88 @@ footnote text
|
|||
</section>
|
||||
````````````````````````````````
|
||||
|
||||
## Task list items
|
||||
|
||||
See
|
||||
[Task lists](https://docs.gitlab.com/ee/user/markdown.html#task-lists) in the GitLab Flavored Markdown documentation.
|
||||
|
||||
Task list items (checkboxes) are defined as a GitHub Flavored Markdown extension in a section above.
|
||||
GitLab extends the behavior of task list items to support additional features.
|
||||
Some of these features are in-progress, and should not yet be considered part of the official
|
||||
GitLab Flavored Markdown specification.
|
||||
|
||||
Some of the behavior of task list items is implemented as client-side JavaScript/CSS.
|
||||
|
||||
The following are some basic examples; more examples may be added in the future.
|
||||
|
||||
Incomplete task:
|
||||
|
||||
```````````````````````````````` example gitlab tasklist
|
||||
- [ ] incomplete
|
||||
.
|
||||
<ul>
|
||||
<li>
|
||||
<task-button/>
|
||||
<input type="checkbox" disabled/>
|
||||
incomplete
|
||||
</li>
|
||||
</ul>
|
||||
````````````````````````````````
|
||||
|
||||
Completed task:
|
||||
|
||||
```````````````````````````````` example gitlab tasklist
|
||||
- [x] completed
|
||||
.
|
||||
<ul>
|
||||
<li>
|
||||
<task-button/>
|
||||
<input type="checkbox" checked disabled/>
|
||||
completed
|
||||
</li>
|
||||
</ul>
|
||||
````````````````````````````````
|
||||
|
||||
Inapplicable task:
|
||||
|
||||
```````````````````````````````` example gitlab tasklist
|
||||
- [~] inapplicable
|
||||
.
|
||||
<ul>
|
||||
<li>
|
||||
<task-button/>
|
||||
<input type="checkbox" data-inapplicable disabled>
|
||||
<s>
|
||||
inapplicable
|
||||
</s>
|
||||
</li>
|
||||
</ul>
|
||||
````````````````````````````````
|
||||
|
||||
Inapplicable task in a "loose" list. Note that the `<del>` tag is not applied to the
|
||||
loose text; it has strikethrough applied with CSS.
|
||||
|
||||
```````````````````````````````` example gitlab tasklist
|
||||
- [~] inapplicable
|
||||
|
||||
text in loose list
|
||||
.
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<task-button/>
|
||||
<input type="checkbox" data-inapplicable disabled>
|
||||
<s>
|
||||
inapplicable
|
||||
</s>
|
||||
</p>
|
||||
<p>
|
||||
text in loose list
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
````````````````````````````````
|
||||
|
||||
<!-- END TESTS -->
|
||||
|
||||
# Appendix: A parsing strategy
|
||||
|
|
|
@ -8,9 +8,93 @@ require 'task_list/filter'
|
|||
# - app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
|
||||
module Banzai
|
||||
module Filter
|
||||
# TaskList filter replaces task list item markers (`[ ]`, `[x]`, and `[~]`)
|
||||
# with checkboxes, marked up with metadata and behavior.
|
||||
#
|
||||
# This should be run on the HTML generated by the Markdown filter, after the
|
||||
# SanitizationFilter.
|
||||
#
|
||||
# Syntax
|
||||
# ------
|
||||
#
|
||||
# Task list items must be in a list format:
|
||||
#
|
||||
# ```
|
||||
# - [ ] incomplete
|
||||
# - [x] complete
|
||||
# - [~] inapplicable
|
||||
# ```
|
||||
#
|
||||
# This class overrides TaskList::Filter in the `deckar01-task_list` gem
|
||||
# to add support for inapplicable task items
|
||||
class TaskListFilter < TaskList::Filter
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
XPATH = 'descendant-or-self::li[input[@data-inapplicable]] | descendant-or-self::li[p[input[@data-inapplicable]]]'
|
||||
INAPPLICABLE = '[~]'
|
||||
INAPPLICABLEPATTERN = /\[~\]/.freeze
|
||||
|
||||
# Pattern used to identify all task list items.
|
||||
# Useful when you need iterate over all items.
|
||||
NEWITEMPATTERN = /
|
||||
^
|
||||
(?:\s*[-+*]|(?:\d+\.))? # optional list prefix
|
||||
\s* # optional whitespace prefix
|
||||
( # checkbox
|
||||
#{CompletePattern}|
|
||||
#{IncompletePattern}|
|
||||
#{INAPPLICABLEPATTERN}
|
||||
)
|
||||
(?=\s) # followed by whitespace
|
||||
/x.freeze
|
||||
|
||||
# Force the gem's constant to use our new one
|
||||
superclass.send(:remove_const, :ItemPattern) # rubocop: disable GitlabSecurity/PublicSend
|
||||
superclass.const_set(:ItemPattern, NEWITEMPATTERN)
|
||||
|
||||
def inapplicable?(item)
|
||||
!!(item.checkbox_text =~ INAPPLICABLEPATTERN)
|
||||
end
|
||||
|
||||
override :render_item_checkbox
|
||||
def render_item_checkbox(item)
|
||||
"<task-button></task-button>#{super}"
|
||||
%(<task-button></task-button><input type="checkbox"
|
||||
class="task-list-item-checkbox"
|
||||
#{'checked="checked"' if item.complete?}
|
||||
#{'data-inapplicable' if inapplicable?(item)}
|
||||
disabled="disabled"/>)
|
||||
end
|
||||
|
||||
override :render_task_list_item
|
||||
def render_task_list_item(item)
|
||||
source = item.source
|
||||
|
||||
if inapplicable?(item)
|
||||
# Add a `<s>` tag around the list item text. However because of the
|
||||
# way tasks are built, the source can include an embedded sublist, like
|
||||
# `[~] foobar\n<ol><li....`
|
||||
# The `<s>` should only be added to the main text.
|
||||
source = source.partition("#{INAPPLICABLE} ")
|
||||
text = source.last.partition(/\<(ol|ul)/)
|
||||
text[0] = "<s>#{text[0]}</s>"
|
||||
source[-1] = text.join
|
||||
source = source.join
|
||||
end
|
||||
|
||||
Nokogiri::HTML.fragment \
|
||||
source.sub(ItemPattern, render_item_checkbox(item)), 'utf-8'
|
||||
end
|
||||
|
||||
override :call
|
||||
def call
|
||||
super
|
||||
|
||||
# add class to li for any inapplicable checkboxes
|
||||
doc.xpath(XPATH).each do |li|
|
||||
li.add_class('inapplicable')
|
||||
end
|
||||
|
||||
doc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# Set `project_settings.legacy_open_source_license_available` to false for public projects with no issues & no repo
|
||||
class DisableLegacyOpenSourceLicenseForNoIssuesNoRepoProjects < ::Gitlab::BackgroundMigration::BatchedMigrationJob
|
||||
PUBLIC = 20
|
||||
|
||||
# Migration only version of `project_settings` table
|
||||
class ProjectSetting < ApplicationRecord
|
||||
self.table_name = 'project_settings'
|
||||
end
|
||||
|
||||
def perform
|
||||
each_sub_batch(
|
||||
operation_name: :disable_legacy_open_source_license_for_no_issues_no_repo_projects,
|
||||
batching_scope: ->(relation) { relation.where(visibility_level: PUBLIC) }
|
||||
) do |sub_batch|
|
||||
no_issues_no_repo_projects =
|
||||
sub_batch
|
||||
.joins('LEFT OUTER JOIN project_statistics ON project_statistics.project_id = projects.id')
|
||||
.joins('LEFT OUTER JOIN project_settings ON project_settings.project_id = projects.id')
|
||||
.joins('LEFT OUTER JOIN issues ON issues.project_id = projects.id')
|
||||
.where('project_statistics.repository_size' => 0,
|
||||
'project_settings.legacy_open_source_license_available' => true)
|
||||
.group('projects.id')
|
||||
.having('COUNT(issues.id) = 0')
|
||||
|
||||
ProjectSetting
|
||||
.where(project_id: no_issues_no_repo_projects)
|
||||
.update_all(legacy_open_source_license_available: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5233,7 +5233,7 @@ msgstr ""
|
|||
msgid "At risk"
|
||||
msgstr ""
|
||||
|
||||
msgid "Attach a file"
|
||||
msgid "Attach a file or image"
|
||||
msgstr ""
|
||||
|
||||
msgid "Attaching File - %{progress}"
|
||||
|
|
|
@ -151,7 +151,7 @@ RSpec.describe "User creates issue" do
|
|||
click_button 'Cancel'
|
||||
end
|
||||
|
||||
expect(page).to have_button('Attach a file')
|
||||
expect(page).to have_selector('[data-testid="button-attach-file"]')
|
||||
expect(page).not_to have_button('Cancel')
|
||||
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
|
||||
end
|
||||
|
|
|
@ -109,10 +109,24 @@ RSpec.describe 'Copy as GFM', :js do
|
|||
<<~GFM,
|
||||
* [ ] Unchecked task
|
||||
* [x] Checked task
|
||||
* [~] Inapplicable task
|
||||
* [~] Inapplicable task with ~~del~~ and <s>strike</s> embedded
|
||||
GFM
|
||||
<<~GFM
|
||||
<<~GFM,
|
||||
1. [ ] Unchecked ordered task
|
||||
1. [x] Checked ordered task
|
||||
1. [~] Inapplicable ordered task
|
||||
1. [~] Inapplicable ordered task with ~~del~~ and <s>strike</s> embedded
|
||||
GFM
|
||||
<<~GFM
|
||||
* [ ] Unchecked loose list task
|
||||
* [x] Checked loose list task
|
||||
* [~] Inapplicable loose list task
|
||||
|
||||
With a paragraph
|
||||
* [~] Inapplicable loose list task with ~~del~~ and <s>strike</s> embedded
|
||||
|
||||
With a paragraph
|
||||
GFM
|
||||
)
|
||||
|
||||
|
@ -605,7 +619,8 @@ RSpec.describe 'Copy as GFM', :js do
|
|||
'###### Heading',
|
||||
'**Bold**',
|
||||
'*Italics*',
|
||||
'~~Strikethrough~~',
|
||||
'~~Strikethrough (del)~~',
|
||||
'<s>Strikethrough</s>',
|
||||
'---',
|
||||
# table
|
||||
<<~GFM,
|
||||
|
|
|
@ -103,9 +103,9 @@ RSpec.describe 'Project > Tags', :js do
|
|||
end
|
||||
end
|
||||
|
||||
it 'release notes form shows "Attach a file" button', :js do
|
||||
it 'release notes form shows "Attach a file or image" button', :js do
|
||||
page.within('.content form.release-form') do
|
||||
expect(page).to have_button('Attach a file')
|
||||
expect(page).to have_selector('[data-testid="button-attach-file"]')
|
||||
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,8 +16,8 @@ RSpec.describe 'User uploads file to note' do
|
|||
end
|
||||
|
||||
context 'before uploading' do
|
||||
it 'shows "Attach a file" button', :js do
|
||||
expect(page).to have_button('Attach a file')
|
||||
it 'shows "Attach a file or image" button', :js do
|
||||
expect(page).to have_selector('[data-testid="button-attach-file"]')
|
||||
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
|
||||
end
|
||||
end
|
||||
|
@ -30,7 +30,7 @@ RSpec.describe 'User uploads file to note' do
|
|||
click_button 'Cancel'
|
||||
end
|
||||
|
||||
expect(page).to have_button('Attach a file')
|
||||
expect(page).to have_selector('[data-testid="button-attach-file"]')
|
||||
expect(page).not_to have_button('Cancel')
|
||||
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
|
||||
end
|
||||
|
@ -60,16 +60,15 @@ RSpec.describe 'User uploads file to note' do
|
|||
expect(page).to have_selector('.uploading-error-message', visible: true, text: error_text)
|
||||
expect(page).to have_button('Try again', visible: true)
|
||||
expect(page).to have_button('attach a new file', visible: true)
|
||||
expect(page).not_to have_button('Attach a file')
|
||||
end
|
||||
end
|
||||
|
||||
context 'uploading is complete' do
|
||||
it 'shows "Attach a file" button on uploading complete', :js do
|
||||
it 'shows "Attach a file or image" button on uploading complete', :js do
|
||||
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_button('Attach a file')
|
||||
expect(page).to have_selector('[data-testid="button-attach-file"]')
|
||||
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
|
||||
end
|
||||
|
||||
|
|
|
@ -275,9 +275,11 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
|
|||
|
||||
- [ ] Incomplete task 1
|
||||
- [x] Complete task 1
|
||||
- [~] Inapplicable task 1
|
||||
- [ ] Incomplete task 2
|
||||
- [ ] Incomplete sub-task 1
|
||||
- [ ] Incomplete sub-task 2
|
||||
- [~] Inapplicable sub-task 1
|
||||
- [x] Complete sub-task 1
|
||||
- [X] Complete task 2
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue';
|
||||
import EnvironmentsDetailHeader from '~/environments/components/environments_detail_header.vue';
|
||||
import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
|
||||
|
@ -43,6 +44,9 @@ describe('Environments detail header component', () => {
|
|||
GlSprintf,
|
||||
TimeAgo,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
propsData: {
|
||||
canAdminEnvironment: false,
|
||||
canUpdateEnvironment: false,
|
||||
|
@ -185,6 +189,14 @@ describe('Environments detail header component', () => {
|
|||
it('displays the metrics button with correct path', () => {
|
||||
expect(findMetricsButton().attributes('href')).toBe(metricsPath);
|
||||
});
|
||||
|
||||
it('uses a gl tooltip for the title', () => {
|
||||
const button = findMetricsButton();
|
||||
const tooltip = getBinding(button.element, 'gl-tooltip');
|
||||
|
||||
expect(tooltip).toBeDefined();
|
||||
expect(button.attributes('title')).toBe('See metrics');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when has all admin rights', () => {
|
||||
|
|
|
@ -173,4 +173,50 @@ describe('~/lib/dompurify', () => {
|
|||
expect(sanitize(html)).toBe(`<a>internal link</a>`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('links with target attribute', () => {
|
||||
const getSanitizedNode = (html) => {
|
||||
return document.createRange().createContextualFragment(sanitize(html)).firstElementChild;
|
||||
};
|
||||
|
||||
it('adds secure context', () => {
|
||||
const html = `<a href="https://example.com" target="_blank">link</a>`;
|
||||
const el = getSanitizedNode(html);
|
||||
|
||||
expect(el.getAttribute('target')).toBe('_blank');
|
||||
expect(el.getAttribute('rel')).toBe('noopener noreferrer');
|
||||
});
|
||||
|
||||
it('adds secure context and merge existing `rel` values', () => {
|
||||
const html = `<a href="https://example.com" target="_blank" rel="help External">link</a>`;
|
||||
const el = getSanitizedNode(html);
|
||||
|
||||
expect(el.getAttribute('target')).toBe('_blank');
|
||||
expect(el.getAttribute('rel')).toBe('help external noopener noreferrer');
|
||||
});
|
||||
|
||||
it('does not duplicate noopener/noreferrer `rel` values', () => {
|
||||
const html = `<a href="https://example.com" target="_blank" rel="noreferrer noopener">link</a>`;
|
||||
const el = getSanitizedNode(html);
|
||||
|
||||
expect(el.getAttribute('target')).toBe('_blank');
|
||||
expect(el.getAttribute('rel')).toBe('noreferrer noopener');
|
||||
});
|
||||
|
||||
it('does not update `rel` values when target is not `_blank` ', () => {
|
||||
const html = `<a href="https://example.com" target="_self" rel="help">internal</a>`;
|
||||
const el = getSanitizedNode(html);
|
||||
|
||||
expect(el.getAttribute('target')).toBe('_self');
|
||||
expect(el.getAttribute('rel')).toBe('help');
|
||||
});
|
||||
|
||||
it('does not update `rel` values when target attribute is not present', () => {
|
||||
const html = `<a href="https://example.com">link</a>`;
|
||||
const el = getSanitizedNode(html);
|
||||
|
||||
expect(el.hasAttribute('target')).toBe(false);
|
||||
expect(el.hasAttribute('rel')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -193,6 +193,7 @@ describe('init markdown', () => {
|
|||
${'- [ ] item'} | ${'- [ ] item\n- [ ] '}
|
||||
${'- [x] item'} | ${'- [x] item\n- [ ] '}
|
||||
${'- [X] item'} | ${'- [X] item\n- [ ] '}
|
||||
${'- [~] item'} | ${'- [~] item\n- [ ] '}
|
||||
${'- [ ] nbsp (U+00A0)'} | ${'- [ ] nbsp (U+00A0)\n- [ ] '}
|
||||
${'- item\n - second'} | ${'- item\n - second\n - '}
|
||||
${'- - -'} | ${'- - -'}
|
||||
|
@ -205,6 +206,7 @@ describe('init markdown', () => {
|
|||
${'1. [ ] item'} | ${'1. [ ] item\n2. [ ] '}
|
||||
${'1. [x] item'} | ${'1. [x] item\n2. [ ] '}
|
||||
${'1. [X] item'} | ${'1. [X] item\n2. [ ] '}
|
||||
${'1. [~] item'} | ${'1. [~] item\n2. [ ] '}
|
||||
${'108. item'} | ${'108. item\n109. '}
|
||||
${'108. item\n - second'} | ${'108. item\n - second\n - '}
|
||||
${'108. item\n 1. second'} | ${'108. item\n 1. second\n 2. '}
|
||||
|
@ -228,11 +230,13 @@ describe('init markdown', () => {
|
|||
${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'}
|
||||
${'- [x] item\n- [x] '} | ${'- [x] item\n'}
|
||||
${'- [X] item\n- [X] '} | ${'- [X] item\n'}
|
||||
${'- [~] item\n- [~] '} | ${'- [~] item\n'}
|
||||
${'- item\n - second\n - '} | ${'- item\n - second\n'}
|
||||
${'1. item\n2. '} | ${'1. item\n'}
|
||||
${'1. [ ] item\n2. [ ] '} | ${'1. [ ] item\n'}
|
||||
${'1. [x] item\n2. [x] '} | ${'1. [x] item\n'}
|
||||
${'1. [X] item\n2. [X] '} | ${'1. [X] item\n'}
|
||||
${'1. [~] item\n2. [~] '} | ${'1. [~] item\n'}
|
||||
${'108. item\n109. '} | ${'108. item\n'}
|
||||
${'108. item\n - second\n - '} | ${'108. item\n - second\n'}
|
||||
${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'}
|
||||
|
|
|
@ -76,7 +76,7 @@ describe('Markdown field component', () => {
|
|||
const getMarkdownButton = () => subject.find('.js-md');
|
||||
const getListBulletedButton = () => subject.findAll('.js-md[title="Add a bullet list"]');
|
||||
const getVideo = () => subject.find('video');
|
||||
const getAttachButton = () => subject.find('.button-attach-file');
|
||||
const getAttachButton = () => subject.findByTestId('button-attach-file');
|
||||
const clickAttachButton = () => getAttachButton().trigger('click');
|
||||
const findDropzone = () => subject.find('.div-dropzone');
|
||||
const findMarkdownHeader = () => subject.findComponent(MarkdownFieldHeader);
|
||||
|
@ -232,13 +232,10 @@ describe('Markdown field component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should render attach a file button', () => {
|
||||
expect(getAttachButton().text()).toBe('Attach a file');
|
||||
});
|
||||
|
||||
it('should trigger dropzone when attach button is clicked', () => {
|
||||
expect(dropzoneSpy).not.toHaveBeenCalled();
|
||||
|
||||
getAttachButton().trigger('click');
|
||||
clickAttachButton();
|
||||
|
||||
expect(dropzoneSpy).toHaveBeenCalled();
|
||||
|
|
|
@ -56,6 +56,7 @@ describe('Markdown field header component', () => {
|
|||
'Add a task list',
|
||||
'Add a collapsible section',
|
||||
'Add a table',
|
||||
'Attach a file or image',
|
||||
'Go full screen',
|
||||
];
|
||||
const elements = findToolbarButtons();
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::WorkItems::Widgets::StartAndDueDateType do
|
||||
it 'exposes the expected fields' do
|
||||
expected_fields = %i[due_date start_date type]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
end
|
||||
end
|
|
@ -14,6 +14,16 @@ RSpec.describe GitlabScriptTagHelper do
|
|||
expect(helper.javascript_include_tag(script_url).to_s)
|
||||
.to eq "<script src=\"/javascripts/#{script_url}\" defer=\"defer\" nonce=\"noncevalue\"></script>"
|
||||
end
|
||||
|
||||
it 'returns a script tag with defer=false and a nonce' do
|
||||
expect(helper.javascript_include_tag(script_url, defer: nil).to_s)
|
||||
.to eq "<script src=\"/javascripts/#{script_url}\" nonce=\"noncevalue\"></script>"
|
||||
end
|
||||
|
||||
it 'returns a script tag with a nonce even nonce is set to nil' do
|
||||
expect(helper.javascript_include_tag(script_url, nonce: nil).to_s)
|
||||
.to eq "<script src=\"/javascripts/#{script_url}\" defer=\"defer\" nonce=\"noncevalue\"></script>"
|
||||
end
|
||||
end
|
||||
|
||||
describe 'inline script tag' do
|
||||
|
|
|
@ -67,38 +67,6 @@ RSpec.describe ProfilesHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#user_status_set_to_busy?" do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:availability, :result) do
|
||||
"busy" | true
|
||||
"not_set" | false
|
||||
"" | false
|
||||
nil | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { expect(helper.user_status_set_to_busy?(OpenStruct.new(availability: availability))).to eq(result) }
|
||||
end
|
||||
end
|
||||
|
||||
describe "#show_status_emoji?" do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:message, :emoji, :result) do
|
||||
"Some message" | UserStatus::DEFAULT_EMOJI | true
|
||||
"Some message" | "" | true
|
||||
"" | "basketball" | true
|
||||
"" | "basketball" | true
|
||||
"" | UserStatus::DEFAULT_EMOJI | false
|
||||
"" | UserStatus::DEFAULT_EMOJI | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { expect(helper.show_status_emoji?(OpenStruct.new(message: message, emoji: emoji))).to eq(result) }
|
||||
end
|
||||
end
|
||||
|
||||
describe "#ssh_key_expiration_tooltip" do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Rails YAML safe load patch' do
|
||||
let(:unsafe_load) { false }
|
||||
|
||||
let(:klass) do
|
||||
Class.new(ActiveRecord::Base) do
|
||||
self.table_name = 'issues'
|
||||
|
||||
serialize :description
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
allow(ActiveRecord::Base).to receive(:use_yaml_unsafe_load).and_return(unsafe_load)
|
||||
end
|
||||
|
||||
context 'with safe load' do
|
||||
let(:instance) { klass.new(description: data) }
|
||||
|
||||
context 'with default permitted classes' do
|
||||
let(:data) do
|
||||
{
|
||||
"test" => Time.now,
|
||||
ab: 1
|
||||
}
|
||||
end
|
||||
|
||||
it 'deserializes data' do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
|
||||
|
||||
instance.save!
|
||||
|
||||
expect(klass.find(instance.id).description).to eq(data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unpermitted classes' do
|
||||
let(:data) { DateTime.now }
|
||||
|
||||
it 'logs an exception and loads the data' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).twice
|
||||
|
||||
instance.save!
|
||||
|
||||
expect(klass.find(instance.id).description).to eq(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unsafe load' do
|
||||
let(:unsafe_load) { true }
|
||||
let(:data) { DateTime.now }
|
||||
let(:instance) { klass.new(description: data) }
|
||||
|
||||
it 'loads the data' do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
|
||||
|
||||
instance.save!
|
||||
|
||||
expect(klass.find(instance.id).description).to eq(data)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,4 +10,38 @@ RSpec.describe Banzai::Filter::TaskListFilter do
|
|||
|
||||
expect(doc.xpath('.//li//task-button').count).to eq(2)
|
||||
end
|
||||
|
||||
describe 'inapplicable list items' do
|
||||
shared_examples 'a valid inapplicable task list item' do |html|
|
||||
it "behaves correctly for `#{html}`" do
|
||||
doc = filter("<ul><li>#{html}</li></ul>")
|
||||
|
||||
expect(doc.css('li.inapplicable input[data-inapplicable]').count).to eq(1)
|
||||
expect(doc.css('li.inapplicable > s').count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'an invalid inapplicable task list item' do |html|
|
||||
it "does nothing for `#{html}`" do
|
||||
doc = filter("<ul><li>#{html}</li></ul>")
|
||||
|
||||
expect(doc.css('li.inapplicable input[data-inapplicable]').count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'a valid inapplicable task list item', '[~] foobar'
|
||||
it_behaves_like 'a valid inapplicable task list item', '[~] foo <em>bar</em>'
|
||||
it_behaves_like 'an invalid inapplicable task list item', '[ ] foobar'
|
||||
it_behaves_like 'an invalid inapplicable task list item', '[x] foobar'
|
||||
it_behaves_like 'an invalid inapplicable task list item', 'foo [~] bar'
|
||||
|
||||
it 'does not wrap a sublist with <s>' do
|
||||
html = '[~] foo <em>bar</em>\n<ol><li>sublist</li></ol>'
|
||||
doc = filter("<ul><li>#{html}</li></ul>")
|
||||
|
||||
expect(doc.to_html).to include('<s>foo <em>bar</em>\n</s>')
|
||||
expect(doc.css('li.inapplicable input[data-inapplicable]').count).to eq(1)
|
||||
expect(doc.css('li.inapplicable > s').count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForNoIssuesNoRepoProjects,
|
||||
:migration,
|
||||
schema: 20220722084543 do
|
||||
let(:namespaces_table) { table(:namespaces) }
|
||||
let(:projects_table) { table(:projects) }
|
||||
let(:project_settings_table) { table(:project_settings) }
|
||||
let(:project_statistics_table) { table(:project_statistics) }
|
||||
let(:issues_table) { table(:issues) }
|
||||
|
||||
subject(:perform_migration) do
|
||||
described_class.new(start_id: projects_table.minimum(:id),
|
||||
end_id: projects_table.maximum(:id),
|
||||
batch_table: :projects,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 2,
|
||||
pause_ms: 0,
|
||||
connection: ActiveRecord::Base.connection)
|
||||
.perform
|
||||
end
|
||||
|
||||
it 'sets `legacy_open_source_license_available` to false only for public projects with no issues and no repo',
|
||||
:aggregate_failures do
|
||||
project_with_no_issues_no_repo = create_legacy_license_public_project('project-with-no-issues-no-repo')
|
||||
project_with_repo = create_legacy_license_public_project('project-with-repo', repo_size: 1)
|
||||
project_with_issues = create_legacy_license_public_project('project-with-issues', with_issue: true)
|
||||
project_with_issues_and_repo =
|
||||
create_legacy_license_public_project('project-with-issues-and-repo', repo_size: 1, with_issue: true)
|
||||
|
||||
queries = ActiveRecord::QueryRecorder.new { perform_migration }
|
||||
|
||||
expect(queries.count).to eq(7)
|
||||
expect(migrated_attribute(project_with_no_issues_no_repo)).to be_falsey
|
||||
expect(migrated_attribute(project_with_repo)).to be_truthy
|
||||
expect(migrated_attribute(project_with_issues)).to be_truthy
|
||||
expect(migrated_attribute(project_with_issues_and_repo)).to be_truthy
|
||||
end
|
||||
|
||||
def create_legacy_license_public_project(path, repo_size: 0, with_issue: false)
|
||||
namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}")
|
||||
project_namespace =
|
||||
namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project')
|
||||
project = projects_table
|
||||
.create!(
|
||||
name: path, path: path, namespace_id: namespace.id,
|
||||
project_namespace_id: project_namespace.id, visibility_level: 20
|
||||
)
|
||||
|
||||
project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: repo_size)
|
||||
issues_table.create!(project_id: project.id) if with_issue
|
||||
project_settings_table.create!(project_id: project.id, legacy_open_source_license_available: true)
|
||||
|
||||
project
|
||||
end
|
||||
|
||||
def migrated_attribute(project)
|
||||
project_settings_table.find(project.id).legacy_open_source_license_available
|
||||
end
|
||||
end
|
|
@ -25,7 +25,7 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
|
|||
it 'renders correct html' do
|
||||
expected_html = <<~EOS
|
||||
<div class="gl-form-checkbox custom-control custom-checkbox">
|
||||
<input name="user[view_diffs_file_by_file]" type="hidden" value="0" />
|
||||
<input name="user[view_diffs_file_by_file]" type="hidden" value="0" autocomplete="off" />
|
||||
<input class="custom-control-input" type="checkbox" value="1" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
|
||||
<label class="custom-control-label" for="user_view_diffs_file_by_file">
|
||||
<span>Show one file at a time on merge request's Changes tab</span>
|
||||
|
@ -51,7 +51,7 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
|
|||
it 'renders help text' do
|
||||
expected_html = <<~EOS
|
||||
<div class="gl-form-checkbox custom-control custom-checkbox">
|
||||
<input name="user[view_diffs_file_by_file]" type="hidden" value="1" />
|
||||
<input name="user[view_diffs_file_by_file]" type="hidden" value="1" autocomplete="off" />
|
||||
<input class="custom-control-input checkbox-foo-bar" type="checkbox" value="3" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
|
||||
<label class="custom-control-label label-foo-bar" for="user_view_diffs_file_by_file">
|
||||
<span>Show one file at a time on merge request's Changes tab</span>
|
||||
|
@ -101,7 +101,7 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
|
|||
it 'renders correct html' do
|
||||
expected_html = <<~EOS
|
||||
<div class="gl-form-checkbox custom-control custom-checkbox">
|
||||
<input name="user[view_diffs_file_by_file]" type="hidden" value="0" />
|
||||
<input name="user[view_diffs_file_by_file]" type="hidden" value="0" autocomplete="off" />
|
||||
<input class="custom-control-input" type="checkbox" value="1" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
|
||||
<label class="custom-control-label" for="user_view_diffs_file_by_file">
|
||||
<span>Show one file at a time on merge request's Changes tab</span>
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe ScheduleDisableLegacyOpenSourceLicenseForNoIssuesNoRepoProjects do
|
||||
context 'when on gitlab.com' do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules background jobs for each batch of projects' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to(
|
||||
have_scheduled_batched_migration(
|
||||
table_name: :projects,
|
||||
column_name: :id,
|
||||
interval: described_class::INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
max_batch_size: described_class::MAX_BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on self-managed instance' do
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(false)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'does not schedule background job' do
|
||||
expect(migration).not_to receive(:queue_batched_background_migration)
|
||||
|
||||
migration.up
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'does not delete background job' do
|
||||
expect(migration).not_to receive(:delete_batched_background_migration)
|
||||
|
||||
migration.down
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -47,4 +47,30 @@ RSpec.describe UserStatus do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#customized?' do
|
||||
it 'is customized when message text is present' do
|
||||
subject.message = 'My custom status'
|
||||
|
||||
expect(subject).to be_customized
|
||||
end
|
||||
|
||||
it 'is not customized when message text is absent' do
|
||||
subject.message = nil
|
||||
|
||||
expect(subject).not_to be_customized
|
||||
end
|
||||
|
||||
it 'is customized without message but with custom emoji' do
|
||||
subject.emoji = 'bow'
|
||||
|
||||
expect(subject).to be_customized
|
||||
end
|
||||
|
||||
it 'is not customized without message but with default custom emoji' do
|
||||
subject.emoji = 'speech_balloon'
|
||||
|
||||
expect(subject).not_to be_customized
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,9 +40,12 @@ RSpec.describe WorkItem do
|
|||
subject { build(:work_item).widgets }
|
||||
|
||||
it 'returns instances of supported widgets' do
|
||||
is_expected.to include(instance_of(WorkItems::Widgets::Description),
|
||||
instance_of(WorkItems::Widgets::Hierarchy),
|
||||
instance_of(WorkItems::Widgets::Assignees))
|
||||
is_expected.to include(
|
||||
instance_of(WorkItems::Widgets::Description),
|
||||
instance_of(WorkItems::Widgets::Hierarchy),
|
||||
instance_of(WorkItems::Widgets::Assignees),
|
||||
instance_of(WorkItems::Widgets::StartAndDueDate)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -64,9 +64,12 @@ RSpec.describe WorkItems::Type do
|
|||
subject { described_class.available_widgets }
|
||||
|
||||
it 'returns list of all possible widgets' do
|
||||
is_expected.to include(::WorkItems::Widgets::Description,
|
||||
::WorkItems::Widgets::Hierarchy,
|
||||
::WorkItems::Widgets::Assignees)
|
||||
is_expected.to include(
|
||||
::WorkItems::Widgets::Description,
|
||||
::WorkItems::Widgets::Hierarchy,
|
||||
::WorkItems::Widgets::Assignees,
|
||||
::WorkItems::Widgets::StartAndDueDate
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WorkItems::Widgets::StartAndDueDate do
|
||||
let_it_be(:work_item) { create(:work_item, start_date: Date.today, due_date: 1.week.from_now) }
|
||||
|
||||
describe '.type' do
|
||||
subject { described_class.type }
|
||||
|
||||
it { is_expected.to eq(:start_and_due_date) }
|
||||
end
|
||||
|
||||
describe '#type' do
|
||||
subject { described_class.new(work_item).type }
|
||||
|
||||
it { is_expected.to eq(:start_and_due_date) }
|
||||
end
|
||||
|
||||
describe '#start_date' do
|
||||
subject { described_class.new(work_item).start_date }
|
||||
|
||||
it { is_expected.to eq(work_item.start_date) }
|
||||
end
|
||||
|
||||
describe '#due_date' do
|
||||
subject { described_class.new(work_item).due_date }
|
||||
|
||||
it { is_expected.to eq(work_item.due_date) }
|
||||
end
|
||||
end
|
|
@ -8,7 +8,16 @@ RSpec.describe 'Query.work_item(id)' do
|
|||
let_it_be(:developer) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :private) }
|
||||
let_it_be(:work_item) { create(:work_item, project: project, description: '- List item') }
|
||||
let_it_be(:work_item) do
|
||||
create(
|
||||
:work_item,
|
||||
project: project,
|
||||
description: '- List item',
|
||||
start_date: Date.today,
|
||||
due_date: 1.week.from_now
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:child_item1) { create(:work_item, :task, project: project) }
|
||||
let_it_be(:child_item2) { create(:work_item, :task, confidential: true, project: project) }
|
||||
let_it_be(:child_link1) { create(:parent_link, work_item_parent: work_item, work_item: child_item1) }
|
||||
|
@ -205,6 +214,34 @@ RSpec.describe 'Query.work_item(id)' do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'start and due date widget' do
|
||||
let(:work_item_fields) do
|
||||
<<~GRAPHQL
|
||||
id
|
||||
widgets {
|
||||
type
|
||||
... on WorkItemWidgetStartAndDueDate {
|
||||
startDate
|
||||
dueDate
|
||||
}
|
||||
}
|
||||
GRAPHQL
|
||||
end
|
||||
|
||||
it 'returns widget information' do
|
||||
expect(work_item_data).to include(
|
||||
'id' => work_item.to_gid.to_s,
|
||||
'widgets' => include(
|
||||
hash_including(
|
||||
'type' => 'START_AND_DUE_DATE',
|
||||
'startDate' => work_item.start_date.to_s,
|
||||
'dueDate' => work_item.due_date.to_s
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an Issue Global ID is provided' do
|
||||
|
|
|
@ -189,8 +189,10 @@ module MarkdownMatchers
|
|||
|
||||
match do |actual|
|
||||
expect(actual).to have_selector('ul.task-list', count: 2)
|
||||
expect(actual).to have_selector('li.task-list-item', count: 7)
|
||||
expect(actual).to have_selector('li.task-list-item', count: 9)
|
||||
expect(actual).to have_selector('li.task-list-item.inapplicable > s', count: 2)
|
||||
expect(actual).to have_selector('input[checked]', count: 3)
|
||||
expect(actual).to have_selector('input[data-inapplicable]', count: 2)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@ RSpec.shared_examples 'wiki file attachments' do
|
|||
end
|
||||
|
||||
context 'before uploading' do
|
||||
it 'shows "Attach a file" button' do
|
||||
expect(page).to have_button('Attach a file')
|
||||
it 'shows "Attach a file or image" button' do
|
||||
expect(page).to have_selector('[data-testid="button-attach-file"]')
|
||||
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
|
||||
end
|
||||
end
|
||||
|
@ -26,7 +26,7 @@ RSpec.shared_examples 'wiki file attachments' do
|
|||
click_button 'Cancel'
|
||||
end
|
||||
|
||||
expect(page).to have_button('Attach a file')
|
||||
expect(page).to have_selector('[data-testid="button-attach-file"]')
|
||||
expect(page).not_to have_button('Cancel')
|
||||
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
|
||||
end
|
||||
|
@ -41,11 +41,11 @@ RSpec.shared_examples 'wiki file attachments' do
|
|||
end
|
||||
|
||||
context 'uploading is complete' do
|
||||
it 'shows "Attach a file" button on uploading complete' do
|
||||
it 'shows "Attach a file or image" button on uploading complete' do
|
||||
attach_with_dropzone
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_button('Attach a file')
|
||||
expect(page).to have_selector('[data-testid="button-attach-file"]')
|
||||
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
|
||||
end
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ RSpec.describe ProjectCacheWorker do
|
|||
.twice
|
||||
|
||||
expect(UpdateProjectStatisticsWorker).to receive(:perform_in)
|
||||
.with(lease_timeout, project.id, statistics)
|
||||
.with(lease_timeout, lease_key, project.id, statistics)
|
||||
.and_call_original
|
||||
|
||||
expect(Namespaces::ScheduleAggregationWorker)
|
||||
|
|
|
@ -3,17 +3,35 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe UpdateProjectStatisticsWorker do
|
||||
include ExclusiveLeaseHelpers
|
||||
|
||||
let(:worker) { described_class.new }
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:statistics) { %w(repository_size) }
|
||||
let(:lease_key) { "namespace:namespaces_root_statistics:#{project.namespace_id}" }
|
||||
|
||||
describe '#perform' do
|
||||
it 'updates the project statistics' do
|
||||
expect(Projects::UpdateStatisticsService).to receive(:new)
|
||||
.with(project, nil, statistics: statistics)
|
||||
.and_call_original
|
||||
context 'when a lease could be obtained' do
|
||||
it 'updates the project statistics' do
|
||||
expect(Projects::UpdateStatisticsService).to receive(:new)
|
||||
.with(project, nil, statistics: statistics)
|
||||
.and_call_original
|
||||
|
||||
worker.perform(project.id, statistics)
|
||||
worker.perform(lease_key, project.id, statistics)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a lease could not be obtained' do
|
||||
before do
|
||||
stub_exclusive_lease_taken(lease_key, timeout: ProjectCacheWorker::LEASE_TIMEOUT)
|
||||
end
|
||||
|
||||
it 'does not update the project statistics' do
|
||||
lease_key = "namespace:namespaces_root_statistics:#{project.namespace_id}"
|
||||
expect(Projects::UpdateStatisticsService).not_to receive(:new)
|
||||
|
||||
worker.perform(lease_key, project.id, statistics)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue