Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-26 09:09:02 +00:00
parent 6723a4288d
commit d79bf171e4
82 changed files with 1362 additions and 205 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -134,6 +134,7 @@
"WorkItemWidgetAssignees",
"WorkItemWidgetDescription",
"WorkItemWidgetHierarchy",
"WorkItemWidgetStartAndDueDate",
"WorkItemWidgetWeight"
]
}

View File

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

View File

@ -32,6 +32,22 @@ const removeUnsafeHref = (node, attr) => {
}
};
/**
* Appends 'noopener' & 'noreferrer' to rel
* attr values to prevent reverse tabnabbing.
*
* @param {String} rel
* @returns {String}
*/
const appendSecureRelValue = (rel) => {
const attributes = new Set(rel ? rel.toLowerCase().split(' ') : []);
attributes.add('noopener');
attributes.add('noreferrer');
return Array.from(attributes).join(' ');
};
/**
* Sanitize icons' <use> tag attributes, to safely include
* svgs such as in:
@ -57,4 +73,23 @@ addHook('afterSanitizeAttributes', (node) => {
}
});
const TEMPORARY_ATTRIBUTE = 'data-temp-href-target';
addHook('beforeSanitizeAttributes', (node) => {
if (node.tagName === 'A' && node.hasAttribute('target')) {
node.setAttribute(TEMPORARY_ATTRIBUTE, node.getAttribute('target'));
}
});
addHook('afterSanitizeAttributes', (node) => {
if (node.tagName === 'A' && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
node.setAttribute('target', node.getAttribute(TEMPORARY_ATTRIBUTE));
node.removeAttribute(TEMPORARY_ATTRIBUTE);
if (node.getAttribute('target') === '_blank') {
const rel = node.getAttribute('rel');
node.setAttribute('rel', appendSecureRelValue(rel));
}
}
});
export const sanitize = (val, config) => dompurifySanitize(val, { ...defaultConfig, ...config });

View File

@ -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();

View File

@ -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() {

View File

@ -156,6 +156,14 @@ export default {
})
.catch(() => {});
},
handleAttachFile(e) {
e.preventDefault();
const $gfmForm = $(this.$el).closest('.gfm-form');
const $gfmTextarea = $gfmForm.find('.js-gfm-input');
$gfmForm.find('.div-dropzone').click();
$gfmTextarea.focus();
},
},
shortcuts: {
bold: keysFor(BOLD_TEXT),
@ -324,6 +332,14 @@ export default {
:button-title="__('Add a table')"
icon="table"
/>
<toolbar-button
v-if="!restrictedToolBarItems.includes('attach-file')"
data-testid="button-attach-file"
:prepend="true"
:button-title="__('Attach a file or image')"
icon="paperclip"
@click="handleAttachFile"
/>
<toolbar-button
v-if="!restrictedToolBarItems.includes('full-screen')"
class="js-zen-enter"

View File

@ -74,7 +74,7 @@ export default {
</div>
<span v-if="canAttachFile" class="uploading-container">
<span class="uploading-progress-container hide">
<gl-icon name="media" />
<gl-icon name="paperclip" />
<span class="attaching-file-message"></span>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<span class="uploading-progress">0%</span>
@ -82,7 +82,7 @@ export default {
</span>
<span class="uploading-error-container hide">
<span class="uploading-error-icon">
<gl-icon name="media" />
<gl-icon name="paperclip" />
</span>
<span class="uploading-error-message"></span>
@ -113,14 +113,6 @@ export default {
</template>
</gl-sprintf>
</span>
<gl-button
icon="media"
variant="link"
category="primary"
class="markdown-selector button-attach-file gl-vertical-align-text-bottom"
>
{{ __('Attach a file') }}
</gl-button>
<gl-button
variant="link"
category="primary"

View File

@ -88,6 +88,6 @@ export default {
category="tertiary"
class="js-md"
data-container="body"
@click="() => $emit('click')"
@click="$emit('click', $event)"
/>
</template>

View File

@ -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/'] {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
b189304b940d01a527bba4ad8b0865ae44de1e3af2ef1b711d95993821106b6b

View File

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

View File

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

View File

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

View File

@ -33,10 +33,10 @@ To subscribe to GitLab SaaS:
and decide which tier you want.
1. Create a user account for yourself by using the
[sign up page](https://gitlab.com/users/sign_up).
1. Create a [group](../../user/group/index.md#create-a-group). Your license tier applies to the top-level group, its subgroups, and projects.
1. Create a [group](../../user/group/index.md#create-a-group). Your subscription tier applies to the top-level group, its subgroups, and projects.
1. Create additional users and
[add them to the group](../../user/group/index.md#add-users-to-a-group). The users in this group, its subgroups, and projects can use
the features of your license tier, and they consume a seat in your subscription.
the features of your subscription tier, and they consume a seat in your subscription.
1. On the left sidebar, select **Billing** and choose a tier.
1. Fill out the form to complete your purchase.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -376,6 +376,8 @@ the [Asciidoctor user manual](https://asciidoctor.org/docs/user-manual/#activati
### Task lists
> Inapplicable checkboxes [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85982) in GitLab 15.3.
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#task-lists).
You can add task lists anywhere Markdown is supported.
@ -384,22 +386,28 @@ You can add task lists anywhere Markdown is supported.
- In all other places, you cannot select the boxes. You must edit the Markdown manually
by adding or removing an `x` in the brackets.
Besides complete and incomplete, tasks can also be **inapplicable**. Selecting an inapplicable checkbox
in an issue, merge request, or comment has no effect.
To create a task list, follow the format of an ordered or unordered list:
```markdown
- [x] Completed task
- [~] Inapplicable task
- [ ] Incomplete task
- [ ] Sub-task 1
- [x] Sub-task 2
- [x] Sub-task 1
- [~] Sub-task 2
- [ ] Sub-task 3
1. [x] Completed task
1. [~] Inapplicable task
1. [ ] Incomplete task
1. [ ] Sub-task 1
1. [x] Sub-task 2
1. [x] Sub-task 1
1. [~] Sub-task 2
1. [ ] Sub-task 3
```
![Task list as rendered by GitLab](img/completed_tasks_v13_3.png)
![Task list as rendered by GitLab](img/completed_tasks_v15_3.png)
### Table of contents

View File

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

View File

@ -7588,3 +7588,75 @@
wysiwyg: |-
<p>footnote reference tag <sup identifier="fortytwo">fortytwo</sup></p>
<div node="footnoteDefinition(paragraph(&quot;footnote text&quot;))" htmlattributes="[object Object]"><p>footnote text</p></div>
07_02__gitlab_specific_markdown__task_list_items__001:
canonical: |
<ul>
<li>
<task-button/>
<input type="checkbox" disabled/>
incomplete
</li>
</ul>
static: |-
<ul data-sourcepos="1:1-1:16" class="task-list" dir="auto">
<li data-sourcepos="1:1-1:16" class="task-list-item">
<task-button></task-button><input type="checkbox" class="task-list-item-checkbox" disabled> incomplete</li>
</ul>
wysiwyg: |-
<ul start="1" parens="false" data-type="taskList"><li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p>incomplete</p></div></li></ul>
07_02__gitlab_specific_markdown__task_list_items__002:
canonical: |
<ul>
<li>
<task-button/>
<input type="checkbox" checked disabled/>
completed
</li>
</ul>
static: |-
<ul data-sourcepos="1:1-1:15" class="task-list" dir="auto">
<li data-sourcepos="1:1-1:15" class="task-list-item">
<task-button></task-button><input type="checkbox" class="task-list-item-checkbox" checked disabled> completed</li>
</ul>
wysiwyg: |-
<ul start="1" parens="false" data-type="taskList"><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p>completed</p></div></li></ul>
07_02__gitlab_specific_markdown__task_list_items__003:
canonical: |
<ul>
<li>
<task-button/>
<input type="checkbox" data-inapplicable disabled>
<s>
inapplicable
</s>
</li>
</ul>
static: |-
<ul data-sourcepos="1:1-1:18" class="task-list" dir="auto">
<li data-sourcepos="1:1-1:18" class="task-list-item inapplicable">
<task-button></task-button><input type="checkbox" class="task-list-item-checkbox" data-inapplicable disabled> <s>inapplicable</s>
</li>
</ul>
07_02__gitlab_specific_markdown__task_list_items__004:
canonical: |
<ul>
<li>
<p>
<task-button/>
<input type="checkbox" data-inapplicable disabled>
<s>
inapplicable
</s>
</p>
<p>
text in loose list
</p>
</li>
</ul>
static: |-
<ul data-sourcepos="1:1-3:20" class="task-list" dir="auto">
<li data-sourcepos="1:1-3:20" class="task-list-item inapplicable">
<p data-sourcepos="1:3-1:18"><task-button></task-button><input type="checkbox" class="task-list-item-checkbox" data-inapplicable disabled> <s>inapplicable</s></p>
<p data-sourcepos="3:3-3:20">text in loose list</p>
</li>
</ul>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5233,7 +5233,7 @@ msgstr ""
msgid "At risk"
msgstr ""
msgid "Attach a file"
msgid "Attach a file or image"
msgstr ""
msgid "Attaching File - %{progress}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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', () => {

View File

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

View File

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

View File

@ -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();

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
it 'renders correct html' do
expected_html = <<~EOS
<div class="gl-form-checkbox custom-control custom-checkbox">
<input name="user[view_diffs_file_by_file]" type="hidden" value="0" />
<input name="user[view_diffs_file_by_file]" type="hidden" value="0" autocomplete="off" />
<input class="custom-control-input" type="checkbox" value="1" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
<label class="custom-control-label" for="user_view_diffs_file_by_file">
<span>Show one file at a time on merge request&#39;s Changes tab</span>
@ -51,7 +51,7 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
it 'renders help text' do
expected_html = <<~EOS
<div class="gl-form-checkbox custom-control custom-checkbox">
<input name="user[view_diffs_file_by_file]" type="hidden" value="1" />
<input name="user[view_diffs_file_by_file]" type="hidden" value="1" autocomplete="off" />
<input class="custom-control-input checkbox-foo-bar" type="checkbox" value="3" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
<label class="custom-control-label label-foo-bar" for="user_view_diffs_file_by_file">
<span>Show one file at a time on merge request&#39;s Changes tab</span>
@ -101,7 +101,7 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
it 'renders correct html' do
expected_html = <<~EOS
<div class="gl-form-checkbox custom-control custom-checkbox">
<input name="user[view_diffs_file_by_file]" type="hidden" value="0" />
<input name="user[view_diffs_file_by_file]" type="hidden" value="0" autocomplete="off" />
<input class="custom-control-input" type="checkbox" value="1" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
<label class="custom-control-label" for="user_view_diffs_file_by_file">
<span>Show one file at a time on merge request&#39;s Changes tab</span>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,17 +3,35 @@
require 'spec_helper'
RSpec.describe UpdateProjectStatisticsWorker do
include ExclusiveLeaseHelpers
let(:worker) { described_class.new }
let(:project) { create(:project, :repository) }
let(:statistics) { %w(repository_size) }
let(:lease_key) { "namespace:namespaces_root_statistics:#{project.namespace_id}" }
describe '#perform' do
it 'updates the project statistics' do
expect(Projects::UpdateStatisticsService).to receive(:new)
.with(project, nil, statistics: statistics)
.and_call_original
context 'when a lease could be obtained' do
it 'updates the project statistics' do
expect(Projects::UpdateStatisticsService).to receive(:new)
.with(project, nil, statistics: statistics)
.and_call_original
worker.perform(project.id, statistics)
worker.perform(lease_key, project.id, statistics)
end
end
context 'when a lease could not be obtained' do
before do
stub_exclusive_lease_taken(lease_key, timeout: ProjectCacheWorker::LEASE_TIMEOUT)
end
it 'does not update the project statistics' do
lease_key = "namespace:namespaces_root_statistics:#{project.namespace_id}"
expect(Projects::UpdateStatisticsService).not_to receive(:new)
worker.perform(lease_key, project.id, statistics)
end
end
end
end