diff --git a/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql b/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql index 2110cc12e45d9d83b6d454374ef4c93f35f89cb6..e57c680f9aead425161941dc7e3480156449145b 100644 --- a/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql +++ b/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql @@ -1,6 +1,9 @@ # id field is requested by ~/graphql_shared/fragments/group.fragment.graphql # eslint-disable-next-line @graphql-eslint/require-selections fragment AdminGroup on Group { + userPermissions { + adminAllResources @gl_introduced(version: "18.3.0") + } projectStatistics { storageSize } diff --git a/app/assets/javascripts/admin/projects/index/graphql/queries/projects.query.graphql b/app/assets/javascripts/admin/projects/index/graphql/queries/projects.query.graphql index 80b3c6bf68a303a11555b63cc2e47508de4e3403..b76dc4b13cbda4c66bff10d6abd89a838b10dcae 100644 --- a/app/assets/javascripts/admin/projects/index/graphql/queries/projects.query.graphql +++ b/app/assets/javascripts/admin/projects/index/graphql/queries/projects.query.graphql @@ -30,6 +30,9 @@ query getAdminProjects( count nodes { ...Project + userPermissions { + adminAllResources @gl_introduced(version: "18.3.0") + } statistics { storageSize } diff --git a/app/assets/javascripts/graphql_shared/fragments/group.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/group.fragment.graphql index 6fd92f94ffd05441dbfd114e9e09390be0b4c473..1d487916de837bfd2ca4326daec49665312d5d77 100644 --- a/app/assets/javascripts/graphql_shared/fragments/group.fragment.graphql +++ b/app/assets/javascripts/graphql_shared/fragments/group.fragment.graphql @@ -20,7 +20,6 @@ fragment Group on Group { canLeave removeGroup viewEditPage - adminAllResources @gl_introduced(version: "18.3.0") } maxAccessLevel { integerValue diff --git a/app/assets/javascripts/graphql_shared/fragments/project.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/project.fragment.graphql index e4e3d814125350313826dc0d9ab528dfff095d28..021c9bc4ea87946f59f8cadbc989225c611ebc56 100644 --- a/app/assets/javascripts/graphql_shared/fragments/project.fragment.graphql +++ b/app/assets/javascripts/graphql_shared/fragments/project.fragment.graphql @@ -33,7 +33,6 @@ fragment Project on Project { archiveProject removeProject viewEditPage - adminAllResources @gl_introduced(version: "18.3.0") } maxAccessLevel { integerValue @@ -46,6 +45,8 @@ fragment Project on Project { ...CiIcon } } - markedForDeletionOn + markedForDeletion + isSelfDeletionInProgress + isSelfDeletionScheduled permanentDeletionDate } diff --git a/app/assets/javascripts/groups/your_work/graphql/utils.js b/app/assets/javascripts/groups/your_work/graphql/utils.js index 936d4c1a59b3ae4eddb08ceaba05ccd563bf95ac..6dcac0cb1f4c511585ad43b88e0bfdbf1f414934 100644 --- a/app/assets/javascripts/groups/your_work/graphql/utils.js +++ b/app/assets/javascripts/groups/your_work/graphql/utils.js @@ -23,10 +23,6 @@ export const formatGroupForGraphQLResolver = (group) => ({ canLeave: group.can_leave, removeGroup: group.can_remove, viewEditPage: group.can_edit, - // Only used in admin area to ensure only instance admins (users with custom - // admin roles can also access admin area) see admin-only per-group action - // buttons. - adminAllResources: false, }, webUrl: group.web_url, groupMembersCount: group.group_members_count ?? null, 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 5c1ba6732416547767e357fa4128ee35a6e0031c..c9e8fbddbc501089b1b5ef824a44c035a7b3c84b 100644 --- a/app/assets/javascripts/vue_shared/components/groups_list/utils.js +++ b/app/assets/javascripts/vue_shared/components/groups_list/utils.js @@ -22,35 +22,38 @@ export const availableGraphQLGroupActions = ({ return []; } - const baseActions = []; + const availableActions = []; if (userPermissions.viewEditPage) { - baseActions.push(ACTION_EDIT); + availableActions.push(ACTION_EDIT); } if (userPermissions.archiveGroup) { - baseActions.push(archived ? ACTION_UNARCHIVE : ACTION_ARCHIVE); + availableActions.push(archived ? ACTION_UNARCHIVE : ACTION_ARCHIVE); } if (userPermissions.removeGroup && isSelfDeletionScheduled) { - baseActions.push(ACTION_RESTORE); + availableActions.push(ACTION_RESTORE); } if (userPermissions.canLeave) { - baseActions.push(ACTION_LEAVE); + availableActions.push(ACTION_LEAVE); } if (userPermissions.removeGroup) { // Groups that are not marked for deletion can be deleted (delayed) if (!markedForDeletion) { - baseActions.push(ACTION_DELETE); + availableActions.push(ACTION_DELETE); // Groups with self deletion scheduled can be deleted immediately - } else if (isSelfDeletionScheduled) { - baseActions.push(ACTION_DELETE_IMMEDIATELY); + } else if ( + isSelfDeletionScheduled && + (userPermissions.adminAllResources || !gon?.features?.disallowImmediateDeletion) + ) { + availableActions.push(ACTION_DELETE_IMMEDIATELY); } } - return baseActions; + return availableActions; }; export const renderDeleteSuccessToast = (item) => { diff --git a/app/assets/javascripts/vue_shared/components/nested_groups_projects_list/mock_data.js b/app/assets/javascripts/vue_shared/components/nested_groups_projects_list/mock_data.js index 6b66da7ccf7fa53c8fcd3ff73fbb773aa41670a0..3381f31c1b9e8755f188bea2d7e2583918ce62fc 100644 --- a/app/assets/javascripts/vue_shared/components/nested_groups_projects_list/mock_data.js +++ b/app/assets/javascripts/vue_shared/components/nested_groups_projects_list/mock_data.js @@ -10,7 +10,7 @@ const makeGroup = ({ name, fullName, childrenToLoad = [] }) => { return { type: LIST_ITEM_TYPE_GROUP, - markedForDeletionOn: null, + markedForDeletion: false, permanentDeletionDate: '2025-02-26', fullPath, descriptionHtml: @@ -43,7 +43,7 @@ const makeProject = ({ name, nameWithNamespace }) => { return { type: LIST_ITEM_TYPE_PROJECT, - markedForDeletionOn: null, + markedForDeletion: false, permanentDeletionDate: '2025-02-26', fullPath, archived: false, diff --git a/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_delayed_deletion_modal_footer.vue b/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_delayed_deletion_modal_footer.vue index 6ef0ec9f4f6a55c02eca16019353e75d73908c10..a2248fe308abf7b2ef155c9c15154a0bea57fea2 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_delayed_deletion_modal_footer.vue +++ b/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_delayed_deletion_modal_footer.vue @@ -22,7 +22,7 @@ export default { }, computed: { showRestoreMessage() { - return !this.project.markedForDeletionOn; + return !this.project.markedForDeletion; }, }, RESTORE_HELP_PATH: helpPagePath('user/project/working_with_projects', { 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 714e05511d31e965b61c12fccba0edb8bd0ad177..506a359d5f57061d70d1dd9e4df8d46fe1c4624a 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/utils.js +++ b/app/assets/javascripts/vue_shared/components/projects_list/utils.js @@ -10,9 +10,16 @@ import { sprintf, __ } from '~/locale'; export const availableGraphQLProjectActions = ({ userPermissions, - markedForDeletionOn, + markedForDeletion, + isSelfDeletionInProgress, + isSelfDeletionScheduled, archived, }) => { + // No actions available when project deletion is in progress + if (isSelfDeletionInProgress) { + return []; + } + const availableActions = []; if (userPermissions.viewEditPage) { @@ -23,12 +30,21 @@ export const availableGraphQLProjectActions = ({ availableActions.push(archived ? ACTION_UNARCHIVE : ACTION_ARCHIVE); } - if (userPermissions.removeProject && markedForDeletionOn) { + if (userPermissions.removeProject && isSelfDeletionScheduled) { availableActions.push(ACTION_RESTORE); } if (userPermissions.removeProject) { - availableActions.push(ACTION_DELETE); + // Projects that are not marked for deletion can be deleted (delayed) + 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); + } } return availableActions; @@ -59,7 +75,7 @@ export const renderRestoreSuccessToast = (project) => { }; export const renderDeleteSuccessToast = (project) => { - if (project.markedForDeletionOn) { + if (project.markedForDeletion) { toast( sprintf(__("Project '%{project_name}' is being deleted."), { project_name: project.nameWithNamespace, @@ -79,7 +95,7 @@ export const renderDeleteSuccessToast = (project) => { export const deleteParams = (project) => { // Project has been marked for delayed deletion so will now be deleted immediately. - if (project.markedForDeletionOn) { + if (project.isSelfDeletionScheduled) { return { permanently_remove: true, full_path: project.fullPath }; } diff --git a/app/assets/javascripts/vue_shared/components/resource_lists/list_item_description.stories.js b/app/assets/javascripts/vue_shared/components/resource_lists/list_item_description.stories.js index 7a0a324f5f7d8e25030f26bc8269167271c6e520..4f6f38bd3341c908ede681863020b0bda0393779 100644 --- a/app/assets/javascripts/vue_shared/components/resource_lists/list_item_description.stories.js +++ b/app/assets/javascripts/vue_shared/components/resource_lists/list_item_description.stories.js @@ -34,7 +34,7 @@ PendingDeletion.args = { ...Default.args, resource: { ...Default.args.resource, - markedForDeletionOn: '2024-12-01', + markedForDeletion: true, permanentDeletionDate: '2024-12-07', }, }; diff --git a/app/assets/javascripts/vue_shared/components/resource_lists/list_item_description.vue b/app/assets/javascripts/vue_shared/components/resource_lists/list_item_description.vue index 20a99bdc69d19f31f8bbbaef25f12610ada2cdbe..53b2faa0d3330863fd467b1cb7e01e1630d2b1bd 100644 --- a/app/assets/javascripts/vue_shared/components/resource_lists/list_item_description.vue +++ b/app/assets/javascripts/vue_shared/components/resource_lists/list_item_description.vue @@ -31,7 +31,7 @@ export default { return this.resource.descriptionHtml; }, isPendingDeletion() { - return Boolean(this.resource.markedForDeletion || this.resource.markedForDeletionOn); + return Boolean(this.resource.markedForDeletion); }, formattedDate() { return formatDate(newDate(this.resource.permanentDeletionDate), SHORT_DATE_FORMAT); diff --git a/app/assets/javascripts/vue_shared/components/resource_lists/list_item_inactive_badge.vue b/app/assets/javascripts/vue_shared/components/resource_lists/list_item_inactive_badge.vue index 6dc10a7717238f5d451a55bddb95cd985af72f79..9ac8b72525e08d3b7ca0c130f585c278634da9eb 100644 --- a/app/assets/javascripts/vue_shared/components/resource_lists/list_item_inactive_badge.vue +++ b/app/assets/javascripts/vue_shared/components/resource_lists/list_item_inactive_badge.vue @@ -22,7 +22,7 @@ export default { return Boolean(this.resource.isSelfDeletionInProgress); }, isPendingDeletion() { - return Boolean(this.resource.markedForDeletionOn || this.resource.markedForDeletion); + return Boolean(this.resource.markedForDeletion); }, isArchived() { return this.resource.archived; diff --git a/config/feature_flags/gitlab_com_derisk/disallow_immediate_deletion.yml b/config/feature_flags/gitlab_com_derisk/disallow_immediate_deletion.yml new file mode 100644 index 0000000000000000000000000000000000000000..9a11c78ca5c13621acfc9d50bf96e09113cffdf9 --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/disallow_immediate_deletion.yml @@ -0,0 +1,10 @@ +--- +name: disallow_immediate_deletion +description: +feature_issue_url: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/201957 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/562308 +milestone: '18.4' +group: group::organizations +type: gitlab_com_derisk +default_enabled: false diff --git a/ee/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql b/ee/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql index e100dbae9a8ca3deedf768f9cbee4c3ab8202e79..dff0c72aa1ea2ad47601906f4f4d273ff5f8913a 100644 --- a/ee/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql +++ b/ee/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql @@ -1,6 +1,9 @@ # id field is requested by ~/graphql_shared/fragments/group.fragment.graphql # eslint-disable-next-line @graphql-eslint/require-selections fragment AdminGroup on Group { + userPermissions { + adminAllResources @gl_introduced(version: "18.3.0") + } projectStatistics { storageSize } diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 083afc52462f62a6dacdd57b4b7edb6e0233dfc4..f7e8f8e7953c23f456adea8b52e8b91904a87345 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -104,6 +104,7 @@ def add_gon_feature_flags push_frontend_feature_flag(:extensible_reference_filters, current_user) push_frontend_feature_flag(:global_topbar, current_user) push_frontend_feature_flag(:paneled_view, current_user) + push_frontend_feature_flag(:disallow_immediate_deletion, current_user) end # Exposes the state of a feature flag to the frontend code. diff --git a/spec/frontend/admin/groups/index/components/app_spec.js b/spec/frontend/admin/groups/index/components/app_spec.js index 5c6f4b9d56906fc425197ead545675e3c3f9fa81..cb591d9f9cb8dd9ab6734c1127569a63bc994fff 100644 --- a/spec/frontend/admin/groups/index/components/app_spec.js +++ b/spec/frontend/admin/groups/index/components/app_spec.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; import { GlEmptyState, GlKeysetPagination } from '@gitlab/ui'; +import adminInactiveGroupsGraphQlResponse from 'test_fixtures/graphql/admin/inactive_groups.query.graphql.json'; import adminGroupsGraphQlResponse from 'test_fixtures/graphql/admin/groups.query.graphql.json'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; import AdminGroupsApp from '~/admin/groups/index/components/app.vue'; @@ -17,6 +18,7 @@ import { ADMIN_GROUPS_TABS, FIRST_TAB_ROUTE_NAMES, ADMIN_GROUPS_ROUTE_NAME, + INACTIVE_TAB, } from '~/admin/groups/index/constants'; import adminGroupCountsQuery from '~/admin/groups/index/graphql/queries/group_counts.query.graphql'; import { @@ -131,6 +133,21 @@ describe('AdminGroupsApp', () => { expect(wrapper.findComponent(GlKeysetPagination).exists()).toBe(true); }); + it('allows deleting immediately on Inactive tab', async () => { + await createComponent({ + mountFn: mountExtended, + handlers: [ + [adminGroupsQuery, jest.fn().mockResolvedValue(adminInactiveGroupsGraphQlResponse)], + ], + route: { name: INACTIVE_TAB.value }, + }); + + await waitForPromises(); + await wrapper.findByRole('button', { name: 'Actions' }).trigger('click'); + + expect(wrapper.findByRole('button', { name: 'Delete immediately' }).exists()).toBe(true); + }); + describe('when there are no groups', () => { beforeEach(async () => { await createComponent({ diff --git a/spec/frontend/admin/projects/index/components/app_spec.js b/spec/frontend/admin/projects/index/components/app_spec.js index ba0c40879361761dca10ff00308ef4db84e1a52b..2cb30840221023ccae2642535dfa59863428af76 100644 --- a/spec/frontend/admin/projects/index/components/app_spec.js +++ b/spec/frontend/admin/projects/index/components/app_spec.js @@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; import { GlKeysetPagination } from '@gitlab/ui'; import adminProjectsGraphQlResponse from 'test_fixtures/graphql/admin/projects.query.graphql.json'; +import adminInactiveProjectsGraphQlResponse from 'test_fixtures/graphql/admin/inactive_projects.query.graphql.json'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; import TabsWithList from '~/groups_projects/components/tabs_with_list.vue'; import AdminProjectsApp from '~/admin/projects/index/components/app.vue'; @@ -29,6 +30,7 @@ import { FILTERED_SEARCH_TERM_KEY, FILTERED_SEARCH_NAMESPACE, ADMIN_PROJECTS_ROUTE_NAME, + INACTIVE_TAB, } from '~/admin/projects/index/constants'; import adminProjectsQuery from '~/admin/projects/index/graphql/queries/projects.query.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -98,6 +100,21 @@ describe('AdminProjectsApp', () => { }); }); + it('allows deleting immediately on Inactive tab', async () => { + await createComponent({ + mountFn: mountExtended, + handlers: [ + [adminProjectsQuery, jest.fn().mockResolvedValue(adminInactiveProjectsGraphQlResponse)], + ], + route: { name: INACTIVE_TAB.value }, + }); + + await waitForPromises(); + await wrapper.findByRole('button', { name: 'Actions' }).trigger('click'); + + expect(wrapper.findByRole('button', { name: 'Delete' }).exists()).toBe(true); + }); + it('renders relative URL that supports relative_url_root', async () => { window.gon = { relative_url_root: '/gitlab' }; diff --git a/spec/frontend/fixtures/admin.rb b/spec/frontend/fixtures/admin.rb index aa4cf6d89ec5070d4cec801539fd3c1caf70dde0..bb9f1f53704bc1229e0608ad484eb5d220cbd41f 100644 --- a/spec/frontend/fixtures/admin.rb +++ b/spec/frontend/fixtures/admin.rb @@ -13,10 +13,18 @@ create_list(:group, 3, :with_avatar, owners: [owner, current_user], description: 'foo bar') end + let_it_be(:pending_deletion_group) do + create(:group_with_deletion_schedule, owners: [owner, current_user], marked_for_deletion_on: Date.yesterday) + end + let_it_be(:projects) do create_list(:project, 3, :with_avatar, owners: [owner, current_user], description: 'foo bar') end + let_it_be(:pending_deletion_project) do + create(:project, :aimed_for_deletion, owners: [owner, current_user]) + end + let_it_be(:project) do create(:project, namespace: groups.first, @@ -48,6 +56,18 @@ expect_graphql_errors_to_be_empty end + + it "#{base_output_path}inactive_#{query_name}.json" do + query = get_graphql_query_as_string("#{base_input_path}#{query_name}") + + post_graphql( + query, + current_user: current_user, + variables: { search: '', first: 3, sort: 'created_at_asc', active: false } + ) + + expect_graphql_errors_to_be_empty + end end describe 'projects' do @@ -66,6 +86,18 @@ expect_graphql_errors_to_be_empty end + + it "#{base_output_path}inactive_#{query_name}.json" do + query = get_graphql_query_as_string("#{base_input_path}#{query_name}") + + post_graphql( + query, + current_user: current_user, + variables: { search: '', first: 3, sort: 'created_at_asc', active: false } + ) + + expect_graphql_errors_to_be_empty + end end end end 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 2ae16121f365e61c3083d93bc254afd139f9f173..bffdeac48bfde4cdb790d84f730591d20990d3ab 100644 --- a/spec/frontend/vue_shared/components/groups_list/utils_spec.js +++ b/spec/frontend/vue_shared/components/groups_list/utils_spec.js @@ -28,16 +28,30 @@ const MOCK_GROUP = { const MOCK_GROUP_WITH_DELAY_DELETION = { ...MOCK_GROUP, markedForDeletion: false, + isSelfDeletionScheduled: false, permanentDeletionDate: '2024-03-31', }; const MOCK_GROUP_PENDING_DELETION = { ...MOCK_GROUP, markedForDeletion: true, + isSelfDeletionScheduled: true, permanentDeletionDate: '2024-03-31', }; +afterEach(() => { + window.gon = {}; +}); + describe('availableGraphQLGroupActions', () => { + beforeEach(() => { + window.gon = { + features: { + disallowImmediateDeletion: false, + }, + }; + }); + describe.each` userPermissions | markedForDeletion | isSelfDeletionInProgress | isSelfDeletionScheduled | archived | availableActions ${{ viewEditPage: false, removeGroup: false }} | ${false} | ${false} | ${false} | ${false} | ${[]} @@ -78,6 +92,40 @@ describe('availableGraphQLGroupActions', () => { }); }, ); + + describe('when disallowImmediateDeletion feature flag is enabled', () => { + beforeEach(() => { + window.gon = { + features: { + disallowImmediateDeletion: true, + }, + }; + }); + + it('does not allow deleting immediately', () => { + expect( + availableGraphQLGroupActions({ + userPermissions: { viewEditPage: true, removeGroup: true }, + markedForDeletion: true, + isSelfDeletionInProgress: false, + isSelfDeletionScheduled: true, + }), + ).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]); + }); + }); + }); }); describe('renderDeleteSuccessToast', () => { diff --git a/spec/frontend/vue_shared/components/projects_list/project_list_item_delayed_deletion_modal_footer_spec.js b/spec/frontend/vue_shared/components/projects_list/project_list_item_delayed_deletion_modal_footer_spec.js index 7877f1fa1502edcb9671e80f2afc6b3337273b08..3b46322218e38904e143257b9e02fbf3f622726d 100644 --- a/spec/frontend/vue_shared/components/projects_list/project_list_item_delayed_deletion_modal_footer_spec.js +++ b/spec/frontend/vue_shared/components/projects_list/project_list_item_delayed_deletion_modal_footer_spec.js @@ -31,18 +31,18 @@ describe('ProjectListItemDelayedDeletionModalFooterEE', () => { const findGlLink = () => wrapper.findComponent(GlLink); describe.each` - markedForDeletionOn | footer | link - ${null} | ${`This project can be restored until ${MOCK_PERM_DELETION_DATE}. Learn more.`} | ${HELP_PATH} - ${'2024-03-24'} | ${false} | ${false} + markedForDeletion | footer | link + ${false} | ${`This project can be restored until ${MOCK_PERM_DELETION_DATE}. Learn more.`} | ${HELP_PATH} + ${true} | ${false} | ${false} `( - 'when project.markedForDeletionOn is $markedForDeletionOn', - ({ markedForDeletionOn, footer, link }) => { + 'when project.markedForDeletion is $markedForDeletion', + ({ markedForDeletion, footer, link }) => { beforeEach(() => { createComponent({ props: { project: { ...project, - markedForDeletionOn, + markedForDeletion, permanentDeletionDate: MOCK_PERM_DELETION_DATE, }, }, 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 006d37103b0d012cf6bcdc27408001a56aa39af8..5499388017689e8b3ac1fd82de2661b17dd27db4 100644 --- a/spec/frontend/vue_shared/components/projects_list/utils_spec.js +++ b/spec/frontend/vue_shared/components/projects_list/utils_spec.js @@ -27,39 +27,101 @@ const MOCK_PROJECT = { const MOCK_PROJECT_DELAY_DELETION_ENABLED = { ...MOCK_PROJECT, - markedForDeletionOn: null, + markedForDeletion: false, + isSelfDeletionScheduled: false, permanentDeletionDate: '2024-03-31', }; const MOCK_PROJECT_PENDING_DELETION = { ...MOCK_PROJECT, - markedForDeletionOn: '2024-03-24', + markedForDeletion: true, + isSelfDeletionScheduled: true, permanentDeletionDate: '2024-03-31', }; describe('availableGraphQLProjectActions', () => { + beforeEach(() => { + window.gon = { + features: { + disallowImmediateDeletion: false, + }, + }; + }); + describe.each` - userPermissions | markedForDeletionOn | archived | availableActions - ${{ viewEditPage: false, removeProject: false }} | ${null} | ${false} | ${[]} - ${{ viewEditPage: true, removeProject: false }} | ${null} | ${false} | ${[ACTION_EDIT]} - ${{ viewEditPage: false, removeProject: true }} | ${null} | ${false} | ${[ACTION_DELETE]} - ${{ viewEditPage: true, removeProject: true }} | ${null} | ${false} | ${[ACTION_EDIT, ACTION_DELETE]} - ${{ viewEditPage: true, removeProject: false }} | ${'2024-12-31'} | ${false} | ${[ACTION_EDIT]} - ${{ viewEditPage: true, removeProject: true }} | ${'2024-12-31'} | ${false} | ${[ACTION_EDIT, ACTION_RESTORE, ACTION_DELETE]} - ${{ archiveProject: true }} | ${null} | ${false} | ${[ACTION_ARCHIVE]} - ${{ archiveProject: true }} | ${null} | ${true} | ${[ACTION_UNARCHIVE]} - ${{ archiveProject: false }} | ${null} | ${false} | ${[]} - ${{ archiveProject: false }} | ${null} | ${true} | ${[]} + userPermissions | markedForDeletion | isSelfDeletionInProgress | isSelfDeletionScheduled | archived | availableActions + ${{ viewEditPage: false, removeProject: false }} | ${false} | ${false} | ${false} | ${false} | ${[]} + ${{ viewEditPage: true, removeProject: false }} | ${false} | ${false} | ${false} | ${false} | ${[ACTION_EDIT]} + ${{ viewEditPage: false, removeProject: true }} | ${false} | ${false} | ${false} | ${false} | ${[ACTION_DELETE]} + ${{ 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} | ${false} | ${false} | ${[ACTION_EDIT]} + ${{ viewEditPage: true, removeProject: true }} | ${true} | ${false} | ${true} | ${false} | ${[ACTION_EDIT, ACTION_RESTORE, ACTION_DELETE]} + ${{ viewEditPage: true, removeProject: true }} | ${true} | ${true} | ${false} | ${false} | ${[]} + ${{ viewEditPage: true, removeProject: true }} | ${true} | ${true} | ${true} | ${false} | ${[]} + ${{ archiveProject: true }} | ${false} | ${false} | ${false} | ${false} | ${[ACTION_ARCHIVE]} + ${{ archiveProject: true }} | ${false} | ${false} | ${false} | ${true} | ${[ACTION_UNARCHIVE]} + ${{ archiveProject: false }} | ${false} | ${false} | ${false} | ${false} | ${[]} + ${{ archiveProject: false }} | ${false} | ${false} | ${false} | ${true} | ${[]} `( 'availableGraphQLProjectActions', - ({ userPermissions, markedForDeletionOn, archived, availableActions }) => { - it(`when userPermissions = ${JSON.stringify(userPermissions)}, markedForDeletionOn is ${markedForDeletionOn}, and archived is ${archived} then availableActions = [${availableActions}] and is sorted correctly`, () => { + ({ + userPermissions, + markedForDeletion, + isSelfDeletionInProgress, + isSelfDeletionScheduled, + archived, + availableActions, + }) => { + 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({ userPermissions, markedForDeletionOn, archived }), + availableGraphQLProjectActions({ + userPermissions, + markedForDeletion, + isSelfDeletionInProgress, + isSelfDeletionScheduled, + archived, + }), ).toStrictEqual(availableActions); }); }, ); + + describe('when disallowImmediateDeletion feature flag is enabled', () => { + beforeEach(() => { + window.gon = { + features: { + disallowImmediateDeletion: true, + }, + }; + }); + + it('does not allow deleting immediately', () => { + expect( + availableGraphQLProjectActions({ + userPermissions: { viewEditPage: true, removeProject: true }, + markedForDeletion: true, + isSelfDeletionInProgress: false, + isSelfDeletionScheduled: true, + }), + ).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]); + }); + }); + }); }); describe('renderArchiveSuccessToast', () => { diff --git a/spec/frontend/vue_shared/components/resource_lists/list_item_description_spec.js b/spec/frontend/vue_shared/components/resource_lists/list_item_description_spec.js index d8bd926007be71b2ebedcd2ed0f7493f845fb8e3..8fcfee5b5821e96baa989dd72a1cbf4a9b1f0173 100644 --- a/spec/frontend/vue_shared/components/resource_lists/list_item_description_spec.js +++ b/spec/frontend/vue_shared/components/resource_lists/list_item_description_spec.js @@ -78,7 +78,7 @@ describe('ListItemDescription', () => { props: { resource: { ...project, - markedForDeletionOn: '2024-12-24', + markedForDeletion: true, permanentDeletionDate: '2024-12-31', }, }, diff --git a/spec/frontend/vue_shared/components/resource_lists/list_item_inactive_badge_spec.js b/spec/frontend/vue_shared/components/resource_lists/list_item_inactive_badge_spec.js index b6a7af197e5e9de3295940b79c3ba66f620ad81a..3d8515e3033e2a32f6625c1a1a567a7a9e5375f1 100644 --- a/spec/frontend/vue_shared/components/resource_lists/list_item_inactive_badge_spec.js +++ b/spec/frontend/vue_shared/components/resource_lists/list_item_inactive_badge_spec.js @@ -21,25 +21,25 @@ describe('ListItemInactiveBadge', () => { const findGlBadge = () => wrapper.findComponent(GlBadge); describe.each` - isSelfDeletionInProgress | markedForDeletionOn | archived | variant | text - ${true} | ${'2024-01-01'} | ${true} | ${'warning'} | ${'Deletion in progress'} - ${true} | ${'2024-01-01'} | ${false} | ${'warning'} | ${'Deletion in progress'} - ${true} | ${null} | ${true} | ${'warning'} | ${'Deletion in progress'} - ${true} | ${null} | ${false} | ${'warning'} | ${'Deletion in progress'} - ${false} | ${'2024-01-01'} | ${true} | ${'warning'} | ${'Pending deletion'} - ${false} | ${'2024-01-01'} | ${false} | ${'warning'} | ${'Pending deletion'} - ${false} | ${null} | ${true} | ${'info'} | ${'Archived'} - ${false} | ${null} | ${false} | ${false} | ${false} + isSelfDeletionInProgress | markedForDeletion | archived | variant | text + ${true} | ${true} | ${true} | ${'warning'} | ${'Deletion in progress'} + ${true} | ${true} | ${false} | ${'warning'} | ${'Deletion in progress'} + ${true} | ${false} | ${true} | ${'warning'} | ${'Deletion in progress'} + ${true} | ${false} | ${false} | ${'warning'} | ${'Deletion in progress'} + ${false} | ${true} | ${true} | ${'warning'} | ${'Pending deletion'} + ${false} | ${true} | ${false} | ${'warning'} | ${'Pending deletion'} + ${false} | ${false} | ${true} | ${'info'} | ${'Archived'} + ${false} | ${false} | ${false} | ${false} | ${false} `( - 'when isSelfDeletionInProgress=$isSelfDeletionInProgress, markedForDeletionOn=markedForDeletionOn, archived=$archived', - ({ isSelfDeletionInProgress, markedForDeletionOn, archived, variant, text }) => { + 'when isSelfDeletionInProgress=$isSelfDeletionInProgress, markedForDeletion=markedForDeletion, archived=$archived', + ({ isSelfDeletionInProgress, markedForDeletion, archived, variant, text }) => { beforeEach(() => { createComponent({ props: { resource: { ...resource, archived, - markedForDeletionOn, + markedForDeletion, isSelfDeletionInProgress, }, },