From dfc65ebde153c500753b4b1c33bdd4b0db95fc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 18 Sep 2025 12:21:50 +0200 Subject: [PATCH 1/3] Support disallowing immediate deletion in group/project list actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../components/groups_list/utils.js | 5 +--- .../project_list_item_actions.vue | 9 +++++- .../components/projects_list/utils.js | 8 ++---- lib/gitlab/gon_helper.rb | 3 +- .../projects/index/components/app_spec.js | 4 ++- .../components/projects_list/utils_spec.js | 28 ++++--------------- 6 files changed, 23 insertions(+), 34 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/groups_list/utils.js b/app/assets/javascripts/vue_shared/components/groups_list/utils.js index 5d0a8a005ab49f..f7121a7c12d916 100644 --- a/app/assets/javascripts/vue_shared/components/groups_list/utils.js +++ b/app/assets/javascripts/vue_shared/components/groups_list/utils.js @@ -49,10 +49,7 @@ export const availableGraphQLGroupActions = ({ if (!markedForDeletion) { availableActions.push(ACTION_DELETE); // Groups with self deletion scheduled can be deleted immediately - } else if ( - isSelfDeletionScheduled && - (userPermissions.adminAllResources || !gon?.features?.disallowImmediateDeletion) - ) { + } else if (isSelfDeletionScheduled && gon?.allow_immediate_namespaces_deletion) { availableActions.push(ACTION_DELETE_IMMEDIATELY); } } diff --git a/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_actions.vue b/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_actions.vue index dada699e8654e9..ddfb5d2f16142a 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_actions.vue +++ b/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_actions.vue @@ -9,6 +9,7 @@ import ProjectListItemDelayedDeletionModalFooter from '~/vue_shared/components/p import { ACTION_ARCHIVE, ACTION_DELETE, + ACTION_DELETE_IMMEDIATELY, ACTION_EDIT, ACTION_RESTORE, ACTION_UNARCHIVE, @@ -104,10 +105,16 @@ export default { [ACTION_DELETE]: { action: this.onActionDelete, }, + [ACTION_DELETE_IMMEDIATELY]: { + action: this.onActionDelete, + }, }; }, hasActionDelete() { - return this.project.availableActions?.includes(ACTION_DELETE); + return ( + this.project.availableActions?.includes(ACTION_DELETE) || + this.project.availableActions?.includes(ACTION_DELETE_IMMEDIATELY) + ); }, }, methods: { diff --git a/app/assets/javascripts/vue_shared/components/projects_list/utils.js b/app/assets/javascripts/vue_shared/components/projects_list/utils.js index 506a359d5f5706..71988c2351da61 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/utils.js +++ b/app/assets/javascripts/vue_shared/components/projects_list/utils.js @@ -1,6 +1,7 @@ import { ACTION_EDIT, ACTION_DELETE, + ACTION_DELETE_IMMEDIATELY, ACTION_RESTORE, ACTION_UNARCHIVE, ACTION_ARCHIVE, @@ -39,11 +40,8 @@ export const availableGraphQLProjectActions = ({ if (!markedForDeletion) { availableActions.push(ACTION_DELETE); // Projects with self deletion scheduled can be deleted immediately - } else if ( - isSelfDeletionScheduled && - (userPermissions.adminAllResources || !gon?.features?.disallowImmediateDeletion) - ) { - availableActions.push(ACTION_DELETE); + } else if (isSelfDeletionScheduled && gon?.allow_immediate_namespaces_deletion) { + availableActions.push(ACTION_DELETE_IMMEDIATELY); } } diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 23dfbff1a9b07b..c8ffbe07dd0270 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -20,6 +20,8 @@ def add_gon_variables gon.markdown_automatic_lists = current_user&.markdown_automatic_lists gon.markdown_maintain_indentation = current_user&.markdown_maintain_indentation gon.math_rendering_limits_enabled = Gitlab::CurrentSettings.math_rendering_limits_enabled + gon.allow_immediate_namespaces_deletion = + Gitlab::CurrentSettings.allow_immediate_namespaces_deletion_for_user?(current_user) # Sentry configurations for the browser client are done # via `Gitlab::CurrentSettings` from the Admin panel: @@ -104,7 +106,6 @@ def add_gon_feature_flags push_frontend_feature_flag(:whats_new_featured_carousel) push_frontend_feature_flag(:extensible_reference_filters, current_user) push_frontend_feature_flag(:paneled_view, current_user) - push_frontend_feature_flag(:disallow_immediate_deletion, current_user) push_frontend_feature_flag(:image_lightboxes, current_user) # Expose the Project Studio user preference as if it were a feature flag diff --git a/spec/frontend/admin/projects/index/components/app_spec.js b/spec/frontend/admin/projects/index/components/app_spec.js index ac17c964caef41..c0d96ce30c4e38 100644 --- a/spec/frontend/admin/projects/index/components/app_spec.js +++ b/spec/frontend/admin/projects/index/components/app_spec.js @@ -107,6 +107,8 @@ describe('AdminProjectsApp', () => { }); it('allows deleting immediately on Inactive tab', async () => { + window.gon = { allow_immediate_namespaces_deletion: true }; + await createComponent({ mountFn: mountExtended, handlers: [ @@ -118,7 +120,7 @@ describe('AdminProjectsApp', () => { await waitForPromises(); await wrapper.findByRole('button', { name: 'Actions' }).trigger('click'); - expect(wrapper.findByRole('button', { name: 'Delete' }).exists()).toBe(true); + expect(wrapper.findByRole('button', { name: 'Delete immediately' }).exists()).toBe(true); }); it('renders relative URL that supports relative_url_root', async () => { diff --git a/spec/frontend/vue_shared/components/projects_list/utils_spec.js b/spec/frontend/vue_shared/components/projects_list/utils_spec.js index 5499388017689e..6f50515481c895 100644 --- a/spec/frontend/vue_shared/components/projects_list/utils_spec.js +++ b/spec/frontend/vue_shared/components/projects_list/utils_spec.js @@ -9,6 +9,7 @@ import { import { ACTION_ARCHIVE, ACTION_DELETE, + ACTION_DELETE_IMMEDIATELY, ACTION_EDIT, ACTION_RESTORE, ACTION_UNARCHIVE, @@ -42,9 +43,7 @@ const MOCK_PROJECT_PENDING_DELETION = { describe('availableGraphQLProjectActions', () => { beforeEach(() => { window.gon = { - features: { - disallowImmediateDeletion: false, - }, + allow_immediate_namespaces_deletion: true, }; }); @@ -56,9 +55,9 @@ describe('availableGraphQLProjectActions', () => { ${{ viewEditPage: true, removeProject: true }} | ${false} | ${false} | ${false} | ${false} | ${[ACTION_EDIT, ACTION_DELETE]} ${{ viewEditPage: true, removeProject: false }} | ${true} | ${false} | ${false} | ${false} | ${[ACTION_EDIT]} ${{ viewEditPage: true, removeProject: true }} | ${true} | ${false} | ${false} | ${false} | ${[ACTION_EDIT]} - ${{ viewEditPage: true, removeProject: true }} | ${true} | ${false} | ${true} | ${false} | ${[ACTION_EDIT, ACTION_RESTORE, ACTION_DELETE]} + ${{ viewEditPage: true, removeProject: true }} | ${true} | ${false} | ${true} | ${false} | ${[ACTION_EDIT, ACTION_RESTORE, ACTION_DELETE_IMMEDIATELY]} ${{ viewEditPage: true, removeProject: true }} | ${true} | ${false} | ${false} | ${false} | ${[ACTION_EDIT]} - ${{ viewEditPage: true, removeProject: true }} | ${true} | ${false} | ${true} | ${false} | ${[ACTION_EDIT, ACTION_RESTORE, ACTION_DELETE]} + ${{ viewEditPage: true, removeProject: true }} | ${true} | ${false} | ${true} | ${false} | ${[ACTION_EDIT, ACTION_RESTORE, ACTION_DELETE_IMMEDIATELY]} ${{ viewEditPage: true, removeProject: true }} | ${true} | ${true} | ${false} | ${false} | ${[]} ${{ viewEditPage: true, removeProject: true }} | ${true} | ${true} | ${true} | ${false} | ${[]} ${{ archiveProject: true }} | ${false} | ${false} | ${false} | ${false} | ${[ACTION_ARCHIVE]} @@ -89,12 +88,10 @@ describe('availableGraphQLProjectActions', () => { }, ); - describe('when disallowImmediateDeletion feature flag is enabled', () => { + describe('when allow_immediate_namespaces_deletion is disabled', () => { beforeEach(() => { window.gon = { - features: { - disallowImmediateDeletion: true, - }, + allow_immediate_namespaces_deletion: false, }; }); @@ -108,19 +105,6 @@ describe('availableGraphQLProjectActions', () => { }), ).toStrictEqual([ACTION_EDIT, ACTION_RESTORE]); }); - - describe('when userPermissions include adminAllResources', () => { - it('allows deleting immediately', () => { - expect( - availableGraphQLProjectActions({ - userPermissions: { removeProject: true, adminAllResources: true }, - markedForDeletion: true, - isSelfDeletionInProgress: false, - isSelfDeletionScheduled: true, - }), - ).toStrictEqual([ACTION_RESTORE, ACTION_DELETE]); - }); - }); }); }); -- GitLab From 20e87eccffc85a171f4fa54ad847d0f6805a2b33 Mon Sep 17 00:00:00 2001 From: Peter Hegman Date: Thu, 2 Oct 2025 07:57:01 -0700 Subject: [PATCH 2/3] Fix frontend specs Because we switched to checking allow_immediate_namespaces_deletion --- .../admin/groups/index/components/app_spec.js | 2 ++ .../components/groups_list/formatter_spec.js | 4 +-- .../components/groups_list/utils_spec.js | 32 ++++--------------- .../formatter_spec.js | 2 +- .../components/projects_list/utils_spec.js | 12 +++---- 5 files changed, 17 insertions(+), 35 deletions(-) diff --git a/spec/frontend/admin/groups/index/components/app_spec.js b/spec/frontend/admin/groups/index/components/app_spec.js index 78f6763a6c2195..e16df213f3f94a 100644 --- a/spec/frontend/admin/groups/index/components/app_spec.js +++ b/spec/frontend/admin/groups/index/components/app_spec.js @@ -140,6 +140,8 @@ describe('AdminGroupsApp', () => { }); it('allows deleting immediately on Inactive tab', async () => { + window.gon = { allow_immediate_namespaces_deletion: true }; + await createComponent({ mountFn: mountExtended, handlers: [ diff --git a/spec/frontend/vue_shared/components/groups_list/formatter_spec.js b/spec/frontend/vue_shared/components/groups_list/formatter_spec.js index de838c6a921060..bf7a4f8d29a484 100644 --- a/spec/frontend/vue_shared/components/groups_list/formatter_spec.js +++ b/spec/frontend/vue_shared/components/groups_list/formatter_spec.js @@ -46,7 +46,7 @@ const itCorrectlyFormatsWithoutActions = (formattedGroup, mockGroup) => { describe('formatGraphQLGroup', () => { it('correctly formats the group with edit, delete, and leave permissions', () => { - window.gon = { relative_url_root: '/gitlab' }; + window.gon = { relative_url_root: '/gitlab', allow_immediate_namespaces_deletion: true }; const [mockGroup] = organizationGroups; const formattedGroup = formatGraphQLGroup(mockGroup, (group) => ({ customProperty: group.fullName, @@ -65,7 +65,7 @@ describe('formatGraphQLGroup', () => { describe('formatGraphQLGroups', () => { it('correctly formats the groups with edit, delete, and leave permissions', () => { - window.gon = { relative_url_root: '/gitlab' }; + window.gon = { relative_url_root: '/gitlab', allow_immediate_namespaces_deletion: true }; const [firstMockGroup] = organizationGroups; const formattedGroups = formatGraphQLGroups(organizationGroups, (group) => ({ customProperty: group.fullName, diff --git a/spec/frontend/vue_shared/components/groups_list/utils_spec.js b/spec/frontend/vue_shared/components/groups_list/utils_spec.js index 8a3a1cacdd2949..19d9670fb70a03 100644 --- a/spec/frontend/vue_shared/components/groups_list/utils_spec.js +++ b/spec/frontend/vue_shared/components/groups_list/utils_spec.js @@ -44,14 +44,6 @@ afterEach(() => { }); describe('availableGraphQLGroupActions', () => { - beforeEach(() => { - window.gon = { - features: { - disallowImmediateDeletion: false, - }, - }; - }); - describe.each` userPermissions | markedForDeletion | isSelfDeletionInProgress | isSelfDeletionScheduled | archived | features | availableActions ${{ viewEditPage: false, removeGroup: false }} | ${false} | ${false} | ${false} | ${false} | ${{}} | ${[]} @@ -85,7 +77,10 @@ describe('availableGraphQLGroupActions', () => { availableActions, }) => { beforeEach(() => { - window.gon.features = features; + window.gon = { + features, + allow_immediate_namespaces_deletion: true, + }; }); it(`when userPermissions = ${JSON.stringify(userPermissions)}, markedForDeletion is ${markedForDeletion}, isSelfDeletionInProgress is ${isSelfDeletionInProgress}, isSelfDeletionScheduled is ${isSelfDeletionScheduled}, and archived is ${archived} then availableActions = [${availableActions}] and is sorted correctly`, () => { @@ -102,12 +97,10 @@ describe('availableGraphQLGroupActions', () => { }, ); - describe('when disallowImmediateDeletion feature flag is enabled', () => { + describe('when allow_immediate_namespaces_deletion is disabled', () => { beforeEach(() => { window.gon = { - features: { - disallowImmediateDeletion: true, - }, + allow_immediate_namespaces_deletion: false, }; }); @@ -121,19 +114,6 @@ describe('availableGraphQLGroupActions', () => { }), ).toStrictEqual([ACTION_EDIT, ACTION_RESTORE]); }); - - describe('when userPermissions include adminAllResources', () => { - it('allows deleting immediately', () => { - expect( - availableGraphQLGroupActions({ - userPermissions: { removeGroup: true, adminAllResources: true }, - markedForDeletion: true, - isSelfDeletionInProgress: false, - isSelfDeletionScheduled: true, - }), - ).toStrictEqual([ACTION_RESTORE, ACTION_DELETE_IMMEDIATELY]); - }); - }); }); }); diff --git a/spec/frontend/vue_shared/components/nested_groups_projects_list/formatter_spec.js b/spec/frontend/vue_shared/components/nested_groups_projects_list/formatter_spec.js index 8d9661f47f8146..a4a9ed8c078fba 100644 --- a/spec/frontend/vue_shared/components/nested_groups_projects_list/formatter_spec.js +++ b/spec/frontend/vue_shared/components/nested_groups_projects_list/formatter_spec.js @@ -42,7 +42,7 @@ const mockGroupsAndProjects = [ describe('formatGraphQLGroupsAndProjects', () => { it('correctly formats the groups and projects', () => { - window.gon = { relative_url_root: '/gitlab' }; + window.gon = { relative_url_root: '/gitlab', allow_immediate_namespaces_deletion: true }; const [firstItem] = formatGraphQLGroupsAndProjects( mockGroupsAndProjects, (group) => ({ diff --git a/spec/frontend/vue_shared/components/projects_list/utils_spec.js b/spec/frontend/vue_shared/components/projects_list/utils_spec.js index 6f50515481c895..cd06985092d8a2 100644 --- a/spec/frontend/vue_shared/components/projects_list/utils_spec.js +++ b/spec/frontend/vue_shared/components/projects_list/utils_spec.js @@ -41,12 +41,6 @@ const MOCK_PROJECT_PENDING_DELETION = { }; describe('availableGraphQLProjectActions', () => { - beforeEach(() => { - window.gon = { - allow_immediate_namespaces_deletion: true, - }; - }); - describe.each` userPermissions | markedForDeletion | isSelfDeletionInProgress | isSelfDeletionScheduled | archived | availableActions ${{ viewEditPage: false, removeProject: false }} | ${false} | ${false} | ${false} | ${false} | ${[]} @@ -74,6 +68,12 @@ describe('availableGraphQLProjectActions', () => { archived, availableActions, }) => { + beforeEach(() => { + window.gon = { + allow_immediate_namespaces_deletion: true, + }; + }); + it(`when userPermissions = ${JSON.stringify(userPermissions)}, markedForDeletion is ${markedForDeletion}, isSelfDeletionInProgress is ${isSelfDeletionInProgress}, isSelfDeletionScheduled is ${isSelfDeletionScheduled}, and archived is ${archived} then availableActions = [${availableActions}] and is sorted correctly`, () => { expect( availableGraphQLProjectActions({ -- GitLab From 2a5887ae156a9250eeb65e7538ec5e531b710094 Mon Sep 17 00:00:00 2001 From: Peter Hegman Date: Thu, 2 Oct 2025 12:22:13 -0700 Subject: [PATCH 3/3] Add spec for gon_helper.rb change --- spec/lib/gitlab/gon_helper_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb index 24f06f7d502564..57b471b05c49f0 100644 --- a/spec/lib/gitlab/gon_helper_spec.rb +++ b/spec/lib/gitlab/gon_helper_spec.rb @@ -120,6 +120,18 @@ helper.add_gon_variables end end + + describe 'allow_immediate_namespaces_deletion' do + before do + allow(Gitlab::CurrentSettings).to receive(:allow_immediate_namespaces_deletion_for_user?).and_return(false) + end + + it 'exposes allow_immediate_namespaces_deletion property' do + expect(gon).to receive(:allow_immediate_namespaces_deletion=).with(false) + + helper.add_gon_variables + end + end end describe '#push_frontend_ability' do -- GitLab