diff --git a/packages/core/admin/ee/server/controllers/workflows/stages/index.js b/packages/core/admin/ee/server/controllers/workflows/stages/index.js index 3cecc9430d..f3833bdd90 100644 --- a/packages/core/admin/ee/server/controllers/workflows/stages/index.js +++ b/packages/core/admin/ee/server/controllers/workflows/stages/index.js @@ -1,7 +1,11 @@ 'use strict'; +const { ApplicationError } = require('@strapi/utils/lib/errors'); const { getService } = require('../../../utils'); -const { validateUpdateStages } = require('../../../validation/review-workflows'); +const { + validateUpdateStages, + validateUpdateStageOnEntity, +} = require('../../../validation/review-workflows'); module.exports = { /** @@ -54,4 +58,24 @@ module.exports = { ctx.body = { data }; }, + + async updateEntity(ctx) { + const stagesService = getService('stages'); + const reviewWorkflowsService = getService('review-workflows'); + const { model_uid: modelUID, id: entityId } = ctx.params; + + const { id: stageId } = await validateUpdateStageOnEntity( + ctx.request?.body?.data, + 'You shall pass an id to the body of the put request.' + ); + + if (!reviewWorkflowsService.isReviewWorkflowEnabled({ strapi }, modelUID)) { + throw new ApplicationError(`Review workflows is not activated on ${modelUID}.`); + } + + // TODO When multiple workflows are possible, check if the stage is part of the right one + // Didn't need this today as their can only be one workflow + + ctx.body = await stagesService.updateEntity({ id: entityId, modelUID }, stageId); + }, }; diff --git a/packages/core/admin/ee/server/routes/index.js b/packages/core/admin/ee/server/routes/index.js index f5f1cec2b0..e789639768 100644 --- a/packages/core/admin/ee/server/routes/index.js +++ b/packages/core/admin/ee/server/routes/index.js @@ -209,4 +209,21 @@ module.exports = [ ], }, }, + { + method: 'PUT', + path: '/content-manager/(collection|single)-types/:model_uid/:id/stage', + handler: 'stages.updateEntity', + config: { + middlewares: [enableFeatureMiddleware('review-workflows')], + policies: [ + 'admin::isAuthenticatedAdmin', + { + name: 'admin::hasPermissions', + config: { + actions: ['admin::review-workflows.read'], + }, + }, + ], + }, + }, ]; diff --git a/packages/core/admin/ee/server/services/review-workflows/review-workflows.js b/packages/core/admin/ee/server/services/review-workflows/review-workflows.js index bc1cc613f0..faa4c04d5c 100644 --- a/packages/core/admin/ee/server/services/review-workflows/review-workflows.js +++ b/packages/core/admin/ee/server/services/review-workflows/review-workflows.js @@ -11,7 +11,7 @@ const { ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows'); const { disableOnContentTypes: disableReviewWorkflows, } = require('../../migrations/review-workflows'); -const { getDefaultWorkflow } = require('../../utils/review-workflows'); +const { getDefaultWorkflow, hasRWEnabled } = require('../../utils/review-workflows'); const getContentTypeUIDsWithActivatedReviewWorkflows = pipe([ // Pick only content-types with reviewWorkflows options set to true @@ -170,5 +170,9 @@ module.exports = ({ strapi }) => { strapi.hook('strapi::content-types.afterSync').register(enableReviewWorkflow({ strapi })); strapi.hook('strapi::content-types.afterSync').register(disableReviewWorkflows); }, + isReviewWorkflowEnabled({ strapi }, modelUID) { + const contentType = strapi.container.get('content-types').get(modelUID); + return hasRWEnabled(contentType); + }, }; }; 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 79adacfaf5..8a24900a7c 100644 --- a/packages/core/admin/ee/server/services/review-workflows/stages.js +++ b/packages/core/admin/ee/server/services/review-workflows/stages.js @@ -5,7 +5,7 @@ const { errors: { ApplicationError }, } = require('@strapi/utils'); -const { STAGE_MODEL_UID } = require('../../constants/workflows'); +const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows'); const { getService } = require('../../utils'); module.exports = ({ strapi }) => { @@ -69,6 +69,21 @@ module.exports = ({ strapi }) => { }); }); }, + + /** + * Update the stage of an entity + * + * @param {object} entityInfo + * @param {string} entityInfo.id - Entity id + * @param {string} entityInfo.modelUID - the content-type of the entity + * @param {string} stageId - The id of the stage to assign to the entity + */ + updateEntity(entityInfo, stageId) { + return strapi.entityService.update(entityInfo.modelUID, entityInfo.id, { + data: { [ENTITY_STAGE_ATTRIBUTE]: Number(stageId) }, + populate: [ENTITY_STAGE_ATTRIBUTE], + }); + }, }; }; diff --git a/packages/core/admin/ee/server/tests/review-workflows.test.api.js b/packages/core/admin/ee/server/tests/review-workflows.test.api.js index ad4296ab0b..ff7fcda880 100644 --- a/packages/core/admin/ee/server/tests/review-workflows.test.api.js +++ b/packages/core/admin/ee/server/tests/review-workflows.test.api.js @@ -101,6 +101,20 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { await builder.cleanup(); }); + beforeEach(async () => { + testWorkflow = await strapi.query(WORKFLOW_MODEL_UID).update({ + where: { id: testWorkflow.id }, + data: { + uid: 'workflow', + stages: [defaultStage.id, secondStage.id], + }, + }); + await updateContentType(productUID, { + components: [], + contentType: model, + }); + }); + describe('Get workflows', () => { test("It shouldn't be available for public", async () => { const res = await requests.public.get('/admin/review-workflows/workflows'); @@ -328,8 +342,6 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); describe('Enabling/Disabling review workflows on a content type', () => { - let response; - beforeAll(async () => { await createEntry(productUID, { name: 'Product' }); await createEntry(productUID, { name: 'Product 1' }); @@ -343,18 +355,18 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); await restart(); - response = await requests.admin({ + const response = await requests.admin({ method: 'GET', url: `/content-type-builder/content-types/api::product.product`, }); expect(response.body.data.schema.reviewWorkflows).toBeTruthy(); - response = await getRWMorphTableResults(strapi.db.getConnection()); + const morphTableResults = await getRWMorphTableResults(strapi.db.getConnection()); - expect(response.length).toEqual(3); - for (let i = 0; i < response.length; i += 1) { - const entry = response[i]; + expect(morphTableResults.length).toEqual(3); + for (let i = 0; i < morphTableResults.length; i += 1) { + const entry = morphTableResults[i]; expect(entry.related_id).toEqual(i + 1); expect(entry.order).toEqual(1); } @@ -368,14 +380,66 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { await restart(); - response = await requests.admin({ + const response = await requests.admin({ method: 'GET', url: `/content-type-builder/content-types/api::product.product`, }); expect(response.body.data.schema.reviewWorkflows).toBeFalsy(); - response = await getRWMorphTableResults(strapi.db.getConnection()); - expect(response.length).toEqual(0); + const morphTableResults = await getRWMorphTableResults(strapi.db.getConnection()); + expect(morphTableResults.length).toEqual(0); + }); + }); + + describe('update a stage on an entity', () => { + describe('Review Workflow is enabled', () => { + beforeAll(async () => { + await updateContentType(productUID, { + components: [], + contentType: { ...model, reviewWorkflows: true }, + }); + await restart(); + }); + test('Should update the accordingly on an entity', async () => { + const entry = await createEntry(productUID, { name: 'Product' }); + + const response = await requests.admin({ + method: 'PUT', + url: `/admin/content-manager/collection-types/${productUID}/${entry.id}/stage`, + body: { + data: { id: secondStage.id }, + }, + }); + + expect(response.status).toEqual(200); + expect(response.body[ENTITY_STAGE_ATTRIBUTE]).toEqual( + expect.objectContaining({ id: secondStage.id }) + ); + }); + }); + describe('Review Workflow is disabled', () => { + beforeAll(async () => { + await updateContentType(productUID, { + components: [], + contentType: { ...model, reviewWorkflows: false }, + }); + await restart(); + }); + test('Should not update the entity', async () => { + const entry = await createEntry(productUID, { name: 'Product' }); + + const response = await requests.admin({ + method: 'PUT', + url: `/admin/content-manager/collection-types/${productUID}/${entry.id}/stage`, + body: { + data: { id: secondStage.id }, + }, + }); + + expect(response.status).toEqual(400); + expect(response.body.error).toBeDefined(); + expect(response.body.error.name).toBe('ApplicationError'); + }); }); }); diff --git a/packages/core/admin/ee/server/validation/review-workflows.js b/packages/core/admin/ee/server/validation/review-workflows.js index caad7726a7..4f67c8645f 100644 --- a/packages/core/admin/ee/server/validation/review-workflows.js +++ b/packages/core/admin/ee/server/validation/review-workflows.js @@ -8,10 +8,17 @@ const stageObject = yup.object().shape({ }); const validateUpdateStagesSchema = yup.array().of(stageObject).required(); +const validateUpdateStageOnEntity = yup + .object() + .shape({ + id: yup.number().integer().min(1).required(), + }) + .required(); module.exports = { validateUpdateStages: validateYupSchema(validateUpdateStagesSchema, { strict: false, stripUnknown: true, }), + validateUpdateStageOnEntity: validateYupSchema(validateUpdateStageOnEntity), };