Merge pull request #16455 from strapi/feature/custom-field-sizes

Let custom fields specify a custom input size
This commit is contained in:
Rémi de Juvigny 2023-04-21 15:30:39 +02:00 committed by GitHub
commit 7c972f569b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 169 additions and 32 deletions

View File

@ -102,7 +102,9 @@ const ModalForm = ({ onMetaChange, onSizeChange }) => {
);
});
const { isResizable } = fieldSizes[attributes[selectedField].type];
// Check for a custom input provided by a custom field, or use the default one for that type
const { type, customField } = attributes[selectedField];
const { isResizable } = fieldSizes[customField] ?? fieldSizes[type];
const sizeField = (
<GridItem col={6} key="size">

View File

@ -30,8 +30,14 @@ const reducer = (state = initialState, action) =>
}
case 'ON_ADD_FIELD': {
const newState = cloneDeep(state);
const type = get(newState, ['modifiedData', 'attributes', action.name, 'type'], '');
const size = action.fieldSizes[type]?.default ?? DEFAULT_FIELD_SIZE;
const attribute = get(newState, ['modifiedData', 'attributes', action.name], {});
// Get the default size, checking custom fields first, then the type and generic defaults
const size =
action.fieldSizes[attribute?.customField]?.default ??
action.fieldSizes[attribute?.type]?.default ??
DEFAULT_FIELD_SIZE;
const listSize = get(newState, layoutPathEdit, []).length;
const actualRowContentPath = [...layoutPathEdit, listSize - 1, 'rowContent'];
const rowContentToSet = get(newState, actualRowContentPath, []);

View File

@ -6,4 +6,5 @@ module.exports = async () => {
await getService('components').syncConfigurations();
await getService('content-types').syncConfigurations();
await getService('permission').registerPermissions();
getService('field-sizes').setCustomFieldInputSizes();
};

View File

@ -1,10 +1,36 @@
'use strict';
const fieldSizesService = require('../field-sizes');
const { ApplicationError } = require('@strapi/utils').errors;
const createFieldSizesService = require('../field-sizes');
const strapi = {
container: {
// Mock container.get('custom-fields')
get: jest.fn(() => ({
// Mock container.get('custom-fields').getAll()
getAll: jest.fn(() => ({
'plugin::mycustomfields.color': {
name: 'color',
plugin: 'mycustomfields',
type: 'string',
},
'plugin::mycustomfields.smallColor': {
name: 'smallColor',
plugin: 'mycustomfields',
type: 'string',
inputSize: {
default: 4,
isResizable: false,
},
},
})),
})),
},
};
describe('field sizes service', () => {
it('should return the correct field sizes', () => {
const { getAllFieldSizes } = fieldSizesService();
const { getAllFieldSizes } = createFieldSizesService({ strapi });
const fieldSizes = getAllFieldSizes();
Object.values(fieldSizes).forEach((fieldSize) => {
expect(typeof fieldSize.isResizable).toBe('boolean');
@ -13,21 +39,42 @@ describe('field sizes service', () => {
});
it('should return the correct field size for a given type', () => {
const { getFieldSize } = fieldSizesService();
const { getFieldSize } = createFieldSizesService({ strapi });
const fieldSize = getFieldSize('string');
expect(fieldSize.isResizable).toBe(true);
expect(fieldSize.default).toBe(6);
});
it('should throw an error if the type is not found', () => {
const { getFieldSize } = fieldSizesService();
expect(() => getFieldSize('not-found')).toThrowError(
'Could not find field size for type not-found'
);
const { getFieldSize } = createFieldSizesService({ strapi });
try {
getFieldSize('not-found');
} catch (error) {
expect(error instanceof ApplicationError).toBe(true);
expect(error.message).toBe('Could not find field size for type not-found');
}
});
it('should throw an error if the type is not provided', () => {
const { getFieldSize } = fieldSizesService();
expect(() => getFieldSize()).toThrowError('The type is required');
const { getFieldSize } = createFieldSizesService({ strapi });
try {
getFieldSize();
} catch (error) {
expect(error instanceof ApplicationError).toBe(true);
expect(error.message).toBe('The type is required');
}
});
it('should set the custom fields input sizes', () => {
const { setCustomFieldInputSizes, getAllFieldSizes } = createFieldSizesService({ strapi });
setCustomFieldInputSizes();
const fieldSizes = getAllFieldSizes();
console.log(fieldSizes);
expect(fieldSizes).not.toHaveProperty('plugin::mycustomfields.color');
expect(fieldSizes['plugin::mycustomfields.smallColor'].default).toBe(4);
expect(fieldSizes['plugin::mycustomfields.smallColor'].isResizable).toBe(false);
});
});

View File

@ -1,5 +1,7 @@
'use strict';
const { ApplicationError } = require('@strapi/utils').errors;
const needsFullSize = {
default: 12,
isResizable: false,
@ -44,20 +46,51 @@ const fieldSizes = {
uid: defaultSize,
};
module.exports = () => ({
getAllFieldSizes() {
return fieldSizes;
},
getFieldSize(type) {
if (!type) {
throw new Error('The type is required');
}
const createFieldSizesService = ({ strapi }) => {
const fieldSizesService = {
getAllFieldSizes() {
return fieldSizes;
},
const fieldSize = fieldSizes[type];
if (!fieldSize) {
throw new Error(`Could not find field size for type ${type}`);
}
getFieldSize(type) {
if (!type) {
throw new ApplicationError('The type is required');
}
return fieldSize;
},
});
const fieldSize = fieldSizes[type];
if (!fieldSize) {
throw new ApplicationError(`Could not find field size for type ${type}`);
}
return fieldSize;
},
setFieldSize(type, size) {
if (!type) {
throw new ApplicationError('The type is required');
}
if (!size) {
throw new ApplicationError('The size is required');
}
fieldSizes[type] = size;
},
setCustomFieldInputSizes() {
// Find all custom fields already registered
const customFields = strapi.container.get('custom-fields').getAll();
// If they have a custom field size, register it
Object.entries(customFields).forEach(([uid, customField]) => {
if (customField.inputSize) {
fieldSizesService.setFieldSize(uid, customField.inputSize);
}
});
},
};
return fieldSizesService;
};
module.exports = createFieldSizesService;

View File

@ -20,9 +20,18 @@ const isAllowedFieldSize = (type, size) => {
return size <= MAX_ROW_SIZE;
};
const getDefaultFieldSize = (type) => {
const getDefaultFieldSize = (attribute) => {
// Check if it's a custom field with a custom size
if (attribute.customField) {
const customField = strapi.container.get('custom-fields').get(attribute.customField);
if (customField.inputSize) {
return customField.inputSize.default;
}
}
// Get the default size for the field type
const { getFieldSize } = getService('field-sizes');
return getFieldSize(type).default;
return getFieldSize(attribute.type).default;
};
async function createDefaultLayouts(schema) {
@ -127,7 +136,7 @@ const appendToEditLayout = (layout = [], keysToAppend, schema) => {
for (const key of keysToAppend) {
const attribute = schema.attributes[key];
const attributeSize = getDefaultFieldSize(attribute.type);
const attributeSize = getDefaultFieldSize(attribute);
const currenRowSize = rowSize(layout[currentRowIndex]);
if (currenRowSize + attributeSize > MAX_ROW_SIZE) {

View File

@ -90,6 +90,28 @@ describe('Custom fields registry', () => {
);
});
it('validates inputSize', () => {
const mockCF = {
name: 'test',
type: 'text',
};
const customFields = customFieldsRegistry(strapi);
expect(() => customFields.add({ ...mockCF, inputSize: 'small' })).toThrowError(
`inputSize should be an object with 'default' and 'isResizable' keys`
);
expect(() => customFields.add({ ...mockCF, inputSize: ['array'] })).toThrowError(
`inputSize should be an object with 'default' and 'isResizable' keys`
);
expect(() =>
customFields.add({ ...mockCF, inputSize: { default: 99, isResizable: true } })
).toThrowError('Custom fields require a valid default input size');
expect(() =>
customFields.add({ ...mockCF, inputSize: { default: 12, isResizable: 'true' } })
).toThrowError('Custom fields should specify if their input is resizable');
});
it('confirms the custom field does not already exist', () => {
const mockCF = {
name: 'test',

View File

@ -1,6 +1,6 @@
'use strict';
const { has } = require('lodash/fp');
const { has, isPlainObject } = require('lodash/fp');
const ALLOWED_TYPES = [
'biginteger',
@ -44,7 +44,7 @@ const customFieldsRegistry = (strapi) => {
throw new Error(`Custom fields require a 'name' and 'type' key`);
}
const { name, plugin, type } = cf;
const { name, plugin, type, inputSize } = cf;
if (!ALLOWED_TYPES.includes(type)) {
throw new Error(
`Custom field type: '${type}' is not a valid Strapi type or it can't be used with a Custom Field`
@ -56,6 +56,23 @@ const customFieldsRegistry = (strapi) => {
throw new Error(`Custom field name: '${name}' is not a valid object key`);
}
// Validate inputSize when provided
if (inputSize) {
if (
!isPlainObject(inputSize) ||
!has('default', inputSize) ||
!has('isResizable', inputSize)
) {
throw new Error(`inputSize should be an object with 'default' and 'isResizable' keys`);
}
if (![4, 6, 8, 12].includes(inputSize.default)) {
throw new Error('Custom fields require a valid default input size');
}
if (typeof inputSize.isResizable !== 'boolean') {
throw new Error('Custom fields should specify if their input is resizable');
}
}
// When no plugin is specified, or it isn't found in Strapi, default to global
const uid = strapi.plugin(plugin) ? `plugin::${plugin}.${name}` : `global::${name}`;