diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-26 09:09:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-26 09:09:02 +0000 |
commit | d79bf171e4bcbb551a8320211ee337368b4d114c (patch) | |
tree | 63f4305ee043eb6ca4b6f2113d8067b388b68ed8 | |
parent | 6723a4288d29b11beec2de92fb7cfd682c9dcc50 (diff) | |
download | gitlab-ce-d79bf171e4bcbb551a8320211ee337368b4d114c.tar.gz |
Add latest changes from gitlab-org/gitlab@master
82 files changed, 1362 insertions, 205 deletions
@@ -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 Binary files differdeleted file mode 100644 index b12d95f0a23..00000000000 --- a/doc/user/img/completed_tasks_v13_3.png +++ /dev/null diff --git a/doc/user/img/completed_tasks_v15_3.png b/doc/user/img/completed_tasks_v15_3.png Binary files differnew file mode 100644 index 00000000000..09174c688da --- /dev/null +++ b/doc/user/img/completed_tasks_v15_3.png 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 ``` - + ### 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("footnote text"))" htmlattributes="[object Object]"><p>footnote text</p></div> +07_02__gitlab_specific_markdown__task_list_items__001: + canonical: | + <ul> + <li> + <task-button/> + <input type="checkbox" disabled/> + incomplete + </li> + </ul> + static: |- + <ul data-sourcepos="1:1-1:16" class="task-list" dir="auto"> + <li data-sourcepos="1:1-1:16" class="task-list-item"> + <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" disabled> incomplete</li> + </ul> + wysiwyg: |- + <ul start="1" parens="false" data-type="taskList"><li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p>incomplete</p></div></li></ul> +07_02__gitlab_specific_markdown__task_list_items__002: + canonical: | + <ul> + <li> + <task-button/> + <input type="checkbox" checked disabled/> + completed + </li> + </ul> + static: |- + <ul data-sourcepos="1:1-1:15" class="task-list" dir="auto"> + <li data-sourcepos="1:1-1:15" class="task-list-item"> + <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" checked disabled> completed</li> + </ul> + wysiwyg: |- + <ul start="1" parens="false" data-type="taskList"><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p>completed</p></div></li></ul> +07_02__gitlab_specific_markdown__task_list_items__003: + canonical: | + <ul> + <li> + <task-button/> + <input type="checkbox" data-inapplicable disabled> + <s> + inapplicable + </s> + </li> + </ul> + static: |- + <ul data-sourcepos="1:1-1:18" class="task-list" dir="auto"> + <li data-sourcepos="1:1-1:18" class="task-list-item inapplicable"> + <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" data-inapplicable disabled> <s>inapplicable</s> + </li> + </ul> +07_02__gitlab_specific_markdown__task_list_items__004: + canonical: | + <ul> + <li> + <p> + <task-button/> + <input type="checkbox" data-inapplicable disabled> + <s> + inapplicable + </s> + </p> + <p> + text in loose list + </p> + </li> + </ul> + static: |- + <ul data-sourcepos="1:1-3:20" class="task-list" dir="auto"> + <li data-sourcepos="1:1-3:20" class="task-list-item inapplicable"> + <p data-sourcepos="1:3-1:18"><task-button></task-button><input type="checkbox" class="task-list-item-checkbox" data-inapplicable disabled> <s>inapplicable</s></p> + <p data-sourcepos="3:3-3:20">text in loose list</p> + </li> + </ul> 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's Changes tab</span> @@ -51,7 +51,7 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do it 'renders help text' do expected_html = <<~EOS <div class="gl-form-checkbox custom-control custom-checkbox"> - <input name="user[view_diffs_file_by_file]" type="hidden" value="1" /> + <input name="user[view_diffs_file_by_file]" type="hidden" value="1" autocomplete="off" /> <input class="custom-control-input checkbox-foo-bar" type="checkbox" value="3" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" /> <label class="custom-control-label label-foo-bar" for="user_view_diffs_file_by_file"> <span>Show one file at a time on merge request's Changes tab</span> @@ -101,7 +101,7 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do it 'renders correct html' do expected_html = <<~EOS <div class="gl-form-checkbox custom-control custom-checkbox"> - <input name="user[view_diffs_file_by_file]" type="hidden" value="0" /> + <input name="user[view_diffs_file_by_file]" type="hidden" value="0" autocomplete="off" /> <input class="custom-control-input" type="checkbox" value="1" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" /> <label class="custom-control-label" for="user_view_diffs_file_by_file"> <span>Show one file at a time on merge request's Changes tab</span> 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 |