Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock112
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/strike.js29
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js44
-rw-r--r--app/assets/javascripts/dropzone_input.js17
-rw-r--r--app/assets/javascripts/environments/components/environments_detail_header.vue1
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json1
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue2
-rw-r--r--app/assets/javascripts/lib/dompurify.js35
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js4
-rw-r--r--app/assets/javascripts/task_list.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue2
-rw-r--r--app/assets/stylesheets/framework/typography.scss29
-rw-r--r--app/graphql/types/work_items/widget_interface.rb5
-rw-r--r--app/graphql/types/work_items/widgets/start_and_due_date_type.rb23
-rw-r--r--app/helpers/gitlab_script_tag_helper.rb4
-rw-r--r--app/helpers/issuables_helper.rb2
-rw-r--r--app/helpers/markup_helper.rb3
-rw-r--r--app/helpers/profiles_helper.rb4
-rw-r--r--app/helpers/users_helper.rb6
-rw-r--r--app/models/user_status.rb4
-rw-r--r--app/models/work_items/type.rb4
-rw-r--r--app/models/work_items/widgets/start_and_due_date.rb9
-rw-r--r--app/serializers/concerns/user_status_tooltip.rb2
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml2
-rw-r--r--app/views/layouts/header/_current_user_dropdown_item.html.haml4
-rw-r--r--app/views/profiles/show.html.haml2
-rw-r--r--app/views/shared/blob/_markdown_buttons.html.haml4
-rw-r--r--app/views/shared/notes/_hints.html.haml9
-rw-r--r--app/views/users/show.html.haml4
-rw-r--r--app/workers/project_cache_worker.rb3
-rw-r--r--app/workers/update_project_statistics_worker.rb13
-rw-r--r--config/application.rb15
-rw-r--r--config/initializers/rails_safe_load_yaml_patch.rb32
-rw-r--r--db/post_migrate/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects.rb33
-rw-r--r--db/schema_migrations/202207220845431
-rw-r--r--doc/api/graphql/reference/index.md16
-rw-r--r--doc/ci/pipelines/merge_request_pipelines.md2
-rw-r--r--doc/development/documentation/styleguide/index.md10
-rw-r--r--doc/subscriptions/gitlab_com/index.md4
-rw-r--r--doc/user/img/completed_tasks_v13_3.pngbin14835 -> 0 bytes
-rw-r--r--doc/user/img/completed_tasks_v15_3.pngbin0 -> 55931 bytes
-rw-r--r--doc/user/markdown.md18
-rw-r--r--glfm_specification/example_snapshots/examples_index.yml12
-rw-r--r--glfm_specification/example_snapshots/html.yml72
-rw-r--r--glfm_specification/example_snapshots/markdown.yml10
-rw-r--r--glfm_specification/example_snapshots/prosemirror_json.yml70
-rw-r--r--glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt82
-rw-r--r--glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml12
-rw-r--r--glfm_specification/output/spec.txt82
-rw-r--r--lib/banzai/filter/task_list_filter.rb86
-rw-r--r--lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects.rb36
-rw-r--r--locale/gitlab.pot2
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb2
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb19
-rw-r--r--spec/features/projects/tags/user_edits_tags_spec.rb4
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb11
-rw-r--r--spec/fixtures/markdown.md.erb2
-rw-r--r--spec/frontend/environments/environments_detail_header_spec.js12
-rw-r--r--spec/frontend/lib/dompurify_spec.js46
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js1
-rw-r--r--spec/graphql/types/work_items/widgets/start_and_due_date_type_spec.rb11
-rw-r--r--spec/helpers/gitlab_script_tag_helper_spec.rb10
-rw-r--r--spec/helpers/profiles_helper_spec.rb32
-rw-r--r--spec/initializers/rails_safe_load_yaml_patch_spec.rb66
-rw-r--r--spec/lib/banzai/filter/task_list_filter_spec.rb34
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb62
-rw-r--r--spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb6
-rw-r--r--spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb64
-rw-r--r--spec/models/user_status_spec.rb26
-rw-r--r--spec/models/work_item_spec.rb9
-rw-r--r--spec/models/work_items/type_spec.rb9
-rw-r--r--spec/models/work_items/widgets/start_and_due_date_spec.rb31
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb39
-rw-r--r--spec/support/matchers/markdown_matchers.rb4
-rw-r--r--spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb10
-rw-r--r--spec/workers/project_cache_worker_spec.rb2
-rw-r--r--spec/workers/update_project_statistics_worker_spec.rb28
82 files changed, 1362 insertions, 205 deletions
diff --git a/Gemfile b/Gemfile
index a66995eab29..991d84d582e 100644
--- a/Gemfile
+++ b/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
diff --git a/Gemfile.lock b/Gemfile.lock
index eba19ba6218..d8c3b881ac2 100644
--- a/Gemfile.lock
+++ b/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)
diff --git a/app/assets/javascripts/behaviors/markdown/marks/strike.js b/app/assets/javascripts/behaviors/markdown/marks/strike.js
index 967c0a120cd..afab266b645 100644
--- a/app/assets/javascripts/behaviors/markdown/marks/strike.js
+++ b/app/assets/javascripts/behaviors/markdown/marks/strike.js
@@ -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,
},
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
index 10ffce9b1b8..095634340c1 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
@@ -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);
},
});
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 491c2ced358..e6f7a31e07b 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -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;
}
diff --git a/app/assets/javascripts/environments/components/environments_detail_header.vue b/app/assets/javascripts/environments/components/environments_detail_header.vue
index 13b9cf14f52..bd67908a6b4 100644
--- a/app/assets/javascripts/environments/components/environments_detail_header.vue
+++ b/app/assets/javascripts/environments/components/environments_detail_header.vue
@@ -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"
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 45c5cca68cc..6f821935f44 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -134,6 +134,7 @@
"WorkItemWidgetAssignees",
"WorkItemWidgetDescription",
"WorkItemWidgetHierarchy",
+ "WorkItemWidgetStartAndDueDate",
"WorkItemWidgetWeight"
]
}
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 449da394841..3a93bccee88 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -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');
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index a01c6df0003..af7a1201889 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -33,6 +33,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 });
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 243de48948c..262cf024ee3 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -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();
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index 79a30340856..6e72d95c8e6 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -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() {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 4fdf7f45643..46cd35aa0b7 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -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),
@@ -325,6 +333,14 @@ export default {
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"
:prepend="true"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 6c99a749edc..aa325862f06 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -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>
@@ -114,14 +114,6 @@ export default {
</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"
class="button-cancel-uploading-files gl-vertical-align-baseline hide"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 6a83939795c..49217e38a1b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -88,6 +88,6 @@ export default {
category="tertiary"
class="js-md"
data-container="body"
- @click="() => $emit('click')"
+ @click="$emit('click', $event)"
/>
</template>
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index b5e0dcd875a..031f5dc45ca 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -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/'] {
diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb
index 3f4758a6334..aa9c4e3722a 100644
--- a/app/graphql/types/work_items/widget_interface.rb
+++ b/app/graphql/types/work_items/widget_interface.rb
@@ -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
diff --git a/app/graphql/types/work_items/widgets/start_and_due_date_type.rb b/app/graphql/types/work_items/widgets/start_and_due_date_type.rb
new file mode 100644
index 00000000000..553c06a0b7f
--- /dev/null
+++ b/app/graphql/types/work_items/widgets/start_and_due_date_type.rb
@@ -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
diff --git a/app/helpers/gitlab_script_tag_helper.rb b/app/helpers/gitlab_script_tag_helper.rb
index f784bb69dd8..55653c592e5 100644
--- a/app/helpers/gitlab_script_tag_helper.rb
+++ b/app/helpers/gitlab_script_tag_helper.rb
@@ -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
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 486d5bb3866..069e7316ac1 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -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
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 6077a059f6f..75f092d083d 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -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
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index 20d0dd9b30c..104026ff21e 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -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
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 4ea2512bc67..46006f7f098 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -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
diff --git a/app/models/user_status.rb b/app/models/user_status.rb
index 7a803e8f1f6..cb6f4dd9dae 100644
--- a/app/models/user_status.rb
+++ b/app/models/user_status.rb
@@ -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')
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index 7c4da00479c..af0462a9d8f 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -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
diff --git a/app/models/work_items/widgets/start_and_due_date.rb b/app/models/work_items/widgets/start_and_due_date.rb
new file mode 100644
index 00000000000..0b828c5b5a9
--- /dev/null
+++ b/app/models/work_items/widgets/start_and_due_date.rb
@@ -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
diff --git a/app/serializers/concerns/user_status_tooltip.rb b/app/serializers/concerns/user_status_tooltip.rb
index ca2854224a7..38b3c16dd2a 100644
--- a/app/serializers/concerns/user_status_tooltip.rb
+++ b/app/serializers/concerns/user_status_tooltip.rb
@@ -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|
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 11dd8ba6c08..353f07c07c5 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -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')
diff --git a/app/views/layouts/header/_current_user_dropdown_item.html.haml b/app/views/layouts/header/_current_user_dropdown_item.html.haml
index 06c597b4932..3fded43ee4f 100644
--- a/app/views/layouts/header/_current_user_dropdown_item.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown_item.html.haml
@@ -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
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index dda1640968e..a64968cdcbb 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -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
diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml
index 60641006e96..f7f4115bed2 100644
--- a/app/views/shared/blob/_markdown_buttons.html.haml
+++ b/app/views/shared/blob/_markdown_buttons.html.haml
@@ -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")
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index c845d4df7df..44740db5a00 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -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")
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index e2f7a88569a..5c82b22fc0e 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -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)
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 0e90b41e28d..cb1a7c8560a 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -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
diff --git a/app/workers/update_project_statistics_worker.rb b/app/workers/update_project_statistics_worker.rb
index 45a6cc8f476..3308fa149f5 100644
--- a/app/workers/update_project_statistics_worker.rb
+++ b/app/workers/update_project_statistics_worker.rb
@@ -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
diff --git a/config/application.rb b/config/application.rb
index b758f2df857..6745c186140 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -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
diff --git a/config/initializers/rails_safe_load_yaml_patch.rb b/config/initializers/rails_safe_load_yaml_patch.rb
new file mode 100644
index 00000000000..dcc0426673a
--- /dev/null
+++ b/config/initializers/rails_safe_load_yaml_patch.rb
@@ -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
diff --git a/db/post_migrate/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects.rb b/db/post_migrate/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects.rb
new file mode 100644
index 00000000000..7fbf09846cf
--- /dev/null
+++ b/db/post_migrate/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects.rb
@@ -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
diff --git a/db/schema_migrations/20220722084543 b/db/schema_migrations/20220722084543
new file mode 100644
index 00000000000..44d94a312b8
--- /dev/null
+++ b/db/schema_migrations/20220722084543
@@ -0,0 +1 @@
+b189304b940d01a527bba4ad8b0865ae44de1e3af2ef1b711d95993821106b6b \ No newline at end of file
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 2cd7c5ebd00..23dc964687a 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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. |
diff --git a/doc/ci/pipelines/merge_request_pipelines.md b/doc/ci/pipelines/merge_request_pipelines.md
index 89839de718b..b5c00071abc 100644
--- a/doc/ci/pipelines/merge_request_pipelines.md
+++ b/doc/ci/pipelines/merge_request_pipelines.md
@@ -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
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 1af0cb72055..11625e466ff 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -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.
diff --git a/doc/subscriptions/gitlab_com/index.md b/doc/subscriptions/gitlab_com/index.md
index da84cc6211e..52d6d4f7aee 100644
--- a/doc/subscriptions/gitlab_com/index.md
+++ b/doc/subscriptions/gitlab_com/index.md
@@ -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.
diff --git a/doc/user/img/completed_tasks_v13_3.png b/doc/user/img/completed_tasks_v13_3.png
deleted file mode 100644
index b12d95f0a23..00000000000
--- a/doc/user/img/completed_tasks_v13_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/img/completed_tasks_v15_3.png b/doc/user/img/completed_tasks_v15_3.png
new file mode 100644
index 00000000000..09174c688da
--- /dev/null
+++ b/doc/user/img/completed_tasks_v15_3.png
Binary files differ
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index b089c84a784..6a524fe206a 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -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
diff --git a/glfm_specification/example_snapshots/examples_index.yml b/glfm_specification/example_snapshots/examples_index.yml
index 9b601460b1d..a6668173cc9 100644
--- a/glfm_specification/example_snapshots/examples_index.yml
+++ b/glfm_specification/example_snapshots/examples_index.yml
@@ -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
diff --git a/glfm_specification/example_snapshots/html.yml b/glfm_specification/example_snapshots/html.yml
index 376a4bc72ca..834dd49a934 100644
--- a/glfm_specification/example_snapshots/html.yml
+++ b/glfm_specification/example_snapshots/html.yml
@@ -7588,3 +7588,75 @@
wysiwyg: |-
<p>footnote reference tag <sup identifier="fortytwo">fortytwo</sup></p>
<div node="footnoteDefinition(paragraph(&quot;footnote text&quot;))" 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>
diff --git a/glfm_specification/example_snapshots/markdown.yml b/glfm_specification/example_snapshots/markdown.yml
index c4c30dcb513..ffbc7d77ce5 100644
--- a/glfm_specification/example_snapshots/markdown.yml
+++ b/glfm_specification/example_snapshots/markdown.yml
@@ -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
diff --git a/glfm_specification/example_snapshots/prosemirror_json.yml b/glfm_specification/example_snapshots/prosemirror_json.yml
index 0b468945042..f770d341c42 100644
--- a/glfm_specification/example_snapshots/prosemirror_json.yml
+++ b/glfm_specification/example_snapshots/prosemirror_json.yml
@@ -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
diff --git a/glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt b/glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt
index d0d450b66bf..a50c28c9296 100644
--- a/glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt
+++ b/glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt
@@ -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>
+````````````````````````````````
diff --git a/glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml b/glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml
index b09a092c02a..3881819e38a 100644
--- a/glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml
+++ b/glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml
@@ -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
diff --git a/glfm_specification/output/spec.txt b/glfm_specification/output/spec.txt
index 3fc27efdc34..b2735219d02 100644
--- a/glfm_specification/output/spec.txt
+++ b/glfm_specification/output/spec.txt
@@ -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
diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb
index 896f67cb875..e8a7677b102 100644
--- a/lib/banzai/filter/task_list_filter.rb
+++ b/lib/banzai/filter/task_list_filter.rb
@@ -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
diff --git a/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects.rb b/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects.rb
new file mode 100644
index 00000000000..019c3d15b3e
--- /dev/null
+++ b/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9b7a73ba023..1cf6a679287 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5233,7 +5233,7 @@ msgstr ""
msgid "At risk"
msgstr ""
-msgid "Attach a file"
+msgid "Attach a file or image"
msgstr ""
msgid "Attaching File - %{progress}"
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index 151d3c60fa2..f2be85a4d0e 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -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
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index d472134a2c7..b5bf9279371 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -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,
diff --git a/spec/features/projects/tags/user_edits_tags_spec.rb b/spec/features/projects/tags/user_edits_tags_spec.rb
index c8438b73dc3..af7ffa2c1ec 100644
--- a/spec/features/projects/tags/user_edits_tags_spec.rb
+++ b/spec/features/projects/tags/user_edits_tags_spec.rb
@@ -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
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
index 589cc9f9b02..2547e2d274c 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -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
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 2da16408fbc..18cd63b7bcb 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -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
diff --git a/spec/frontend/environments/environments_detail_header_spec.js b/spec/frontend/environments/environments_detail_header_spec.js
index 305e7385b43..4687119127d 100644
--- a/spec/frontend/environments/environments_detail_header_spec.js
+++ b/spec/frontend/environments/environments_detail_header_spec.js
@@ -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', () => {
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js
index b585c69e911..29b927ef628 100644
--- a/spec/frontend/lib/dompurify_spec.js
+++ b/spec/frontend/lib/dompurify_spec.js
@@ -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);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index d1bca3c73b6..8f37c79e235 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -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'}
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 85a135d2b89..50864a4bf25 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -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();
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 67222cab247..12972fff58e 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -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();
diff --git a/spec/graphql/types/work_items/widgets/start_and_due_date_type_spec.rb b/spec/graphql/types/work_items/widgets/start_and_due_date_type_spec.rb
new file mode 100644
index 00000000000..ddc26d964be
--- /dev/null
+++ b/spec/graphql/types/work_items/widgets/start_and_due_date_type_spec.rb
@@ -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
diff --git a/spec/helpers/gitlab_script_tag_helper_spec.rb b/spec/helpers/gitlab_script_tag_helper_spec.rb
index 35f2c0795be..9d71e25286e 100644
--- a/spec/helpers/gitlab_script_tag_helper_spec.rb
+++ b/spec/helpers/gitlab_script_tag_helper_spec.rb
@@ -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
diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb
index 399726263db..63641e65942 100644
--- a/spec/helpers/profiles_helper_spec.rb
+++ b/spec/helpers/profiles_helper_spec.rb
@@ -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
diff --git a/spec/initializers/rails_safe_load_yaml_patch_spec.rb b/spec/initializers/rails_safe_load_yaml_patch_spec.rb
new file mode 100644
index 00000000000..2637bb167b6
--- /dev/null
+++ b/spec/initializers/rails_safe_load_yaml_patch_spec.rb
@@ -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
diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb
index c89acd1a643..920904b0f29 100644
--- a/spec/lib/banzai/filter/task_list_filter_spec.rb
+++ b/spec/lib/banzai/filter/task_list_filter_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
new file mode 100644
index 00000000000..d20eaef3650
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
index 2b1fcac9257..a7b195a16b4 100644
--- a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
+++ b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
@@ -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&#39;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&#39;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&#39;s Changes tab</span>
diff --git a/spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb b/spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
new file mode 100644
index 00000000000..cb0f941aea1
--- /dev/null
+++ b/spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
@@ -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
diff --git a/spec/models/user_status_spec.rb b/spec/models/user_status_spec.rb
index 87d1fa14aca..663df9712ab 100644
--- a/spec/models/user_status_spec.rb
+++ b/spec/models/user_status_spec.rb
@@ -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
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 777ade511b0..e281a2a8fff 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -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
diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb
index 057bf045f60..ec0b5536546 100644
--- a/spec/models/work_items/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -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
diff --git a/spec/models/work_items/widgets/start_and_due_date_spec.rb b/spec/models/work_items/widgets/start_and_due_date_spec.rb
new file mode 100644
index 00000000000..b023cc73e0f
--- /dev/null
+++ b/spec/models/work_items/widgets/start_and_due_date_spec.rb
@@ -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
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index e9753affcc3..b4f4cb68350 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -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
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 1932f78506f..8bec3be2535 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -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
diff --git a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb
index 0ef1ccdfe57..8d1502bed84 100644
--- a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb
@@ -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
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 7f42c700ce4..30c85464452 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -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)
diff --git a/spec/workers/update_project_statistics_worker_spec.rb b/spec/workers/update_project_statistics_worker_spec.rb
index 1f840e363ea..2f356376d7c 100644
--- a/spec/workers/update_project_statistics_worker_spec.rb
+++ b/spec/workers/update_project_statistics_worker_spec.rb
@@ -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(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(project.id, statistics)
+ worker.perform(lease_key, project.id, statistics)
+ end
end
end
end