diff --git a/api-tests/core/admin/admin-permission.test.api.js b/api-tests/core/admin/admin-permission.test.api.js index 96a952e8ce..5976e483c2 100644 --- a/api-tests/core/admin/admin-permission.test.api.js +++ b/api-tests/core/admin/admin-permission.test.api.js @@ -379,6 +379,12 @@ describe('Role CRUD End to End', () => { "displayName": "Create", "subCategory": "options", }, + { + "action": "admin::review-workflows.delete", + "category": "review workflows", + "displayName": "Delete", + "subCategory": "options", + }, { "action": "admin::review-workflows.read", "category": "review workflows", diff --git a/api-tests/core/admin/ee/review-workflows-content-types.test.api.js b/api-tests/core/admin/ee/review-workflows-content-types.test.api.js index 8bf2bc4856..644399cb0e 100644 --- a/api-tests/core/admin/ee/review-workflows-content-types.test.api.js +++ b/api-tests/core/admin/ee/review-workflows-content-types.test.api.js @@ -54,6 +54,10 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); }; + const deleteWorkflow = async (id) => { + return requests.admin.delete(`/admin/review-workflows/workflows/${id}`); + }; + const getWorkflow = async (id) => { const { body } = await requests.admin.get(`/admin/review-workflows/workflows/${id}?populate=*`); return body.data; @@ -106,7 +110,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { let workflow1, workflow2; describe('Create workflow and assign content type', () => { - test('Can create and assign a content type', async () => { + test('It should create a workflow and assign a content type', async () => { const res = await createWorkflow({ contentTypes: [productUID] }); expect(res.status).toBe(200); @@ -114,7 +118,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { workflow1 = res.body.data; }); - test('All product entities have the first stage', async () => { + test('All product entities should have the first stage', async () => { const products = await findAll(productUID); expect(products.results).toHaveLength(2); @@ -125,7 +129,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); describe('Create workflow and steal content type from another workflow', () => { - test('Can create workflow stealing content type from another', async () => { + test('It should create workflow stealing content type from another', async () => { const res = await createWorkflow({ contentTypes: [productUID], stages: [{ name: 'Review' }], @@ -136,7 +140,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { workflow2 = res.body.data; }); - test('All product entities have the new first stage', async () => { + test('All product entities should have the new first stage', async () => { const products = await findAll(productUID); expect(products.results).toHaveLength(2); @@ -145,13 +149,13 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { } }); - test('Original workflow is updated', async () => { + test('Original workflow should be updated', async () => { const workflow = await getWorkflow(workflow1.id); expect(workflow).toMatchObject({ contentTypes: [] }); }); }); - test('Can not create with invalid content type', async () => { + test("It shouldn't create a workflow with invalid content type", async () => { const res = await createWorkflow({ contentTypes: ['someUID'] }); expect(res.status).toBe(400); }); @@ -161,7 +165,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { let workflow1, workflow2; describe('Basic update', () => { - test('Can assign a content type', async () => { + test('It should assign a content type', async () => { workflow1 = await createWorkflow({ contentTypes: [] }).then((res) => res.body.data); const res = await updateWorkflow(workflow1.id, { @@ -172,7 +176,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { expect(res.body.data).toMatchObject({ contentTypes: [productUID] }); }); - test('All product entities have the first stage', async () => { + test('All product entities should have the first stage', async () => { const products = await findAll(productUID); expect(products.results).toHaveLength(2); @@ -184,14 +188,14 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { // Depends on the previous test describe('Steal content type', () => { - test('Can steal content type from another', async () => { + test('It should be able to steal a content type from another workflow', async () => { workflow2 = await createWorkflow({ contentTypes: [] }).then((res) => res.body.data); const res = await updateWorkflow(workflow2.id, { contentTypes: [productUID] }); expect(res.status).toBe(200); expect(res.body.data).toMatchObject({ contentTypes: [productUID] }); }); - test('All product entities have the new first stage', async () => { + test('All product entities should have the new first stage', async () => { const products = await findAll(productUID); expect(products.results).toHaveLength(2); @@ -200,7 +204,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { } }); - test('Original workflow is updated', async () => { + test('Original workflow should be updated', async () => { const workflow = await getWorkflow(workflow1.id); expect(workflow).toMatchObject({ contentTypes: [] }); }); @@ -208,13 +212,13 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { // Depends on the previous test describe('Unassign content type', () => { - test('Can unassign content type', async () => { + test('It should unassign content type', async () => { const res = await updateWorkflow(workflow2.id, { contentTypes: [] }); expect(res.status).toBe(200); expect(res.body.data).toMatchObject({ contentTypes: [] }); }); - test('All product entities have null stage', async () => { + test('All product entities should have null stage', async () => { const products = await findAll(productUID); expect(products.results).toHaveLength(2); @@ -225,7 +229,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); describe('Assign and update stages', () => { - test('Can assign and update stages', async () => { + test('It should assign and update stages', async () => { workflow1 = await createWorkflow({ contentTypes: [] }).then((res) => res.body.data); // Update stages @@ -241,7 +245,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); }); - test('All product entities have the new first stage', async () => { + test('All product entities should have the new first stage', async () => { const products = await findAll(productUID); expect(products.results).toHaveLength(2); @@ -253,9 +257,28 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); }); - describe('Creating an entity in a review workflow content type', () => { + describe('Delete workflow', () => { let workflow; - test('when content type is assigned to workflow, new entries should be added to the first stage of the default workflow', async () => { + test('It should delete the workflow', async () => { + workflow = await createWorkflow({ contentTypes: [productUID] }).then((res) => res.body.data); + + const res = await deleteWorkflow(workflow.id); + expect(res.status).toBe(200); + }); + + // Depends on the previous test + test('All entities should have null stage', async () => { + const products = await findAll(productUID); + + expect(products.results).toHaveLength(2); + for (const product of products.results) { + expect(product[ENTITY_STAGE_ATTRIBUTE]).toBeNull(); + } + }); + }); + describe('Creating entity assigned to a workflow', () => { + let workflow; + test('When content type is assigned to workflow, new entries should be added to the first stage of the default workflow', async () => { // Create a workflow with product content type workflow = await createWorkflow({ contentTypes: [productUID] }).then((res) => res.body.data); @@ -264,7 +287,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); // Depends on the previous test - test('when content type is not assigned to workflow, new entries should have a null stage', async () => { + test('When content type is not assigned to workflow, new entries should have a null stage', async () => { // Unassign product content type from default workflow await updateWorkflow(workflow.id, { contentTypes: [] }); diff --git a/api-tests/core/admin/ee/review-workflows.test.api.js b/api-tests/core/admin/ee/review-workflows.test.api.js index ac574db7de..d1ccb393dd 100644 --- a/api-tests/core/admin/ee/review-workflows.test.api.js +++ b/api-tests/core/admin/ee/review-workflows.test.api.js @@ -177,7 +177,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); describe('Create workflow', () => { - test('You can not create a workflow without stages', async () => { + test('It should create a workflow without stages', async () => { const res = await requests.admin.post('/admin/review-workflows/workflows', { body: { name: 'testWorkflow', @@ -193,7 +193,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { expect(res.body.data).toBeUndefined(); } }); - test('You can create a workflow with stages', async () => { + test('It should create a workflow with stages', async () => { const res = await requests.admin.post('/admin/review-workflows/workflows?populate=stages', { body: { name: 'createdWorkflow', @@ -217,7 +217,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); describe('Update workflow', () => { - test('You can update a workflow', async () => { + test('It should update a workflow', async () => { const res = await requests.admin.put( `/admin/review-workflows/workflows/${createdWorkflow.id}`, { body: { name: 'updatedWorkflow' } } @@ -232,7 +232,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { } }); - test('You can update a workflow with stages', async () => { + test('It should update a workflow with stages', async () => { const res = await requests.admin.put( `/admin/review-workflows/workflows/${createdWorkflow.id}?populate=stages`, { @@ -262,6 +262,27 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); }); + describe('Delete workflow', () => { + test('It should delete a workflow', async () => { + const createdRes = await requests.admin.post('/admin/review-workflows/workflows', { + body: { name: 'testWorkflow', stages: [{ name: 'Stage 1' }] }, + }); + + const res = await requests.admin.delete( + `/admin/review-workflows/workflows/${createdRes.body.data.id}` + ); + + expect(res.status).toBe(200); + expect(res.body.data).toMatchObject({ name: 'testWorkflow' }); + }); + test("It shouldn't delete a workflow that does not exist", async () => { + const res = await requests.admin.delete(`/admin/review-workflows/workflows/123456789`); + + expect(res.status).toBe(404); + expect(res.body.data).toBeNull(); + }); + }); + describe('Get workflow stages', () => { test("It shouldn't be available for public", async () => { const res = await requests.public.get( diff --git a/packages/core/admin/ee/server/config/admin-actions.js b/packages/core/admin/ee/server/config/admin-actions.js index 4429062bf9..91e373ad59 100644 --- a/packages/core/admin/ee/server/config/admin-actions.js +++ b/packages/core/admin/ee/server/config/admin-actions.js @@ -46,6 +46,14 @@ module.exports = { category: 'review workflows', subCategory: 'options', }, + { + uid: 'review-workflows.delete', + displayName: 'Delete', + pluginName: 'admin', + section: 'settings', + category: 'review workflows', + subCategory: 'options', + }, { uid: 'review-workflows.read', displayName: 'Read', diff --git a/packages/core/admin/ee/server/controllers/workflows/index.js b/packages/core/admin/ee/server/controllers/workflows/index.js index 208ec20cc3..a430b20dab 100644 --- a/packages/core/admin/ee/server/controllers/workflows/index.js +++ b/packages/core/admin/ee/server/controllers/workflows/index.js @@ -50,6 +50,27 @@ module.exports = { }; }, + /** + * Delete a workflow + * @param {import('koa').BaseContext} ctx - koa context + */ + async delete(ctx) { + const { id } = ctx.params; + const { populate } = ctx.query; + const workflowService = getService('workflows'); + + const workflow = await workflowService.findById(id, { populate: ['stages'] }); + if (!workflow) { + return ctx.notFound("Workflow doesn't exist"); + } + + const data = await workflowService.delete(workflow, { populate }); + + ctx.body = { + data, + }; + }, + /** * List all workflows * @param {import('koa').BaseContext} ctx - koa context diff --git a/packages/core/admin/ee/server/routes/review-workflows.js b/packages/core/admin/ee/server/routes/review-workflows.js index a859c0a394..dd1fe19c5d 100644 --- a/packages/core/admin/ee/server/routes/review-workflows.js +++ b/packages/core/admin/ee/server/routes/review-workflows.js @@ -40,6 +40,23 @@ module.exports = { ], }, }, + { + method: 'DELETE', + path: '/review-workflows/workflows/:id', + handler: 'workflows.delete', + config: { + middlewares: [enableFeatureMiddleware('review-workflows')], + policies: [ + 'admin::isAuthenticatedAdmin', + { + name: 'admin::hasPermissions', + config: { + actions: ['admin::review-workflows.delete'], + }, + }, + ], + }, + }, { method: 'GET', path: '/review-workflows/workflows', diff --git a/packages/core/admin/ee/server/services/review-workflows/stages.js b/packages/core/admin/ee/server/services/review-workflows/stages.js index 6be23203c7..2b5e499f9d 100644 --- a/packages/core/admin/ee/server/services/review-workflows/stages.js +++ b/packages/core/admin/ee/server/services/review-workflows/stages.js @@ -60,6 +60,12 @@ module.exports = ({ strapi }) => { return stage; }, + async deleteMany(stagesId) { + return strapi.entityService.deleteMany(STAGE_MODEL_UID, { + filters: { id: { $in: stagesId } }, + }); + }, + count() { return strapi.entityService.count(STAGE_MODEL_UID); }, diff --git a/packages/core/admin/ee/server/services/review-workflows/workflows/index.js b/packages/core/admin/ee/server/services/review-workflows/workflows/index.js index 03e3152f4a..07b55ba6fc 100644 --- a/packages/core/admin/ee/server/services/review-workflows/workflows/index.js +++ b/packages/core/admin/ee/server/services/review-workflows/workflows/index.js @@ -122,6 +122,36 @@ module.exports = ({ strapi }) => { }); }, + /** + * Deletes an existing workflow. + * Also deletes all the workflow stages and migrate all assigned the content types. + * @param {*} workflow + * @param {*} opts + * @returns + */ + async delete(workflow, opts) { + const stageService = getService('stages', { strapi }); + + const workflowCount = await this.count(); + + if (workflowCount <= 1) { + throw new ApplicationError('Can not delete the last workflow'); + } + + return strapi.db.transaction(async () => { + // Delete stages + await stageService.deleteMany(workflow.stages.map((stage) => stage.id)); + + // Unassign all content types, this will migrate the content types to null + await workflowsContentTypes.migrate({ + srcContentTypes: workflow.contentTypes, + destContentTypes: [], + }); + + // Delete Workflow + return strapi.entityService.delete(WORKFLOW_MODEL_UID, workflow.id, opts); + }); + }, /** * Returns the total count of workflows. * @returns {Promise} - Total count of workflows.