diff --git a/app/assets/javascripts/issuable/issuable_template_selector.js b/app/assets/javascripts/issuable/issuable_template_selector.js index cce903d388d..6b8f3de8d49 100644 --- a/app/assets/javascripts/issuable/issuable_template_selector.js +++ b/app/assets/javascripts/issuable/issuable_template_selector.js @@ -17,7 +17,15 @@ export default class IssuableTemplateSelector extends TemplateSelector { name: this.dropdown.data('selected'), }; - if (initialQuery.name) this.requestFile(initialQuery); + // Only use the default template if we don't have description data from autosave + if (!initialQuery.name && this.dropdown.data('default') && !this.editor.getValue().length) { + initialQuery.name = this.dropdown.data('default'); + } + + if (initialQuery.name) { + this.requestFile(initialQuery); + this.setToggleText(initialQuery.name); + } $('.reset-template', this.dropdown.parent()).on('click', () => { this.setInputValueToTemplateContent(); @@ -53,10 +61,14 @@ export default class IssuableTemplateSelector extends TemplateSelector { } this.setInputValueToTemplateContent(); - $('.dropdown-toggle-text', this.dropdown).text(__('Choose a template')); + this.setToggleText(__('Choose a template')); this.previousSelectedIndex = null; } + setToggleText(text) { + $('.dropdown-toggle-text', this.dropdown).text(text); + } + setSelectedIndex() { this.previousSelectedIndex = this.dropdown.data('deprecatedJQueryDropdown').selectedIndex; } diff --git a/app/helpers/issuables_description_templates_helper.rb b/app/helpers/issuables_description_templates_helper.rb index a82a5ac0fb0..58b86dca1e0 100644 --- a/app/helpers/issuables_description_templates_helper.rb +++ b/app/helpers/issuables_description_templates_helper.rb @@ -5,8 +5,12 @@ module IssuablesDescriptionTemplatesHelper include GitlabRoutingHelper def template_dropdown_tag(issuable, &block) - selected_template = selected_template(issuable) - title = selected_template || _('Choose a template') + template_names = template_names(issuable) + + selected_template = selected_template_name(template_names) + default_template = default_template_name(template_names, issuable) + title = _('Choose a template') + options = { toggle_class: 'js-issuable-selector', title: title, @@ -17,6 +21,7 @@ module IssuablesDescriptionTemplatesHelper data: issuable_templates(ref_project, issuable.to_ability_name), field_name: 'issuable_template', selected: selected_template, + default: default_template, project_id: ref_project.id } } @@ -32,19 +37,19 @@ module IssuablesDescriptionTemplatesHelper @template_types[project.id][issuable_type] ||= TemplateFinder.all_template_names(project, issuable_type.pluralize) end - def selected_template(issuable) - all_templates = issuable_templates(ref_project, issuable.to_ability_name) + def selected_template_name(template_names) + template_names.find { |tmpl_name| tmpl_name == params[:issuable_template] } + end + def default_template_name(template_names, issuable) + return if issuable.description.present? || issuable.persisted? + + template_names.find { |tmpl_name| tmpl_name.casecmp?('default') } + end + + def template_names(issuable) # Only local templates will be listed if licenses for inherited templates are not present - all_templates = all_templates.values.flatten.map { |tpl| tpl[:name] }.compact.uniq - - template = all_templates.find { |tmpl_name| tmpl_name == params[:issuable_template] } - - unless issuable.description.present? - template ||= all_templates.find { |tmpl_name| tmpl_name.casecmp?('default') } - end - - template + issuable_templates(ref_project, issuable.to_ability_name).values.flatten.map { |tpl| tpl[:name] }.compact.uniq end def available_service_desk_templates_for(project) diff --git a/app/services/todos/destroy/destroyed_issuable_service.rb b/app/services/todos/destroy/destroyed_issuable_service.rb index 7a85b59eeea..759c430ec7a 100644 --- a/app/services/todos/destroy/destroyed_issuable_service.rb +++ b/app/services/todos/destroy/destroyed_issuable_service.rb @@ -5,9 +5,14 @@ module Todos class DestroyedIssuableService BATCH_SIZE = 100 + # Since we are moving towards work items, in some instances we create todos with + # `target_type: WorkItem` in other instances we still create todos with `target_type: Issue` + # So when an issue/work item is deleted, we just make sure to delete todos for both target types + BOUND_TARGET_TYPES = %w(Issue WorkItem).freeze + def initialize(target_id, target_type) @target_id = target_id - @target_type = target_type + @target_type = BOUND_TARGET_TYPES.include?(target_type) ? BOUND_TARGET_TYPES : target_type end def execute diff --git a/doc/administration/get_started.md b/doc/administration/get_started.md index b2bdd876499..01b555a2a06 100644 --- a/doc/administration/get_started.md +++ b/doc/administration/get_started.md @@ -144,7 +144,7 @@ You can restore a backup only to **the exact same version and type** (Community ### Back up GitLab SaaS -Backups of GitLab databases and filesystems are taken every 24 hours, and are kept for two weeks on a rolling schedule. All backups are encrypted. +Backups of GitLab databases and file systems are taken every 24 hours, and are kept for two weeks on a rolling schedule. All backups are encrypted. - GitLab SaaS creates backups to ensure your data is secure, but you can't use these methods to export or back up your data yourself. - Issues are stored in the database. They can't be stored in Git itself. diff --git a/glfm_specification/example_snapshots/html.yml b/glfm_specification/example_snapshots/html.yml index 6c05fbe744b..376a4bc72ca 100644 --- a/glfm_specification/example_snapshots/html.yml +++ b/glfm_specification/example_snapshots/html.yml @@ -7556,16 +7556,35 @@ wysiwyg: |-

