diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index f2a9f34dcda..7e053404100 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -28,6 +28,7 @@ import { WORK_ITEM_TYPE_VALUE_EPIC, WIDGET_TYPE_WEIGHT, WIDGET_TYPE_DEVELOPMENT, + STATE_OPEN, } from '../constants'; import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql'; @@ -451,6 +452,12 @@ export default { activeChildItemId() { return this.activeChildItem?.id; }, + workItemIsOpen() { + return this.workItem?.state === STATE_OPEN; + }, + showCreateBranchMergeRequestSplitButton() { + return this.workItemDevelopment && this.workItemIsOpen; + }, }, methods: { handleWorkItemCreated() { @@ -865,7 +872,7 @@ export default { @error="onUploadDesignError" /> - {{ buttonText }} + {{ createMergeRequestButtonText }} diff --git a/app/models/preloaders/group_policy_preloader.rb b/app/models/preloaders/group_policy_preloader.rb index c52e49408c5..751d165643a 100644 --- a/app/models/preloaders/group_policy_preloader.rb +++ b/app/models/preloaders/group_policy_preloader.rb @@ -8,6 +8,8 @@ module Preloaders end def execute + return if groups.blank? + ActiveRecord::Associations::Preloader.new( records: groups, associations: [:organization] diff --git a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb index 4615b22cfba..72fb12c21b7 100644 --- a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb +++ b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb @@ -10,6 +10,8 @@ module Preloaders end def execute + return unless @user + preload_with_traversal_ids end diff --git a/doc/development/application_limits.md b/doc/development/application_limits.md index d40f549d6b2..fa804f5aacc 100644 --- a/doc/development/application_limits.md +++ b/doc/development/application_limits.md @@ -161,10 +161,10 @@ GitLab.com: - `default`: Any system-wide feature. - `free`: Namespaces and projects with a Free subscription. - `bronze`: Namespaces and projects with a Bronze subscription. This tier is no longer available for purchase. -- `silver`: Namespaces and projects with a Premium subscription. +- `silver`: Namespaces and projects with a Premium subscription. This tier is no longer available for purchase. - `premium`: Namespaces and projects with a Premium subscription. - `premium_trial`: Namespaces and projects with a Premium Trial subscription. -- `gold`: Namespaces and projects with an Ultimate subscription. +- `gold`: Namespaces and projects with an Ultimate subscription. This tier is no longer available for purchase. - `ultimate`: Namespaces and projects with an Ultimate subscription. - `ultimate_trial`: Namespaces and projects with an Ultimate Trial subscription. - `ultimate_trial_paid_customer`: Namespaces and projects on a Premium subscription that are trialling Ultimate for 30 days. diff --git a/doc/tutorials/scrum_events/index.md b/doc/tutorials/scrum_events/index.md index 57045b89422..d1061732365 100644 --- a/doc/tutorials/scrum_events/index.md +++ b/doc/tutorials/scrum_events/index.md @@ -174,7 +174,7 @@ Repeat these steps to create all the labels you'll need: - **Type:** You will use these labels to represent the different types of work typically pulled into a single iteration: - `type::story` - `type::bug` - - `type::mantainence` + - `type::maintenance` ### Create an iteration cadence diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb index 88ce63a63fe..7ec9286aa19 100644 --- a/lib/banzai/reference_parser/base_parser.rb +++ b/lib/banzai/reference_parser/base_parser.rb @@ -72,13 +72,18 @@ module Banzai # Returns all the nodes that are visible to the given user. def nodes_visible_to_user(user, nodes) projects = lazy { projects_for_nodes(nodes) } + groups = lazy { groups_for_nodes(nodes) } project_attr = 'data-project' + group_attr = 'data-group' preload_associations(projects, user) + preload_group_associations(groups, user) nodes.select do |node| if node.has_attribute?(project_attr) can_read_reference?(user, projects[node], node) + elsif node.has_attribute?(group_attr) + can_read_reference?(user, groups[node], node) else true end @@ -233,8 +238,11 @@ module Banzai # { node => project } # def projects_for_nodes(nodes) - @projects_for_nodes ||= - grouped_objects_for_nodes(nodes, Project.includes(:project_feature), 'data-project') + @projects_for_nodes ||= grouped_objects_for_nodes(nodes, Project.includes(:project_feature), 'data-project') + end + + def groups_for_nodes(nodes) + @groups_for_nodes ||= grouped_objects_for_nodes(nodes, Group, 'data-group') end def can?(user, permission, subject = :global) @@ -279,6 +287,10 @@ module Banzai def preload_associations(projects, user) ::Preloaders::ProjectPolicyPreloader.new(projects.values, user).execute end + + def preload_group_associations(groups, user) + ::Preloaders::GroupPolicyPreloader.new(groups.values, user).execute + end end end end diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index e613ae41ddf..64b5954a70c 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -25,7 +25,7 @@ import DesignWidget from '~/work_items/components/design_management/design_manag import DesignUploadButton from '~/work_items/components//design_management/upload_button.vue'; import WorkItemCreateBranchMergeRequestSplitButton from '~/work_items/components/work_item_development/work_item_create_branch_merge_request_split_button.vue'; import uploadDesignMutation from '~/work_items/components/design_management/graphql/upload_design.mutation.graphql'; -import { i18n } from '~/work_items/constants'; +import { i18n, STATE_CLOSED } from '~/work_items/constants'; import workItemByIdQuery from '~/work_items/graphql/work_item_by_id.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; @@ -934,6 +934,22 @@ describe('WorkItemDetail component', () => { expect(findCreateMergeRequestSplitButton().exists()).toBe(true); }); + + it('should not show the button when the work item is closed', async () => { + createComponent({ + handler: jest.fn().mockResolvedValue( + workItemByIidResponseFactory({ + canUpdate: true, + canDelete: true, + developmentWidgetPresent: true, + state: STATE_CLOSED, + }), + ), + }); + await waitForPromises(); + + expect(findCreateMergeRequestSplitButton().exists()).toBe(false); + }); }); describe('work item attributes wrapper', () => { diff --git a/spec/frontend/work_items/components/work_item_development/work_item_create_branch_merge_request_split_button_spec.js b/spec/frontend/work_items/components/work_item_development/work_item_create_branch_merge_request_split_button_spec.js index 9d0f4185337..c7a580b3184 100644 --- a/spec/frontend/work_items/components/work_item_development/work_item_create_branch_merge_request_split_button_spec.js +++ b/spec/frontend/work_items/components/work_item_development/work_item_create_branch_merge_request_split_button_spec.js @@ -42,7 +42,7 @@ describe('WorkItemCreateBranchMergeRequestSplitButton', () => { }); it('shows loading indicator till the permission call is made', () => { - expect(findMainButton().text()).toBe('Checking branch availability...'); + expect(findMainButton().text()).toBe('Create merge request'); expect(findMainButton().props('loading')).toBe(true); }); diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index a54242a4041..4046a19288e 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -1089,6 +1089,62 @@ RSpec.describe Note, feature_category: :team_planning do end end + describe 'all_referenced_mentionables_allowed?' do + let_it_be(:user) { create(:user) } + let_it_be(:issue) { create(:issue) } + + RSpec.shared_examples 'does not generate N+1 queries for reference parsing' do + it 'does not generate N+1 queries for reference parsing', :request_store do + ref1 = milestone1.to_reference(issue.project, format: :name, full: true, absolute_path: true) + ref2 = milestone2.to_reference(issue.project, format: :name, full: true, absolute_path: true) + ref3 = milestone3.to_reference(issue.project, format: :name, full: true, absolute_path: true) + + text = "mentioned in #{ref1}" + note = create(:note, :system, noteable: issue, note: text, project: issue.project) + + note.system_note_visible_for?(user) + + text = "mentioned in #{ref1} and #{ref2}" + note.update!(note: text) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + note.system_note_visible_for?(user) + end + + text = "mentioned in #{ref1} and #{ref2} and #{ref3}" + note.update!(note: text) + + expect do + note.system_note_visible_for?(user) + end.to issue_same_number_of_queries_as(control).or_fewer + end + end + + context 'with a project level milestone' do + let_it_be(:milestone1) { create(:milestone, project: create(:project, :private)) } + let_it_be(:milestone2) { create(:milestone, project: create(:project, :private)) } + let_it_be(:milestone3) { create(:milestone, project: create(:project, :private)) } + let_it_be(:milestone_event) { create(:resource_milestone_event, issue: issue, milestone: milestone1) } + let_it_be(:note) { MilestoneNote.from_event(milestone_event, resource: issue, resource_parent: issue.project) } + + it { expect(note.system_note_visible_for?(user)).to be false } + + it_behaves_like 'does not generate N+1 queries for reference parsing' + end + + context 'with a group level milestone' do + let_it_be(:milestone1) { create(:milestone, group: create(:group, :private)) } + let_it_be(:milestone2) { create(:milestone, group: create(:group, :private)) } + let_it_be(:milestone3) { create(:milestone, group: create(:group, :private)) } + let_it_be(:milestone_event) { create(:resource_milestone_event, issue: issue, milestone: milestone1) } + let_it_be(:note) { MilestoneNote.from_event(milestone_event, resource: issue, resource_parent: issue.project) } + + it { expect(note.system_note_visible_for?(user)).to be false } + + it_behaves_like 'does not generate N+1 queries for reference parsing' + end + end + describe 'clear_blank_line_code!' do it 'clears a blank line code before validation' do note = build(:note, line_code: ' ')