Handle component update and delete with categories

This commit is contained in:
Alexandre Bodin 2019-10-29 12:18:42 +01:00
parent cfc5cf07ad
commit d405285fae
14 changed files with 213 additions and 96 deletions

View File

@ -40,7 +40,7 @@
},
"body": {
"type": "dynamiczone",
"components": ["closingperiod", "restaurantservice"]
"components": ["default.closingperiod", "default.restaurantservice"]
},
"description": {
"type": "richtext",
@ -69,11 +69,6 @@
},
"short_description": {
"type": "text"
},
"metas": {
"type": "component",
"component": "seo.meta",
"repeatable": true
}
}
}

View File

@ -1,16 +0,0 @@
{
"info": {
"name": "meta",
"description": ""
},
"connection": "default",
"collectionName": "seo_meta",
"attributes": {
"key": {
"type": "string"
},
"value": {
"type": "text"
}
}
}

View File

@ -25,7 +25,7 @@ describe.each([
});
await modelsUtils.createModelWithType('withcomponent', 'component', {
component: 'somecomponent',
component: 'default.somecomponent',
repeatable: true,
required: false,
min: 1,

View File

@ -25,7 +25,7 @@ describe.each([
});
await modelsUtils.createModelWithType('withcomponent', 'component', {
component: 'somecomponent',
component: 'default.somecomponent',
repeatable: true,
required: false,
});

View File

@ -25,7 +25,7 @@ describe.each([
});
await modelsUtils.createModelWithType('withcomponent', 'component', {
component: 'somecomponent',
component: 'default.somecomponent',
repeatable: true,
required: true,
});

View File

@ -25,7 +25,7 @@ describe.each([
});
await modelsUtils.createModelWithType('withcomponent', 'component', {
component: 'somecomponent',
component: 'default.somecomponent',
repeatable: false,
required: false,
});

View File

@ -25,7 +25,7 @@ describe.each([
});
await modelsUtils.createModelWithType('withcomponent', 'component', {
component: 'somecomponent',
component: 'default.somecomponent',
repeatable: false,
required: true,
});

View File

@ -1,6 +1,9 @@
'use strict';
const validateComponentInput = require('./validation/component');
const _ = require('lodash');
const {
validateComponentInput,
validateUpdateComponentInput,
} = require('./validation/component');
/**
* Components controller
*/
@ -59,7 +62,7 @@ module.exports = {
strapi.reload.isWatching = false;
const newComponent = await service.createComponent(uid, body);
const newComponent = await service.createComponent({ uid, infos: body });
strapi.reload();
@ -82,24 +85,24 @@ module.exports = {
return ctx.send({ error: 'component.notFound' }, 404);
}
// convert zero length string on default attributes to undefined
if (_.has(body, 'attributes')) {
Object.keys(body.attributes).forEach(attribute => {
if (body.attributes[attribute].default === '') {
body.attributes[attribute].default = undefined;
}
});
}
try {
await validateComponentInput(body);
await validateUpdateComponentInput(body);
} catch (error) {
return ctx.send({ error }, 400);
}
const newUID = service.createComponentUID(body);
if (newUID !== uid && service.getComponent(newUID)) {
return ctx.send({ error: 'new.component.alreadyExists' }, 400);
}
strapi.reload.isWatching = false;
const updatedComponent = await service.updateComponent(component, body);
const updatedComponent = await service.updateComponent({
newUID,
component,
infos: body,
});
await service.updateComponentInModels(component.uid, updatedComponent.uid);
strapi.reload();

View File

@ -8,7 +8,25 @@ const { isValidName, isValidKey } = require('./common');
const getTypeValidator = require('./types');
const getRelationValidator = require('./relations');
module.exports = data => {
const validateComponentInput = data => {
return componentSchema
.validate(data, {
strict: true,
abortEarly: false,
})
.catch(error => Promise.reject(formatYupErrors(error)));
};
const validateUpdateComponentInput = data => {
// convert zero length string on default attributes to undefined
if (_.has(data, 'attributes')) {
Object.keys(data.attributes).forEach(attribute => {
if (data.attributes[attribute].default === '') {
data.attributes[attribute].default = undefined;
}
});
}
return componentSchema
.validate(data, {
strict: true,
@ -69,3 +87,8 @@ const componentSchema = yup
}),
})
.noUnknown();
module.exports = {
validateComponentInput,
validateUpdateComponentInput,
};

View File

@ -107,11 +107,10 @@ const formatAttribute = (key, attribute, { component }) => {
* @param {string} uid
* @param {Object} infos
*/
async function createComponent(uid, infos) {
const { name, category } = infos;
const schema = createSchema(uid, infos);
async function createComponent({ uid, infos }) {
const schema = createSchema(infos);
await writeSchema({ name, schema, category });
await writeSchema({ uid, schema });
return { uid };
}
@ -120,13 +119,14 @@ async function createComponent(uid, infos) {
* @param {Object} component
* @param {Object} infos
*/
async function updateComponent(component, infos) {
async function updateComponent({ component, newUID, infos }) {
const { uid, schema: oldSchema } = component;
// don't update collectionName if not provided
const updatedSchema = {
info: {
name: infos.name || oldSchema.name,
icon: infos.icon,
name: infos.name,
description: infos.description || oldSchema.description,
},
connection: infos.connection || oldSchema.connection,
@ -134,7 +134,6 @@ async function updateComponent(component, infos) {
attributes: convertAttributes(infos.attributes),
};
const newUID = createComponentUID(infos.name);
if (uid !== newUID) {
await deleteSchema(uid);
@ -146,11 +145,23 @@ async function updateComponent(component, infos) {
]).updateUID(uid, newUID);
}
await writeSchema(newUID, updatedSchema);
await writeSchema({
uid: newUID,
schema: updatedSchema,
});
const [category] = uid.split('.');
const categoryDir = path.join(strapi.dir, 'components', category);
const categoryCompos = await fse.readdir(categoryDir);
if (categoryCompos.length === 0) {
await fse.rmdir(categoryDir);
}
return { uid: newUID };
}
await writeSchema(uid, updatedSchema);
await writeSchema({ uid, schema: updatedSchema });
return { uid };
}
@ -158,7 +169,7 @@ async function updateComponent(component, infos) {
* Create a schema
* @param {Object} infos
*/
const createSchema = (uid, infos) => {
const createSchema = infos => {
const {
name,
icon,
@ -252,16 +263,15 @@ async function deleteComponent(component) {
/**
* Writes a component schema file
*/
async function writeSchema({ name, schema, category }) {
async function writeSchema({ uid, schema }) {
const [category, filename] = uid.split('.');
const categoryDir = path.join(strapi.dir, 'components', category);
if (!(await fse.pathExists(categoryDir))) {
await fse.mkdir(categoryDir);
await fse.ensureDir(categoryDir);
}
const filename = nameToSlug(name);
const filepath = path.join(categoryDir, `${filename}.json`);
await fse.writeFile(filepath, JSON.stringify(schema, null, 2));
}
@ -270,7 +280,8 @@ async function writeSchema({ name, schema, category }) {
* @param {string} ui
*/
async function deleteSchema(uid) {
await strapi.fs.removeAppFile(`components/${uid}.json`);
const [category, filename] = uid.split('.');
await strapi.fs.removeAppFile(`components/${category}/${filename}.json`);
}
const updateComponentInModels = (oldUID, newUID) => {
@ -295,6 +306,13 @@ const updateComponentInModels = (oldUID, newUID) => {
[]
);
const dynamicoznesToUpdate = Object.keys(model.attributes).filter(key => {
return (
model.attributes[key].type === 'dynamiczone' &&
model.attributes[key].components.includes(oldUID)
);
}, []);
if (attributesToModify.length > 0) {
const modelJSON = contentTypeService.readModel(modelKey, {
plugin,
@ -305,6 +323,15 @@ const updateComponentInModels = (oldUID, newUID) => {
modelJSON.attributes[key].component = newUID;
});
dynamicoznesToUpdate.forEach(key => {
modelJSON.attributes[key] = {
...modelJSON.attributes[key],
components: modelJSON.attributes[key].components.map(val =>
val !== oldUID ? val : newUID
),
};
});
contentTypeService.writeModel(modelKey, modelJSON, {
plugin,
api: model.apiName,
@ -319,10 +346,35 @@ const updateComponentInModels = (oldUID, newUID) => {
updateModels(strapi.plugins[pluginKey].models, { plugin: pluginKey });
});
// add strapi.components or strapi.admin if necessary
Object.keys(strapi.components).forEach(uid => {
const component = strapi.components[uid];
const componentsToRemove = Object.keys(component.attributes).filter(key => {
return (
component.attributes[key].type === 'component' &&
component.attributes[key].component === oldUID
);
}, []);
if (componentsToRemove.length > 0) {
const newSchema = {
info: component.info,
connection: component.connection,
collectionName: component.collectionName,
attributes: component.attributes,
};
componentsToRemove.forEach(key => {
newSchema.attributes[key].component = newUID;
});
writeSchema({ uid, schema: newSchema });
}
});
};
const deleteComponentInModels = componentUID => {
const deleteComponentInModels = async componentUID => {
const [category] = componentUID.split('.');
const contentTypeService =
strapi.plugins['content-type-builder'].services.contenttypebuilder;
@ -330,30 +382,39 @@ const deleteComponentInModels = componentUID => {
Object.keys(models).forEach(modelKey => {
const model = models[modelKey];
const attributesToDelete = Object.keys(model.attributes).reduce(
(acc, key) => {
if (
model.attributes[key].type === 'component' &&
model.attributes[key].component === componentUID
) {
acc.push(key);
}
const componentsToRemove = Object.keys(model.attributes).filter(key => {
return (
model.attributes[key].type === 'component' &&
model.attributes[key].component === componentUID
);
}, []);
return acc;
},
[]
);
const dynamicoznesToUpdate = Object.keys(model.attributes).filter(key => {
return (
model.attributes[key].type === 'dynamiczone' &&
model.attributes[key].components.includes(componentUID)
);
}, []);
if (attributesToDelete.length > 0) {
if (componentsToRemove.length > 0 || dynamicoznesToUpdate.length > 0) {
const modelJSON = contentTypeService.readModel(modelKey, {
plugin,
api: model.apiName,
});
attributesToDelete.forEach(key => {
componentsToRemove.forEach(key => {
delete modelJSON.attributes[key];
});
dynamicoznesToUpdate.forEach(key => {
modelJSON.attributes[key] = {
...modelJSON.attributes[key],
components: modelJSON.attributes[key].components.filter(
val => val !== componentUID
),
};
});
contentTypeService.writeModel(modelKey, modelJSON, {
plugin,
api: model.apiName,
@ -368,7 +429,37 @@ const deleteComponentInModels = componentUID => {
updateModels(strapi.plugins[pluginKey].models, { plugin: pluginKey });
});
// add strapi.components or strapi.admin if necessary
Object.keys(strapi.components).forEach(uid => {
const component = strapi.components[uid];
const componentsToRemove = Object.keys(component.attributes).filter(key => {
return (
component.attributes[key].type === 'component' &&
component.attributes[key].component === componentUID
);
}, []);
if (componentsToRemove.length > 0) {
const newSchema = {
info: component.info,
connection: component.connection,
collectionName: component.collectionName,
attributes: component.attributes,
};
componentsToRemove.forEach(key => {
delete newSchema.attributes[key];
});
writeSchema({ uid, schema: newSchema });
}
});
const categoryDir = path.join(strapi.dir, 'components', category);
const categoryCompos = await fse.readdir(categoryDir);
if (categoryCompos.length === 0) {
await fse.rmdir(categoryDir);
}
};
module.exports = {

View File

@ -4,6 +4,7 @@ describe('Component Service', () => {
describe('createSchema', () => {
test('Formats schema and create default values', () => {
const input = {
category: 'default',
name: 'Some name',
attributes: {},
};
@ -24,11 +25,11 @@ describe('Component Service', () => {
description: '',
},
connection: 'default',
collectionName: 'components_some_names',
collectionName: 'components_default_some_names',
attributes: {},
};
expect(createSchema('some_name', input)).toEqual(expected);
expect(createSchema(input)).toEqual(expected);
});
test('Accepts overrides', () => {
@ -49,15 +50,23 @@ describe('Component Service', () => {
attributes: {},
};
expect(createSchema('some_name', input)).toEqual(expected);
expect(createSchema(input)).toEqual(expected);
});
});
describe('createComponentUID', () => {
test('Generats normalized uids', () => {
expect(createComponentUID('some char')).toBe('some_char');
expect(createComponentUID('some-char')).toBe('some_char');
expect(createComponentUID('Some Char')).toBe('some_char');
expect(
createComponentUID({ category: 'default', name: 'some char' })
).toBe('default.some_char');
expect(
createComponentUID({ category: 'default', name: 'some-char' })
).toBe('default.some_char');
expect(
createComponentUID({ category: 'default', name: 'Some Char' })
).toBe('default.some_char');
});
});
});

View File

@ -20,6 +20,8 @@ describe.only('Content Type Builder - Components', () => {
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({
error: {
category: ['category.required'],
icon: ['icon.required'],
attributes: ['attributes.required'],
name: ['name.required'],
},
@ -31,7 +33,9 @@ describe.only('Content Type Builder - Components', () => {
method: 'POST',
url: '/content-type-builder/components',
body: {
name: 'SomeComponent',
category: 'default',
icon: 'default',
name: 'Some Component',
attributes: {
title: {
type: 'string',
@ -46,7 +50,7 @@ describe.only('Content Type Builder - Components', () => {
expect(res.statusCode).toBe(201);
expect(res.body).toEqual({
data: {
uid: 'some_component',
uid: 'default.some_component',
},
});
@ -58,6 +62,8 @@ describe.only('Content Type Builder - Components', () => {
method: 'POST',
url: '/content-type-builder/components',
body: {
category: 'default',
icon: 'default',
name: 'someComponent',
attributes: {},
},
@ -113,18 +119,20 @@ describe.only('Content Type Builder - Components', () => {
test('Returns correct format', async () => {
const res = await rq({
method: 'GET',
url: '/content-type-builder/components/some_component',
url: '/content-type-builder/components/default.some_component',
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
uid: 'some_component',
uid: 'default.some_component',
category: 'default',
schema: {
name: 'SomeComponent',
icon: 'default',
name: 'Some Component',
description: '',
connection: 'default',
collectionName: 'components_some_components',
collectionName: 'components_default_some_components',
attributes: {
title: {
type: 'string',
@ -157,7 +165,7 @@ describe.only('Content Type Builder - Components', () => {
test('Validates input and return 400 in case of invalid input', async () => {
const res = await rq({
method: 'PUT',
url: '/content-type-builder/components/some_component',
url: '/content-type-builder/components/default.some_component',
body: {
attributes: {},
},
@ -166,6 +174,8 @@ describe.only('Content Type Builder - Components', () => {
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({
error: {
category: ['category.required'],
icon: ['icon.required'],
name: ['name.required'],
},
});
@ -174,8 +184,10 @@ describe.only('Content Type Builder - Components', () => {
test('Updates a component properly', async () => {
const res = await rq({
method: 'PUT',
url: '/content-type-builder/components/some_component',
url: '/content-type-builder/components/default.some_component',
body: {
category: 'default',
icon: 'default',
name: 'NewComponent',
attributes: {},
},
@ -184,7 +196,7 @@ describe.only('Content Type Builder - Components', () => {
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({
data: {
uid: 'new_component',
uid: 'default.new_component',
},
});
@ -208,13 +220,13 @@ describe.only('Content Type Builder - Components', () => {
test('Deletes a component correctly', async () => {
const res = await rq({
method: 'DELETE',
url: '/content-type-builder/components/new_component',
url: '/content-type-builder/components/default.new_component',
});
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({
data: {
uid: 'new_component',
uid: 'default.new_component',
},
});
@ -222,7 +234,7 @@ describe.only('Content Type Builder - Components', () => {
const tryGet = await rq({
method: 'GET',
url: '/content-type-builder/components/new_component',
url: '/content-type-builder/components/default.new_component',
});
expect(tryGet.statusCode).toBe(404);

View File

@ -116,10 +116,8 @@ function watchFileChanges({ dir, strapiInstance }) {
/tmp/,
'**/admin',
'**/admin/**',
'**/components',
'**/components/**',
'**/documentation',
'**/documentation/**',
'extensions/**/admin',
'extensions/**/admin/**',
'**/node_modules',
'**/node_modules/**',
'**/plugins.json',

View File

@ -6,6 +6,8 @@ module.exports = ({ rq }) => {
url: '/content-type-builder/components',
method: 'POST',
body: {
category: 'default',
icon: 'default',
connection: 'default',
...data,
},