diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e69de29 diff --git a/i18n.js b/i18n.js new file mode 100644 index 0000000..ec6f357 --- /dev/null +++ b/i18n.js @@ -0,0 +1,62 @@ +import i18n from 'i18next'; + +i18n.init({ + fallbackLng: 'en', + supportedLngs: ['en', 'pl'], + resources: { + en: { + translation: { + Source: 'Column field name', + SourceHelpText: + 'Pick the field which will be used to organize cards in columns, each possible value -> new column. Allowed types: {{types}}', + ContentType: 'Content Type', + ContentTypeHelpText: '', + Title: 'Title', + TitleHelpText: + 'Pick the field which will be used to display title in card preview. Allowed types: {{types}}', + Image: 'Image', + ImageHelpText: + 'Pick the field which will be used to display image in card preview (optional). Allowed types: {{types}}', + AdditionalFields: 'Additional Fields', + AdditionalFieldsHelpText: + 'Pick the fields which will be used to display additional fields in card preview (optional). Allowed types: {{types}}', + FieldRequired: 'Field is required', + WrongFieldType: 'This field type is not supported', + CardDelete: 'Content objects deleted (1)', + FetchError: + 'Error occurred while connecting to the server, please try again later.', + NonRequiredFieldsInCTD: + 'Make sure the selected content type contains fields that can be used in the plugin. Allowed types: {{types}}', + StateUpdateError: 'Failed to update card status', + }, + }, + pl: { + translation: { + Source: 'Pole kolumny', + SourceHelpText: + 'Wybierz pole, które będzie użyte do organizowania kart w kolumnach, każda możliwa wartość -> nowa kolumna. Dozwolone typy: {{types}}', + ContentType: 'Typ zawartości', + ContentTypeHelpText: '', + Title: 'Tytuł', + TitleHelpText: + 'Wybierz pole, które będzie użyte do wyświetlania tytułu w podglądzie karty. Dozwolone typy: {{types}}', + Image: 'Obraz', + ImageHelpText: + 'Wybierz pole, które będzie użyte do wyświetlania obrazu w podglądzie karty (opcjonalne). Dozwolone typy: {{types}}', + AdditionalFields: 'Dodatkowe Pole 1', + AdditionalFieldsHelpText: + 'Wybierz pola, które będą użyte do wyświetlania dodatkowych pól w podglądzie karty (opcjonalne). Dozwolone typy: {{types}}', + FieldRequired: 'Pole jest wymagane', + WrongFieldType: 'Ten typ pola nie jest wspierany', + CardDelete: 'Usunięto obiekty (1)', + FetchError: + 'Wystąpił błąd połączenia z serwerem, spróbuj ponownie później.', + NonRequiredFieldsInCTD: + 'pewnij się, że wybrany typ definicji zawiera pola, które mogą być wykorzystane we wtyczce. Dozwolone typy: {{types}}', + StateUpdateError: 'Nie udało się zaktualizować karty', + }, + }, + }, +}); + +export default i18n; diff --git a/plugins/common/plugin-helpers.js b/plugins/common/plugin-helpers.js new file mode 100644 index 0000000..46de6f9 --- /dev/null +++ b/plugins/common/plugin-helpers.js @@ -0,0 +1,44 @@ +const appRoots = {}; + +export const addElementToCache = (element, root, key) => { + appRoots[key] = { + element, + root, + }; + + let detachTimeoutId; + + element.addEventListener('flotiq.attached', () => { + if (detachTimeoutId) { + clearTimeout(detachTimeoutId); + detachTimeoutId = null; + } + }); + + element.addEventListener('flotiq.detached', () => { + detachTimeoutId = setTimeout(() => { + delete appRoots[key]; + }, 50); + }); +}; + +export const getCachedElement = (key) => { + return appRoots[key]; +}; + +export const registerFn = (pluginInfo, callback) => { + if (window.FlotiqPlugins?.add) { + window.FlotiqPlugins.add(pluginInfo, callback); + return; + } + if (!window.initFlotiqPlugins) window.initFlotiqPlugins = []; + window.initFlotiqPlugins.push({ pluginInfo, callback }); +}; + +export const addObjectToCache = (key, data = {}) => { + appRoots[key] = data; +}; + +export const removeRoot = (key) => { + delete appRoots[key]; +}; diff --git a/plugins/common/valid-fields.js b/plugins/common/valid-fields.js new file mode 100644 index 0000000..42278b1 --- /dev/null +++ b/plugins/common/valid-fields.js @@ -0,0 +1,101 @@ +import pluginInfo from '../plugin-manifest.json'; + +export const validSourceFields = ['select', 'radio']; + +export const validCardTitleFields = ['text']; + +export const validCardImageFields = ['datasource']; + +export const validCardAdditionalFields = [ + 'text', + 'number', + 'select', + 'dateTime', + 'checkbox', + 'radio', + 'richtext', + 'textarea', +]; + +export const getValidFields = (contentTypes) => { + const sourceFields = {}; + const sourceFieldsKeys = {}; + + const cardTitleFields = {}; + const cardTitleFieldsKeys = {}; + + const cardImageFields = {}; + const cardImageFieldsKeys = {}; + + const cardAdditionalFields = {}; + const cardAdditionalFieldsKeys = {}; + + contentTypes + ?.filter(({ internal }) => !internal) + ?.map(({ name, label }) => ({ value: name, label })); + + (contentTypes || []).forEach(({ name, metaDefinition }) => { + sourceFields[name] = []; + sourceFieldsKeys[name] = []; + + cardTitleFields[name] = []; + cardTitleFieldsKeys[name] = []; + + cardImageFields[name] = []; + cardImageFieldsKeys[name] = []; + + cardAdditionalFields[name] = []; + cardAdditionalFieldsKeys[name] = []; + + Object.entries(metaDefinition?.propertiesConfig || {}).forEach( + ([key, fieldConfig]) => { + const inputType = fieldConfig?.inputType; + + if (validSourceFields?.includes(inputType)) { + sourceFields[name].push({ value: key, label: fieldConfig.label }); + sourceFieldsKeys[name].push(key); + } + + if (validCardTitleFields?.includes(inputType)) { + cardTitleFields[name].push({ + value: key, + label: fieldConfig.label, + }); + cardTitleFieldsKeys[name].push(key); + } + + if ( + validCardImageFields?.includes(inputType) && + fieldConfig?.validation?.relationContenttype === '_media' + ) { + cardImageFields[name].push({ + value: key, + label: fieldConfig.label, + }); + cardImageFieldsKeys[name].push(key); + } + + if (validCardAdditionalFields?.includes(inputType)) { + cardAdditionalFields[name].push({ + value: key, + label: fieldConfig.label, + }); + cardAdditionalFieldsKeys[name].push(key); + } + }, + ); + }); + + return { + sourceFields, + sourceFieldsKeys, + cardTitleFields, + cardTitleFieldsKeys, + cardImageFields, + cardImageFieldsKeys, + cardAdditionalFields, + cardAdditionalFieldsKeys, + }; +}; + +export const validFieldsCacheKey = `${pluginInfo.id}-form-valid-fields`; diff --git a/plugins/field-config/plugin-form/index.js b/plugins/field-config/plugin-form/index.js new file mode 100644 index 0000000..4386adc --- /dev/null +++ b/plugins/field-config/plugin-form/index.js @@ -0,0 +1,92 @@ +import { getCachedElement } from '../../../../surferseo-plugin/plugins/common/plugin-helpers'; +import { + validCardAdditionalFields, + validCardTitleFields, + validFieldsCacheKey, + validSourceFields, +} from '../../common/valid-fields'; +import i18n from '../../../../surferseo-plugin/i18n'; + +const insertSelectOptions = (config, options = [], emptyOptionMessage) => { + config.additionalHelpTextClasses = 'break-normal'; + + if (options.length === 0) { + config.options = [ + { value: 'empty', label: emptyOptionMessage, disabled: true }, + ]; + return; + } + config.options = options; +}; + +export const handlePluginFormConfig = ({ name, config, formik }) => { + const { index, type } = + name.match(/kanbanBoard\[(?\d+)\].(?\w+)/)?.groups || {}; + + if (index == null || !type) return; + const ctd = formik.values.kanbanBoard[index].content_type; + const { + sourceFields, + cardTitleFields, + cardImageFields, + cardAdditionalFields, + } = getCachedElement(validFieldsCacheKey); + + const keysToClearOnCtdChange = [ + 'source', + 'title', + 'image', + 'additional_fields', + ]; + + switch (type) { + case 'content_type': + config.onChange = (_, value) => { + if (value == null) formik.setFieldValue(name, ''); + else formik.setFieldValue(name, value); + + keysToClearOnCtdChange.forEach((key) => { + formik.setFieldValue(`kanbanBoard[${index}].${key}`, ''); + }); + }; + break; + case 'source': + insertSelectOptions( + config, + sourceFields?.[ctd], + i18n.t('NonRequiredFieldsInCTD', { + types: validSourceFields.join(', '), + }), + ); + break; + case 'title': + insertSelectOptions( + config, + cardTitleFields?.[ctd], + i18n.t('NonRequiredFieldsInCTD', { + types: validCardTitleFields.join(', '), + }), + ); + break; + case 'image': + insertSelectOptions( + config, + cardImageFields?.[ctd], + i18n.t('NonRequiredFieldsInCTD', { + types: ['Relation to media, media'], + }), + ); + break; + case 'additional_fields': + insertSelectOptions( + config, + cardAdditionalFields?.[ctd], + i18n.t('NonRequiredFieldsInCTD', { + types: validCardAdditionalFields.join(', '), + }), + ); + break; + default: + break; + } +}; diff --git a/plugins/grid-renderers/index.js b/plugins/grid-renderers/index.js deleted file mode 100644 index 42990c3..0000000 --- a/plugins/grid-renderers/index.js +++ /dev/null @@ -1,62 +0,0 @@ -import { getRelationData } from "../../common/api-helpers"; -import { - addElementToCache, - getCachedElement, -} from "../../common/plugin-element-cache"; - -const textColors = [ - "rgb(239 68 68)", - "rgb(249 115 22)", - "rgb(234 179 8)", - "rgb(132 204 22)", - "rgb(34 197 94)", - "rgb(20 184 166)", - "rgb(59 130 246)", - "rgb(139 92 246)", - "rgb(168 85 247)", - "rgb(217 70 239)", -]; - -export function handleGridPlugin( - { accessor, contentObject, inputType, data }, - client, - pluginInfo, -) { - if (!["text", "number", "datasource"].includes(inputType)) return; - - const cacheKey = `${pluginInfo.id}-${contentObject.id}-${accessor}`; - - let element = getCachedElement(cacheKey)?.element; - if (!element) { - element = document.createElement("div"); - element.classList.add("plugin-name-cell-renderer"); - if (inputType === "text") { - const textColor = textColors[Math.floor(Math.random() * 10)]; - element.style.color = textColor; - element.textContent = data; - } else if (inputType === "number") { - element.style.fontWeight = 900; - element.textContent = data; - } else { - if (data) - Promise.all( - data.map((relation) => - getRelationData(client, relation.dataUrl).then( - (data) => - data.internal?.objectTitle || - data.title || - data.name || - data.id, - ), - ), - ).then((resultArray) => { - const joinedData = (resultArray || []).filter((r) => !!r).join(", "); - element.textContent = joinedData; - }); - } - } - - addElementToCache(element, cacheKey); - - return element; -} diff --git a/plugins/manage/index.js b/plugins/manage/index.js new file mode 100644 index 0000000..e69de29 diff --git a/plugins/manage/settings-schema.js b/plugins/manage/settings-schema.js new file mode 100644 index 0000000..fda9204 --- /dev/null +++ b/plugins/manage/settings-schema.js @@ -0,0 +1,184 @@ +import i18n from '../i18n'; +import pluginInfo from '../plugin-manifest.json'; +import { + validCardAdditionalFields, + validCardTitleFields, + validSourceFields, +} from '../common/valid-fields'; + +export const getSchema = (contentTypes) => ({ + id: pluginInfo.id, + name: pluginInfo.id, + label: pluginInfo.name, + internal: false, + schemaDefinition: { + type: 'object', + allOf: [ + { + $ref: '#/components/schemas/AbstractContentTypeSchemaDefinition', + }, + { + type: 'object', + properties: { + kanbanBoard: { + type: 'array', + items: { + type: 'object', + required: ['content_type', 'source', 'title'], + properties: { + source: { + type: 'string', + minLength: 1, + }, + content_type: { + type: 'string', + minLength: 1, + }, + title: { + type: 'string', + minLength: 1, + }, + image: { + type: 'string', + minLength: 1, + }, + additional_fields: { + type: 'array', + }, + }, + }, + }, + }, + }, + ], + required: [], + additionalProperties: false, + }, + metaDefinition: { + order: ['kanbanBoard'], + propertiesConfig: { + kanbanBoard: { + items: { + order: [ + 'content_type', + 'source', + 'title', + 'image', + 'additional_fields', + ], + propertiesConfig: { + source: { + label: i18n.t('Source'), + unique: false, + helpText: i18n.t('SourceHelpText', { + types: validSourceFields.join(', '), + }), + inputType: 'select', + options: [], + }, + content_type: { + label: i18n.t('ContentType'), + unique: false, + helpText: i18n.t('ContentTypeHelpText'), + inputType: 'select', + optionsWithLabels: contentTypes, + useOptionsWithLabels: true, + }, + title: { + label: i18n.t('Title'), + unique: false, + helpText: i18n.t('TitleHelpText', { + types: validCardTitleFields.join(', '), + }), + inputType: 'select', + options: [], + }, + image: { + label: i18n.t('Image'), + unique: false, + helpText: i18n.t('ImageHelpText', { + types: ['Relation to media, media'], + }), + inputType: 'select', + options: [], + }, + additional_fields: { + label: i18n.t('AdditionalFields'), + unique: false, + helpText: i18n.t('AdditionalFieldsHelpText', { + types: validCardAdditionalFields.join(', '), + }), + isMultiple: true, + useOptionsWithLabels: true, + inputType: 'select', + options: [], + }, + }, + }, + label: i18n.t('Configure'), + unique: false, + helpText: '', + inputType: 'object', + }, + }, + }, +}); + +const addToErrors = (errors, index, field, error) => { + if (!errors.kanbanBoard) errors.kanbanBoard = []; + if (!errors.kanbanBoard[index]) errors.kanbanBoard[index] = {}; + errors.kanbanBoard[index][field] = error; +}; + +export const getValidator = ( + sourceFieldKeys, + cardTitleFieldsKeys, + cardImageFieldsKeys, + cardAdditionalFieldsKeys, +) => { + return (values) => { + const errors = {}; + values.kanbanBoard?.forEach((settings, index) => { + const { content_type } = settings; + + const requiredFields = ['content_type', 'source', 'title']; + + requiredFields.forEach((requiredField) => { + if (!settings[requiredField]) { + addToErrors(errors, index, requiredField, i18n.t('FieldRequired')); + } + }); + + const validTypes = [ + { key: 'source', validFieldsKeys: sourceFieldKeys[content_type] }, + { key: 'title', validFieldsKeys: cardTitleFieldsKeys[content_type] }, + { key: 'image', validFieldsKeys: cardImageFieldsKeys[content_type] }, + { + key: 'additional_fields', + validFieldsKeys: cardAdditionalFieldsKeys[content_type], + }, + ]; + + validTypes.forEach(({ key, validFieldsKeys }) => { + const value = settings[key]; + + if (Array.isArray(value)) { + if ( + value && + value?.length > 0 && + !value.every((element) => validFieldsKeys.includes(element || [])) + ) { + addToErrors(errors, index, key, i18n.t('WrongFieldType')); + } + return; + } + + if (value && !(validFieldsKeys || []).includes(value)) { + addToErrors(errors, index, key, i18n.t('WrongFieldType')); + } + }); + }); + + return errors; + }; +};