diff --git a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js index 3bc9b3a4a8..504f72b126 100644 --- a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +++ b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js @@ -25,9 +25,7 @@ export function InformationBoxEE() { const { formatMessage } = useIntl(); const { formatAPIError } = useAPIErrorHandler(); - const { - workflows: { data: workflow }, - } = useReviewWorkflows(activeWorkflowStage?.id); + const { workflows: { data: [workflow] = [] } = {} } = useReviewWorkflows(); const { error, isLoading, mutateAsync } = useMutation(async ({ entityId, stageId, uid }) => { const { 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..5a30dea3fc 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,12 @@ 'use strict'; +const { ApplicationError } = require('@strapi/utils/lib/errors'); const { getService } = require('../../../utils'); -const { validateUpdateStages } = require('../../../validation/review-workflows'); +const { hasReviewWorkflow } = require('../../../utils/review-workflows'); +const { + validateUpdateStages, + validateUpdateStageOnEntity, +} = require('../../../validation/review-workflows'); module.exports = { /** @@ -54,4 +59,37 @@ module.exports = { ctx.body = { data }; }, + + /** + * Updates an entity's stage. + * @async + * @param {Object} ctx - The Koa context object. + * @param {Object} ctx.params - An object containing the parameters from the request URL. + * @param {string} ctx.params.model_uid - The model UID of the entity. + * @param {string} ctx.params.id - The ID of the entity to update. + * @param {Object} ctx.request.body.data - Optional data object containing the new stage ID for the entity. + * @param {string} ctx.request.body.data.id - The ID of the new stage for the entity. + * @throws {ApplicationError} If review workflows is not activated on the specified model UID. + * @throws {ValidationError} If the `data` object in the request body fails to pass validation. + * @returns {Promise} A promise that resolves when the entity's stage has been updated. + */ + async updateEntity(ctx) { + const stagesService = getService('stages'); + const { model_uid: modelUID, id: entityIdString } = ctx.params; + const entityId = Number(entityIdString); + + const { id: stageId } = await validateUpdateStageOnEntity( + ctx.request?.body?.data, + 'You should pass an id to the body of the put request.' + ); + + if (!hasReviewWorkflow({ 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/migrations/review-workflows.js b/packages/core/admin/ee/server/migrations/review-workflows.js index 8b04d69f79..a86c007a11 100644 --- a/packages/core/admin/ee/server/migrations/review-workflows.js +++ b/packages/core/admin/ee/server/migrations/review-workflows.js @@ -1,6 +1,6 @@ 'use strict'; -const { hasRWEnabled } = require('../utils/review-workflows'); +const { hasReviewWorkflow } = require('../utils/review-workflows'); /** * Remove all stage information for all content types that have had review workflows disabled @@ -16,7 +16,10 @@ const disableOnContentTypes = async ({ oldContentTypes, contentTypes }) => { const oldContentType = oldContentTypes[uid]; const contentType = contentTypes?.[uid]; - if (hasRWEnabled(oldContentType) && !hasRWEnabled(contentType)) { + if ( + hasReviewWorkflow({ strapi }, oldContentType) && + !hasReviewWorkflow({ strapi }, contentType) + ) { // If review workflows has been turned off on a content type // remove stage information from all entities within this CT uidsToRemove.push(uid); 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/entity-service-decorator.js b/packages/core/admin/ee/server/services/review-workflows/entity-service-decorator.js index 121af298e4..01bb383488 100644 --- a/packages/core/admin/ee/server/services/review-workflows/entity-service-decorator.js +++ b/packages/core/admin/ee/server/services/review-workflows/entity-service-decorator.js @@ -2,7 +2,7 @@ const { isNil } = require('lodash/fp'); const { ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows'); -const { hasRWEnabled, getDefaultWorkflow } = require('../../utils/review-workflows'); +const { hasReviewWorkflow, getDefaultWorkflow } = require('../../utils/review-workflows'); /** * Assigns the entity data to the default workflow stage if no stage is present in the data @@ -23,8 +23,7 @@ const getDataWithStage = async (data) => { */ const decorator = (service) => ({ async create(uid, opts = {}) { - const model = strapi.getModel(uid); - const hasRW = hasRWEnabled(model); + const hasRW = hasReviewWorkflow({ strapi }, uid); if (!hasRW) { return service.create.call(this, uid, opts); 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..25726355cd 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 @@ -120,7 +120,7 @@ function enableReviewWorkflow({ strapi }) { [joinTable.joinColumn.name]: firstStage.id, [typeColumn.name]: connection.raw('?', [contentTypeUID]), }) - .leftJoin(`${joinTable.name} AS jointable`, function () { + .leftJoin(`${joinTable.name} AS jointable`, () => { this.on('entity.id', '=', `jointable.${idColumn.name}`).andOn( `jointable.${typeColumn.name}`, '=', 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..1fe1f88190 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 {number} entityInfo.id - Entity id + * @param {string} entityInfo.modelUID - the content-type of the entity + * @param {number} 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]: 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/utils/review-workflows.js b/packages/core/admin/ee/server/utils/review-workflows.js index ca5ece48e9..5531fd2578 100644 --- a/packages/core/admin/ee/server/utils/review-workflows.js +++ b/packages/core/admin/ee/server/utils/review-workflows.js @@ -3,17 +3,23 @@ const { WORKFLOW_MODEL_UID } = require('../constants/workflows'); /** - * Determine if a content type has the review workflows feature enabled - * @param {Object} contentType - * @returns + * Checks if a content type has review workflows enabled. + * @param {string|Object} contentType - Either the modelUID of the content type, or the content type object. + * @returns {boolean} Whether review workflows are enabled for the specified content type. */ -const hasRWEnabled = (contentType) => contentType?.options?.reviewWorkflows || false; - +function hasReviewWorkflow({ strapi }, contentType) { + if (typeof contentType === 'string') { + // If the input is a string, assume it's the modelUID of the content type and retrieve the corresponding object. + return hasReviewWorkflow({ strapi }, strapi.getModel(contentType)); + } + // Otherwise, assume it's the content type object itself and return its `reviewWorkflows` option if it exists. + return contentType?.options?.reviewWorkflows || false; +} // TODO To be refactored when multiple workflows are added const getDefaultWorkflow = async ({ strapi }) => strapi.query(WORKFLOW_MODEL_UID).findOne({ populate: ['stages'] }); module.exports = { - hasRWEnabled, + hasReviewWorkflow, getDefaultWorkflow, }; 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), };