mirror of
https://github.com/strapi/strapi.git
synced 2025-09-17 20:40:17 +00:00
Merge pull request #16207 from strapi/feature/review-workflow-entity-change-stage
feat(review-workflow): add route to update a stage on an entity
This commit is contained in:
commit
e956656c81
@ -25,9 +25,7 @@ export function InformationBoxEE() {
|
|||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { formatAPIError } = useAPIErrorHandler();
|
const { formatAPIError } = useAPIErrorHandler();
|
||||||
|
|
||||||
const {
|
const { workflows: { data: [workflow] = [] } = {} } = useReviewWorkflows();
|
||||||
workflows: { data: workflow },
|
|
||||||
} = useReviewWorkflows(activeWorkflowStage?.id);
|
|
||||||
|
|
||||||
const { error, isLoading, mutateAsync } = useMutation(async ({ entityId, stageId, uid }) => {
|
const { error, isLoading, mutateAsync } = useMutation(async ({ entityId, stageId, uid }) => {
|
||||||
const {
|
const {
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const { ApplicationError } = require('@strapi/utils/lib/errors');
|
||||||
const { getService } = require('../../../utils');
|
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 = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
@ -54,4 +59,37 @@ module.exports = {
|
|||||||
|
|
||||||
ctx.body = { data };
|
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<void>} 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);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'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
|
* 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 oldContentType = oldContentTypes[uid];
|
||||||
const contentType = contentTypes?.[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
|
// If review workflows has been turned off on a content type
|
||||||
// remove stage information from all entities within this CT
|
// remove stage information from all entities within this CT
|
||||||
uidsToRemove.push(uid);
|
uidsToRemove.push(uid);
|
||||||
|
@ -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'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const { isNil } = require('lodash/fp');
|
const { isNil } = require('lodash/fp');
|
||||||
const { ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
|
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
|
* 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) => ({
|
const decorator = (service) => ({
|
||||||
async create(uid, opts = {}) {
|
async create(uid, opts = {}) {
|
||||||
const model = strapi.getModel(uid);
|
const hasRW = hasReviewWorkflow({ strapi }, uid);
|
||||||
const hasRW = hasRWEnabled(model);
|
|
||||||
|
|
||||||
if (!hasRW) {
|
if (!hasRW) {
|
||||||
return service.create.call(this, uid, opts);
|
return service.create.call(this, uid, opts);
|
||||||
|
@ -120,7 +120,7 @@ function enableReviewWorkflow({ strapi }) {
|
|||||||
[joinTable.joinColumn.name]: firstStage.id,
|
[joinTable.joinColumn.name]: firstStage.id,
|
||||||
[typeColumn.name]: connection.raw('?', [contentTypeUID]),
|
[typeColumn.name]: connection.raw('?', [contentTypeUID]),
|
||||||
})
|
})
|
||||||
.leftJoin(`${joinTable.name} AS jointable`, function () {
|
.leftJoin(`${joinTable.name} AS jointable`, () => {
|
||||||
this.on('entity.id', '=', `jointable.${idColumn.name}`).andOn(
|
this.on('entity.id', '=', `jointable.${idColumn.name}`).andOn(
|
||||||
`jointable.${typeColumn.name}`,
|
`jointable.${typeColumn.name}`,
|
||||||
'=',
|
'=',
|
||||||
|
@ -5,7 +5,7 @@ const {
|
|||||||
errors: { ApplicationError },
|
errors: { ApplicationError },
|
||||||
} = require('@strapi/utils');
|
} = require('@strapi/utils');
|
||||||
|
|
||||||
const { STAGE_MODEL_UID } = require('../../constants/workflows');
|
const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
|
||||||
const { getService } = require('../../utils');
|
const { getService } = require('../../utils');
|
||||||
|
|
||||||
module.exports = ({ strapi }) => {
|
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],
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -101,6 +101,20 @@ describeOnCondition(edition === 'EE')('Review workflows', () => {
|
|||||||
await builder.cleanup();
|
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', () => {
|
describe('Get workflows', () => {
|
||||||
test("It shouldn't be available for public", async () => {
|
test("It shouldn't be available for public", async () => {
|
||||||
const res = await requests.public.get('/admin/review-workflows/workflows');
|
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', () => {
|
describe('Enabling/Disabling review workflows on a content type', () => {
|
||||||
let response;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await createEntry(productUID, { name: 'Product' });
|
await createEntry(productUID, { name: 'Product' });
|
||||||
await createEntry(productUID, { name: 'Product 1' });
|
await createEntry(productUID, { name: 'Product 1' });
|
||||||
@ -343,18 +355,18 @@ describeOnCondition(edition === 'EE')('Review workflows', () => {
|
|||||||
});
|
});
|
||||||
await restart();
|
await restart();
|
||||||
|
|
||||||
response = await requests.admin({
|
const response = await requests.admin({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `/content-type-builder/content-types/api::product.product`,
|
url: `/content-type-builder/content-types/api::product.product`,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.body.data.schema.reviewWorkflows).toBeTruthy();
|
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);
|
expect(morphTableResults.length).toEqual(3);
|
||||||
for (let i = 0; i < response.length; i += 1) {
|
for (let i = 0; i < morphTableResults.length; i += 1) {
|
||||||
const entry = response[i];
|
const entry = morphTableResults[i];
|
||||||
expect(entry.related_id).toEqual(i + 1);
|
expect(entry.related_id).toEqual(i + 1);
|
||||||
expect(entry.order).toEqual(1);
|
expect(entry.order).toEqual(1);
|
||||||
}
|
}
|
||||||
@ -368,14 +380,66 @@ describeOnCondition(edition === 'EE')('Review workflows', () => {
|
|||||||
|
|
||||||
await restart();
|
await restart();
|
||||||
|
|
||||||
response = await requests.admin({
|
const response = await requests.admin({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `/content-type-builder/content-types/api::product.product`,
|
url: `/content-type-builder/content-types/api::product.product`,
|
||||||
});
|
});
|
||||||
expect(response.body.data.schema.reviewWorkflows).toBeFalsy();
|
expect(response.body.data.schema.reviewWorkflows).toBeFalsy();
|
||||||
|
|
||||||
response = await getRWMorphTableResults(strapi.db.getConnection());
|
const morphTableResults = await getRWMorphTableResults(strapi.db.getConnection());
|
||||||
expect(response.length).toEqual(0);
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,17 +3,23 @@
|
|||||||
const { WORKFLOW_MODEL_UID } = require('../constants/workflows');
|
const { WORKFLOW_MODEL_UID } = require('../constants/workflows');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a content type has the review workflows feature enabled
|
* Checks if a content type has review workflows enabled.
|
||||||
* @param {Object} contentType
|
* @param {string|Object} contentType - Either the modelUID of the content type, or the content type object.
|
||||||
* @returns
|
* @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
|
// TODO To be refactored when multiple workflows are added
|
||||||
const getDefaultWorkflow = async ({ strapi }) =>
|
const getDefaultWorkflow = async ({ strapi }) =>
|
||||||
strapi.query(WORKFLOW_MODEL_UID).findOne({ populate: ['stages'] });
|
strapi.query(WORKFLOW_MODEL_UID).findOne({ populate: ['stages'] });
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
hasRWEnabled,
|
hasReviewWorkflow,
|
||||||
getDefaultWorkflow,
|
getDefaultWorkflow,
|
||||||
};
|
};
|
||||||
|
@ -8,10 +8,17 @@ const stageObject = yup.object().shape({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const validateUpdateStagesSchema = yup.array().of(stageObject).required();
|
const validateUpdateStagesSchema = yup.array().of(stageObject).required();
|
||||||
|
const validateUpdateStageOnEntity = yup
|
||||||
|
.object()
|
||||||
|
.shape({
|
||||||
|
id: yup.number().integer().min(1).required(),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
validateUpdateStages: validateYupSchema(validateUpdateStagesSchema, {
|
validateUpdateStages: validateYupSchema(validateUpdateStagesSchema, {
|
||||||
strict: false,
|
strict: false,
|
||||||
stripUnknown: true,
|
stripUnknown: true,
|
||||||
}),
|
}),
|
||||||
|
validateUpdateStageOnEntity: validateYupSchema(validateUpdateStageOnEntity),
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user