Multiple spaces

07_01__gitlab_specific_markdown__footnotes__001: - canonical: "" + canonical: | +

+ footnote reference tag + + + 1 + + +

+
+
    +
  1. +

    + footnote text + + +

    +
  2. +
+
static: |- -

footnote reference tag 1

+

footnote reference tag 1

    -
  1. -

    footnote text

    +
  2. +

    footnote text

wysiwyg: |- -

footnote reference tag 1

+

footnote reference tag fortytwo

footnote text

diff --git a/glfm_specification/example_snapshots/markdown.yml b/glfm_specification/example_snapshots/markdown.yml index de0e66f7e08..c4c30dcb513 100644 --- a/glfm_specification/example_snapshots/markdown.yml +++ b/glfm_specification/example_snapshots/markdown.yml @@ -2190,6 +2190,6 @@ 06_15__inlines__textual_content__003: | Multiple spaces 07_01__gitlab_specific_markdown__footnotes__001: | - footnote reference tag [^1] + footnote reference tag [^fortytwo] - [^1]: footnote text + [^fortytwo]: footnote text diff --git a/glfm_specification/example_snapshots/prosemirror_json.yml b/glfm_specification/example_snapshots/prosemirror_json.yml index 0a8c15e9650..0b468945042 100644 --- a/glfm_specification/example_snapshots/prosemirror_json.yml +++ b/glfm_specification/example_snapshots/prosemirror_json.yml @@ -19218,8 +19218,8 @@ { "type": "footnoteReference", "attrs": { - "identifier": "1", - "label": "1" + "identifier": "fortytwo", + "label": "fortytwo" } } ] @@ -19227,8 +19227,8 @@ { "type": "footnoteDefinition", "attrs": { - "identifier": "1", - "label": "1" + "identifier": "fortytwo", + "label": "fortytwo" }, "content": [ { 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 e4d01b4a813..d0d450b66bf 100644 --- a/glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt +++ b/glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt @@ -14,7 +14,27 @@ See [the footnotes section of the user-facing documentation for GitLab Flavored Markdown](https://docs.gitlab.com/ee/user/markdown.html#footnotes). ```````````````````````````````` example gitlab footnote -footnote reference tag [^1] +footnote reference tag [^fortytwo] -[^1]: footnote text +[^fortytwo]: footnote text +. +

+footnote reference tag + + +1 + + +

+
+
    +
  1. +

    +footnote text + + +

    +
  2. +
+
```````````````````````````````` diff --git a/glfm_specification/output/spec.txt b/glfm_specification/output/spec.txt index c81daf7f689..3fc27efdc34 100644 --- a/glfm_specification/output/spec.txt +++ b/glfm_specification/output/spec.txt @@ -9616,9 +9616,29 @@ See [the footnotes section of the user-facing documentation for GitLab Flavored Markdown](https://docs.gitlab.com/ee/user/markdown.html#footnotes). ```````````````````````````````` example gitlab footnote -footnote reference tag [^1] +footnote reference tag [^fortytwo] -[^1]: footnote text +[^fortytwo]: footnote text +. +

+footnote reference tag + + +1 + + +

+
+
    +
  1. +

    +footnote text + + +

    +
  2. +
+
```````````````````````````````` diff --git a/scripts/lib/glfm/update_example_snapshots.rb b/scripts/lib/glfm/update_example_snapshots.rb index 893d8d9c014..d8d6cf3cdbc 100644 --- a/scripts/lib/glfm/update_example_snapshots.rb +++ b/scripts/lib/glfm/update_example_snapshots.rb @@ -29,6 +29,8 @@ module Glfm def process(skip_static_and_wysiwyg: false) output('Updating example snapshots...') + setup_environment + output('(Skipping static HTML generation)') if skip_static_and_wysiwyg output("Reading #{GLFM_SPEC_TXT_PATH}...") @@ -47,6 +49,14 @@ module Glfm private + def setup_environment + # Set 'GITLAB_TEST_FOOTNOTE_ID' in order to override random number generation in + # Banzai::Filter::FootnoteFilter#random_number, and thus avoid the need to + # perform normalization on the value. See: + # https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#normalization + ENV['GITLAB_TEST_FOOTNOTE_ID'] = '42' + end + def add_example_names(all_examples) # NOTE: This method and the parse_examples method assume: # 1. Section 2 is the first section which contains examples diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 12c5820a69d..ac83de3e765 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -90,6 +90,34 @@ RSpec.describe 'issuable templates', :js do end end + context 'user creates an issue with a default template from the repo' do + let(:template_content) { 'this is the default template' } + + before do + project.repository.create_file( + user, + '.gitlab/issue_templates/default.md', + template_content, + message: 'added default issue template', + branch_name: 'master' + ) + end + + it 'does not overwrite autosaved description' do + visit new_project_issue_path project + wait_for_requests + + assert_template # default template is loaded the first time + + fill_in 'issue_description', with: 'my own description', fill_options: { clear: :backspace } + + visit new_project_issue_path project + wait_for_requests + + assert_template(expected_content: 'my own description') + end + end + context 'user creates a merge request using templates' do let(:template_content) { 'this is a test "feature-proposal" template' } let(:bug_template_content) { 'this is merge request bug template' } diff --git a/spec/helpers/issuables_description_templates_helper_spec.rb b/spec/helpers/issuables_description_templates_helper_spec.rb index 768ce5975c1..bd8af384d40 100644 --- a/spec/helpers/issuables_description_templates_helper_spec.rb +++ b/spec/helpers/issuables_description_templates_helper_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do - include_context 'project issuable templates context' - describe '#issuable_templates' do + include_context 'project issuable templates context' + let_it_be(:inherited_from) { nil } let_it_be(:user) { create(:user) } let_it_be(:parent_group, reload: true) { create(:group) } @@ -44,7 +44,7 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do end end - describe '#selected_template' do + describe '#available_service_desk_templates_for' do let_it_be(:project) { build(:project) } before do @@ -72,40 +72,9 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do ].to_json expect(helper.available_service_desk_templates_for(@project)).to eq(value) end - - context 'when no issuable_template parameter or default template is present' do - it 'does not select a template' do - expect(helper.selected_template(project)).to be(nil) - end - end - - context 'when an issuable_template parameter has been provided' do - before do - allow(helper).to receive(:params).and_return({ issuable_template: 'another_issue_template' }) - end - - it 'selects the issuable template' do - expect(helper.selected_template(project)).to eq('another_issue_template') - end - end - - context 'when there is a default template' do - let(:templates) do - { - "" => [ - { name: "another_issue_template", id: "another_issue_template", project_id: project.id }, - { name: "default", id: "default", project_id: project.id } - ] - } - end - - it 'selects the default template' do - expect(helper.selected_template(project)).to eq('default') - end - end end - context 'when there are not templates in the project' do + context 'when there are no templates in the project' do let(:templates) { {} } it 'returns empty array' do @@ -114,4 +83,92 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do end end end + + describe '#selected_template_name' do + let(:template_names) { %w(another_issue_template custom_issue_template) } + + context 'when no issuable_template parameter is provided' do + it 'does not select a template' do + expect(helper.selected_template_name(template_names)).to be_nil + end + end + + context 'when an issuable_template parameter has been provided' do + before do + allow(helper).to receive(:params).and_return({ issuable_template: template_param_value }) + end + + context 'when param matches existing templates' do + let(:template_param_value) { 'another_issue_template' } + + it 'returns the matching issuable template' do + expect(helper.selected_template_name(template_names)).to eq('another_issue_template') + end + end + + context 'when param does not match any templates' do + let(:template_param_value) { 'non_matching_issue_template' } + + it 'returns nil' do + expect(helper.selected_template_name(template_names)).to be_nil + end + end + end + end + + describe '#default_template_name' do + context 'when a default template is available' do + let(:template_names) { %w(another_issue_template deFault) } + + it 'returns the default template' do + issue = build(:issue) + + expect(helper.default_template_name(template_names, issue)).to be('deFault') + end + + it 'returns nil when issuable has a description set' do + issue = build(:issue, description: 'from template in project settings') + + expect(helper.default_template_name(template_names, issue)).to be_nil + end + + it 'returns nil when issuable is persisted' do + issue = create(:issue) + + expect(helper.default_template_name(template_names, issue)).to be_nil + end + end + + context 'when there is no default template' do + let(:template_names) { %w(another_issue_template) } + + it 'returns nil' do + expect(helper.default_template_name(template_names, build(:issue))).to be_nil + end + end + end + + describe '#template_names' do + let(:project) { build(:project) } + let(:templates) do + { + "Project templates" => [ + { name: "another_issue_template", id: "another_issue_template", project_id: project.id }, + { name: "custom_issue_template", id: "custom_issue_template", project_id: project.id } + ], + "Group templates" => [ + { name: "another_issue_template", id: "another_issue_template", project_id: project.id } + ] + } + end + + before do + allow(helper).to receive(:ref_project).and_return(project) + allow(helper).to receive(:issuable_templates).and_return(templates) + end + + it 'returns unique list of template names' do + expect(helper.template_names(build(:issue))).to contain_exactly('another_issue_template', 'custom_issue_template') + end + end end diff --git a/spec/controllers/admin/broadcast_messages_controller_spec.rb b/spec/requests/admin/broadcast_messages_controller_spec.rb similarity index 55% rename from spec/controllers/admin/broadcast_messages_controller_spec.rb rename to spec/requests/admin/broadcast_messages_controller_spec.rb index 66a54dc10ac..9101370d42d 100644 --- a/spec/controllers/admin/broadcast_messages_controller_spec.rb +++ b/spec/requests/admin/broadcast_messages_controller_spec.rb @@ -2,16 +2,14 @@ require 'spec_helper' -RSpec.describe Admin::BroadcastMessagesController do +RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode do before do sign_in(create(:admin)) end - describe 'GET /preview' do - render_views - + describe 'POST /preview' do it 'renders preview partial' do - get :preview, params: { broadcast_message: { message: "Hello, world!" } } + post preview_admin_broadcast_messages_path, params: { broadcast_message: { message: "Hello, world!" } } expect(response).to have_gitlab_http_status(:ok) expect(response.body).to render_template(:_preview) diff --git a/spec/services/todos/destroy/destroyed_issuable_service_spec.rb b/spec/services/todos/destroy/destroyed_issuable_service_spec.rb index 24f74bae7c8..6d6abe06d1c 100644 --- a/spec/services/todos/destroy/destroyed_issuable_service_spec.rb +++ b/spec/services/todos/destroy/destroyed_issuable_service_spec.rb @@ -4,31 +4,46 @@ require 'spec_helper' RSpec.describe Todos::Destroy::DestroyedIssuableService do describe '#execute' do - let_it_be(:target) { create(:merge_request) } - let_it_be(:pending_todo) { create(:todo, :pending, project: target.project, target: target, user: create(:user)) } - let_it_be(:done_todo) { create(:todo, :done, project: target.project, target: target, user: create(:user)) } + let_it_be(:user) { create(:user) } - def execute - described_class.new(target.id, target.class.name).execute + subject { described_class.new(target.id, target.class.name).execute } + + context 'when target is merge request' do + let_it_be(:target) { create(:merge_request) } + let_it_be(:pending_todo) { create(:todo, :pending, project: target.project, target: target, user: user) } + let_it_be(:done_todo) { create(:todo, :done, project: target.project, target: target, user: user) } + + it 'deletes todos for specified target ID and type' do + control_count = ActiveRecord::QueryRecorder.new { subject }.count + + # Create more todos for the target + create(:todo, :pending, project: target.project, target: target, user: user) + create(:todo, :pending, project: target.project, target: target, user: user) + create(:todo, :done, project: target.project, target: target, user: user) + create(:todo, :done, project: target.project, target: target, user: user) + + expect { subject }.not_to exceed_query_limit(control_count) + end + + it 'invalidates todos cache counts of todo users', :use_clean_rails_redis_caching do + expect { subject } + .to change { pending_todo.user.todos_pending_count }.from(1).to(0) + .and change { done_todo.user.todos_done_count }.from(1).to(0) + end end - it 'deletes todos for specified target ID and type' do - control_count = ActiveRecord::QueryRecorder.new { execute }.count + context 'when target is an work item' do + let_it_be(:target) { create(:work_item) } + let_it_be(:todo1) { create(:todo, :pending, project: target.project, target: target, user: user) } + let_it_be(:todo2) { create(:todo, :done, project: target.project, target: target, user: user) } + # rubocop: disable Cop/AvoidBecomes + let_it_be(:todo3) { create(:todo, :pending, project: target.project, target: target.becomes(Issue), user: user) } + let_it_be(:todo4) { create(:todo, :done, project: target.project, target: target.becomes(Issue), user: user) } + # rubocop: enable Cop/AvoidBecomes - # Create more todos for the target - create(:todo, :pending, project: target.project, target: target, user: create(:user)) - create(:todo, :pending, project: target.project, target: target, user: create(:user)) - create(:todo, :done, project: target.project, target: target, user: create(:user)) - create(:todo, :done, project: target.project, target: target, user: create(:user)) - - expect { execute }.not_to exceed_query_limit(control_count) - expect(target.reload.todos.count).to eq(0) - end - - it 'invalidates todos cache counts of todo users', :use_clean_rails_redis_caching do - expect { execute } - .to change { pending_todo.user.todos_pending_count }.from(1).to(0) - .and change { done_todo.user.todos_done_count }.from(1).to(0) + it 'deletes todos' do + expect { subject }.to change(Todo, :count).by(-4) + end end end end