From 3a117d8dcbec8baee9e95a2a258ae91e2284a6ed Mon Sep 17 00:00:00 2001 From: Niklas Kiefer Date: Tue, 8 Aug 2023 14:46:11 +0200 Subject: [PATCH] wip wip custom form fields --- .../features/palette/components/Palette.js | 58 ++++++- .../properties-panel/PropertiesPanel.js | 57 +++---- .../PropertiesPanelHeaderProvider.js | 55 ++++--- .../PropertiesPanelRenderer.js | 42 +++++ .../properties-panel/PropertiesProvider.js | 84 ++++++++++ .../src/features/properties-panel/Util.js | 12 +- .../properties-panel/entries/ActionEntry.js | 21 +-- .../properties-panel/entries/AdornerEntry.js | 46 +++--- .../properties-panel/entries/AltTextEntry.js | 21 +-- .../entries/DateTimeConstraintsEntry.js | 51 +++--- .../properties-panel/entries/DateTimeEntry.js | 33 ++-- .../entries/DateTimeSerializationEntry.js | 28 ++-- .../entries/DefaultValueEntry.js | 79 ++++----- .../entries/DescriptionEntry.js | 21 +-- .../properties-panel/entries/DisabledEntry.js | 21 +-- .../properties-panel/entries/IdEntry.js | 17 +- .../entries/ImageSourceEntry.js | 22 +-- .../properties-panel/entries/KeyEntry.js | 21 +-- .../properties-panel/entries/LabelEntry.js | 78 ++++----- .../properties-panel/entries/NumberEntries.js | 14 +- .../entries/NumberSerializationEntry.js | 11 +- .../properties-panel/entries/ReadonlyEntry.js | 21 +-- .../properties-panel/entries/SelectEntries.js | 15 +- .../properties-panel/entries/SpacerEntry.js | 11 +- .../properties-panel/entries/TextEntry.js | 15 +- .../factories/simpleBoolEntryFactory.js | 6 +- .../groups/ValidationGroup.js | 147 ++++++++--------- .../src/features/properties-panel/index.js | 6 +- .../src/render/components/FormEditor.js | 20 ++- .../properties-panel/PropertiesPanel.spec.js | 14 +- .../groups/AppearanceGroup.spec.js | 8 +- .../groups/SerializationGroup.spec.js | 12 +- .../features/properties-panel/helper/index.js | 52 +++++- packages/form-js-playground/karma.conf.js | 34 ++++ .../form-js-playground/test/custom/editor.js | 152 ++++++++++++++++++ .../form-js-playground/test/custom/range.svg | 19 +++ .../form-js-playground/test/custom/styles.css | 4 + .../form-js-playground/test/custom/viewer.js | 122 ++++++++++++++ .../test/spec/Playground.spec.js | 39 ++++- .../form-js-playground/test/spec/custom.json | 28 ++++ .../src/render/components/index.js | 13 +- .../form-js-viewer/test/spec/Form.spec.js | 25 +-- .../form-js-viewer/test/spec/custom/index.js | 77 +++++++-- .../form-js-viewer/test/spec/customField.json | 26 +++ packages/form-js/src/editor.js | 7 +- packages/form-js/src/viewer.js | 8 +- 46 files changed, 1165 insertions(+), 508 deletions(-) create mode 100644 packages/form-js-editor/src/features/properties-panel/PropertiesProvider.js create mode 100644 packages/form-js-playground/test/custom/editor.js create mode 100644 packages/form-js-playground/test/custom/range.svg create mode 100644 packages/form-js-playground/test/custom/styles.css create mode 100644 packages/form-js-playground/test/custom/viewer.js create mode 100644 packages/form-js-playground/test/spec/custom.json create mode 100644 packages/form-js-viewer/test/spec/customField.json diff --git a/packages/form-js-editor/src/features/palette/components/Palette.js b/packages/form-js-editor/src/features/palette/components/Palette.js index a10dd6afb..43515218d 100644 --- a/packages/form-js-editor/src/features/palette/components/Palette.js +++ b/packages/form-js-editor/src/features/palette/components/Palette.js @@ -5,6 +5,8 @@ import { useState } from 'preact/hooks'; +import { useService } from '../../../render/hooks'; + import { Slot } from '../../render-injection/slot-fill'; @@ -15,7 +17,7 @@ import { SearchIcon } from '../../../render/components/icons'; -import { formFields } from '@bpmn-io/form-js-viewer'; +import { formFields, sanitizeImageSource } from '@bpmn-io/form-js-viewer'; export const PALETTE_ENTRIES = formFields.filter(({ config: fieldConfig }) => fieldConfig.type !== 'default').map(({ config: fieldConfig }) => { return { @@ -25,6 +27,7 @@ export const PALETTE_ENTRIES = formFields.filter(({ config: fieldConfig }) => fi }; }); +// todo(pinussilvestrus): should it be possible to configure the palette groups? export const PALETTE_GROUPS = [ { label: 'Basic input', @@ -46,13 +49,17 @@ export const PALETTE_GROUPS = [ export default function Palette(props) { - const [ entries, setEntries ] = useState(PALETTE_ENTRIES); + const formFields = useService('formFields'); + + const initialPaletteEntries = collectPaletteEntries(formFields); + + const [ paletteEntries, setPaletteEntries ] = useState(initialPaletteEntries); const [ searchTerm, setSearchTerm ] = useState(''); const inputRef = useRef(); - const groups = groupEntries(entries); + const groups = groupEntries(paletteEntries); const simplifyString = useCallback((str) => { return str @@ -78,8 +85,8 @@ export default function Palette(props) { // filter entries on search change useEffect(() => { - const entries = PALETTE_ENTRIES.filter(filter); - setEntries(entries); + const entries = initialPaletteEntries.filter(filter); + setPaletteEntries(entries); }, [ filter, searchTerm ]); const handleInput = useCallback(event => { @@ -120,8 +127,9 @@ export default function Palette(props) { { label }
{ - entries.map(({ label, type }) => { - const Icon = iconsByType(type); + entries.map(({ label, type, icon, iconUrl }) => { + + const Icon = getPaletteIcon({ icon, iconUrl, label, type }); return (
{ + + const { config: fieldConfig } = formField; + + return { + label: fieldConfig.label, + type: type, + group: fieldConfig.group, + icon: fieldConfig.icon, + iconUrl: fieldConfig.iconUrl + }; + }).filter(({ type }) => type !== 'default'); +} + +/** + * There are various options to specify an icon for a palette entry. + * + * a) via `iconUrl` property in a form field config + * b) via `icon` property in a form field config + * c) via statically defined iconsByType (fallback) + */ +export function getPaletteIcon(entry) { + const { icon, iconUrl, type, label } = entry; + + let Icon; + + if (iconUrl) { + Icon = () => {; + } else { + Icon = icon || iconsByType(type); + } + + return Icon; } \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/PropertiesPanel.js b/packages/form-js-editor/src/features/properties-panel/PropertiesPanel.js index 3528f630f..158239063 100644 --- a/packages/form-js-editor/src/features/properties-panel/PropertiesPanel.js +++ b/packages/form-js-editor/src/features/properties-panel/PropertiesPanel.js @@ -2,52 +2,22 @@ import { PropertiesPanel } from '@bpmn-io/properties-panel'; import { useCallback, + useMemo, useState, useLayoutEffect } from 'preact/hooks'; +import { reduce, isArray } from 'min-dash'; + import { FormPropertiesPanelContext } from './context'; import { PropertiesPanelHeaderProvider } from './PropertiesPanelHeaderProvider'; import { PropertiesPanelPlaceholderProvider } from './PropertiesPanelPlaceholderProvider'; -import { - ConditionGroup, - AppearanceGroup, - CustomPropertiesGroup, - GeneralGroup, - SerializationGroup, - ConstraintsGroup, - ValidationGroup, - ValuesGroups, - LayoutGroup -} from './groups'; - -function getGroups(field, editField, getService) { - - if (!field) { - return []; - } - - const groups = [ - GeneralGroup(field, editField, getService), - ConditionGroup(field, editField), - LayoutGroup(field, editField), - AppearanceGroup(field, editField), - SerializationGroup(field, editField), - ...ValuesGroups(field, editField), - ConstraintsGroup(field, editField), - ValidationGroup(field, editField), - CustomPropertiesGroup(field, editField) - ]; - - // contract: if a group returns null, it should not be displayed at all - return groups.filter(group => group !== null); -} - export default function FormPropertiesPanel(props) { const { eventBus, + getProviders, injector } = props; @@ -100,6 +70,23 @@ export default function FormPropertiesPanel(props) { const editField = useCallback((formField, key, value) => modeling.editFormField(formField, key, value), [ modeling ]); + // retrieve groups for selected form field + const providers = getProviders(selectedFormField); + + const groups = useMemo(() => { + return reduce(providers, function(groups, provider) { + + // do not collect groups for multi element state + if (isArray(selectedFormField)) { + return []; + } + + const updater = provider.getGroups(selectedFormField, editField); + + return updater(groups); + }, []); + }, [ providers, selectedFormField, editField ]); + return (
diff --git a/packages/form-js-editor/src/features/properties-panel/PropertiesPanelHeaderProvider.js b/packages/form-js-editor/src/features/properties-panel/PropertiesPanelHeaderProvider.js index 40c0e42e7..2a1aacfb0 100644 --- a/packages/form-js-editor/src/features/properties-panel/PropertiesPanelHeaderProvider.js +++ b/packages/form-js-editor/src/features/properties-panel/PropertiesPanelHeaderProvider.js @@ -4,23 +4,28 @@ import { import { iconsByType } from '../../render/components/icons'; -const labelsByType = { - button: 'BUTTON', - checkbox: 'CHECKBOX', - checklist: 'CHECKLIST', - columns: 'COLUMNS', - default: 'FORM', - datetime: 'DATETIME', - image: 'IMAGE VIEW', - number: 'NUMBER', - radio: 'RADIO', - select: 'SELECT', - spacer: 'SPACER', - taglist: 'TAGLIST', - text: 'TEXT VIEW', - textfield: 'TEXT FIELD', - textarea: 'TEXT AREA', -}; +import { getPaletteIcon } from '../palette/components/Palette'; + +import { useService } from './hooks'; + +// todo(pinussilvestrus): obsolete, use form field definition label +// const labelsByType = { +// button: 'BUTTON', +// checkbox: 'CHECKBOX', +// checklist: 'CHECKLIST', +// columns: 'COLUMNS', +// default: 'FORM', +// datetime: 'DATETIME', +// image: 'IMAGE VIEW', +// number: 'NUMBER', +// radio: 'RADIO', +// select: 'SELECT', +// spacer: 'SPACER', +// taglist: 'TAGLIST', +// text: 'TEXT VIEW', +// textfield: 'TEXT FIELD', +// textarea: 'TEXT AREA', +// }; export const PropertiesPanelHeaderProvider = { @@ -49,10 +54,17 @@ export const PropertiesPanelHeaderProvider = { type } = field; - const Icon = iconsByType(type); + // @Note: We know that we are inside the properties panel context, + // so we can savely use the hook here. + // eslint-disable-next-line + const fieldDefinition = useService('formFields').get(type).config; + + const Icon = fieldDefinition.icon || iconsByType(type); if (Icon) { return () => ; + } else if (fieldDefinition.iconUrl) { + return getPaletteIcon({ iconUrl: fieldDefinition.iconUrl, label: fieldDefinition.label }); } }, @@ -61,6 +73,11 @@ export const PropertiesPanelHeaderProvider = { type } = field; - return labelsByType[type]; + // @Note: We know that we are inside the properties panel context, + // so we can savely use the hook here. + // eslint-disable-next-line + const fieldDefinition = useService('formFields').get(type).config; + + return fieldDefinition.label || type; } }; \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/PropertiesPanelRenderer.js b/packages/form-js-editor/src/features/properties-panel/PropertiesPanelRenderer.js index 04a3ae38d..ecadabd7d 100644 --- a/packages/form-js-editor/src/features/properties-panel/PropertiesPanelRenderer.js +++ b/packages/form-js-editor/src/features/properties-panel/PropertiesPanelRenderer.js @@ -9,10 +9,13 @@ import { query as domQuery } from 'min-dom'; +const DEFAULT_PRIORITY = 1000; + /** * @typedef { { parent: Element } } PropertiesPanelConfig * @typedef { import('../../core/EventBus').default } EventBus * @typedef { import('../../types').Injector } Injector + * @typedef { { getGroups: ({ formField, editFormField }) => ({ groups}) => Array } } PropertiesProvider */ /** @@ -82,6 +85,7 @@ export default class PropertiesPanelRenderer { _render() { render( , @@ -98,6 +102,44 @@ export default class PropertiesPanelRenderer { this._eventBus.fire('propertiesPanel.destroyed'); } } + + /** + * Register a new properties provider to the properties panel. + * + * @param {PropertiesProvider} provider + * @param {Number} [priority] + */ + registerProvider(provider, priority) { + + if (!priority) { + priority = DEFAULT_PRIORITY; + } + + if (typeof provider.getGroups !== 'function') { + console.error( + 'Properties provider does not implement #getGroups(element) API' + ); + + return; + } + + this._eventBus.on('propertiesPanel.getProviders', priority, function(event) { + event.providers.push(provider); + }); + + this._eventBus.fire('propertiesPanel.providersChanged'); + } + + _getProviders() { + const event = this._eventBus.createEvent({ + type: 'propertiesPanel.getProviders', + providers: [] + }); + + this._eventBus.fire(event); + + return event.providers; + } } PropertiesPanelRenderer.$inject = [ 'config.propertiesPanel', 'injector', 'eventBus' ]; \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/PropertiesProvider.js b/packages/form-js-editor/src/features/properties-panel/PropertiesProvider.js new file mode 100644 index 000000000..8acef2fc7 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/PropertiesProvider.js @@ -0,0 +1,84 @@ +import { + ConditionGroup, + AppearanceGroup, + CustomPropertiesGroup, + GeneralGroup, + SerializationGroup, + ConstraintsGroup, + ValidationGroup, + ValuesGroups, + LayoutGroup +} from './groups'; + +import { hasEntryConfigured } from './Util'; + +export default class PropertiesProvider { + constructor(propertiesPanel, injector) { + this._injector = injector; + propertiesPanel.registerProvider(this); + } + + _filterVisibleEntries(groups, field, getService) { + return groups.forEach(group => { + const { + entries + } = group; + + const { + type + } = field; + + const formFields = getService('formFields'); + + const fieldDefinition = formFields.get(type).config; + + if (!entries) { + return; + } + + group.entries = entries.filter(entry => { + const { + isDefaultVisible + } = entry; + + if (!isDefaultVisible) { + return true; + } + + return isDefaultVisible(field) || hasEntryConfigured(fieldDefinition, entry.id); + }); + }); + } + + getGroups(field, editField) { + return (groups) => { + if (!field) { + return groups; + } + + const getService = (type, strict = true) => this._injector.get(type, strict); + + groups = [ + ...groups, + GeneralGroup(field, editField, getService), + ConditionGroup(field, editField), + LayoutGroup(field, editField), + AppearanceGroup(field, editField), + SerializationGroup(field, editField), + ...ValuesGroups(field, editField), + ConstraintsGroup(field, editField), + ValidationGroup(field, editField), + CustomPropertiesGroup(field, editField) + ].filter(group => group != null); + + this._filterVisibleEntries(groups, field, getService); + + // contract: if a group has no entries or items, it should not be displayed at all + return groups.filter(group => { + return group.items || group.entries && group.entries.length; + }); + }; + } +} + +PropertiesProvider.$inject = [ 'propertiesPanel', 'injector' ]; \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/Util.js b/packages/form-js-editor/src/features/properties-panel/Util.js index 04b25aeb6..0671c1f11 100644 --- a/packages/form-js-editor/src/features/properties-panel/Util.js +++ b/packages/form-js-editor/src/features/properties-panel/Util.js @@ -73,4 +73,14 @@ export const VALUES_INPUTS = [ 'radio', 'select', 'taglist' -]; \ No newline at end of file +]; + +export function hasEntryConfigured(formFieldDefinition, entryId) { + const { propertiesPanelEntries = [] } = formFieldDefinition; + + if (!propertiesPanelEntries.length) { + return false; + } + + return propertiesPanelEntries.some(entry => entry === entryId); +} \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/entries/ActionEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/ActionEntry.js index 474589f45..364987891 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/ActionEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/ActionEntry.js @@ -9,21 +9,16 @@ export default function ActionEntry(props) { field } = props; - const { - type - } = field; - const entries = []; - if (type === 'button') { - entries.push({ - id: 'action', - component: Action, - editField: editField, - field: field, - isEdited: isSelectEntryEdited - }); - } + entries.push({ + id: 'action', + component: Action, + editField: editField, + field: field, + isEdited: isSelectEntryEdited, + isDefaultVisible: (field) => field.type === 'button' + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/AdornerEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/AdornerEntry.js index 8ddfb42c0..a9e28ca6b 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/AdornerEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/AdornerEntry.js @@ -9,10 +9,6 @@ export default function AdornerEntry(props) { field } = props; - const { - type - } = field; - const entries = []; const onChange = (key) => { @@ -29,27 +25,27 @@ export default function AdornerEntry(props) { }; }; - if ([ 'number', 'textfield' ].includes(type)) { - entries.push({ - id: 'prefix-adorner', - component: PrefixAdorner, - isEdited: isFeelEntryEdited, - editField, - field, - onChange, - getValue - }); - - entries.push({ - id: 'suffix-adorner', - component: SuffixAdorner, - isEdited: isFeelEntryEdited, - editField, - field, - onChange, - getValue - }); - } + entries.push({ + id: 'prefix-adorner', + component: PrefixAdorner, + isEdited: isFeelEntryEdited, + editField, + field, + onChange, + getValue, + isDefaultVisible: (field) => [ 'number', 'textfield' ].includes(field.type) + }); + + entries.push({ + id: 'suffix-adorner', + component: SuffixAdorner, + isEdited: isFeelEntryEdited, + editField, + field, + onChange, + getValue, + isDefaultVisible: (field) => [ 'number', 'textfield' ].includes(field.type) + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/AltTextEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/AltTextEntry.js index bb734f614..dcd6e6716 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/AltTextEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/AltTextEntry.js @@ -10,21 +10,16 @@ export default function AltTextEntry(props) { field } = props; - const { - type - } = field; - const entries = []; - if (type === 'image') { - entries.push({ - id: 'alt', - component: AltText, - editField: editField, - field: field, - isEdited: isFeelEntryEdited - }); - } + entries.push({ + id: 'alt', + component: AltText, + editField: editField, + field: field, + isEdited: isFeelEntryEdited, + isDefaultVisible: (field) => [ 'image' ].includes(field.type) + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DateTimeConstraintsEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeConstraintsEntry.js index 5b9b1c449..098d2f976 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/DateTimeConstraintsEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeConstraintsEntry.js @@ -11,36 +11,37 @@ export default function DateTimeConstraintsEntry(props) { id } = props; - const { - type, - subtype - } = field; - - if (type !== 'datetime') { - return []; + function isDefaultVisible(subtypes) { + return (field) => { + if (field.type !== 'datetime') { + return false; + } + + return subtypes.includes(field.subtype); + }; } const entries = []; - if (subtype === DATETIME_SUBTYPES.TIME || subtype === DATETIME_SUBTYPES.DATETIME) { - entries.push({ - id: id + '-timeInterval', - component: TimeIntervalSelect, - isEdited: isSelectEntryEdited, - editField, - field - }); - } + entries.push({ + id: id + '-timeInterval', + component: TimeIntervalSelect, + isEdited: isSelectEntryEdited, + editField, + field, + isDefaultVisible: isDefaultVisible([ DATETIME_SUBTYPES.TIME, DATETIME_SUBTYPES.DATETIME ]) + }); + + + entries.push({ + id: id + '-disallowPassedDates', + component: DisallowPassedDates, + isEdited: isCheckboxEntryEdited, + editField, + field, + isDefaultVisible: isDefaultVisible([ DATETIME_SUBTYPES.DATE, DATETIME_SUBTYPES.DATETIME ]) + }); - if (subtype === DATETIME_SUBTYPES.DATE || subtype === DATETIME_SUBTYPES.DATETIME) { - entries.push({ - id: id + '-disallowPassedDates', - component: DisallowPassedDates, - isEdited: isCheckboxEntryEdited, - editField, - field - }); - } return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DateTimeEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeEntry.js index 770547cf8..ff9d245b4 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/DateTimeEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeEntry.js @@ -21,36 +21,27 @@ export default function DateTimeEntry(props) { field } = props; - const { - type, - subtype - } = field; - - if (type !== 'datetime') { - return []; - } - const entries = [ { id: 'subtype', component: DateTimeSubtypeSelect, isEdited: isSelectEntryEdited, editField, - field + field, + isDefaultVisible: (field) => field.type === 'datetime' } ]; - if (subtype === DATETIME_SUBTYPES.TIME || subtype === DATETIME_SUBTYPES.DATETIME) { - - entries.push({ - id: 'use24h', - component: Use24h, - isEdited: isCheckboxEntryEdited, - editField, - field - }); - - } + entries.push({ + id: 'use24h', + component: Use24h, + isEdited: isCheckboxEntryEdited, + editField, + field, + isDefaultVisible: (field) => field.type === 'datetime' && ( + field.subtype === DATETIME_SUBTYPES.TIME || field.subtype === DATETIME_SUBTYPES.DATETIME + ) + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DateTimeSerializationEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeSerializationEntry.js index c244e45f3..06f269f77 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/DateTimeSerializationEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeSerializationEntry.js @@ -10,26 +10,18 @@ export default function DateTimeFormatEntry(props) { field } = props; - const { - type, - subtype - } = field; - - if (type !== 'datetime') { - return []; - } - const entries = []; - if (subtype === DATETIME_SUBTYPES.TIME || subtype === DATETIME_SUBTYPES.DATETIME) { - entries.push({ - id: 'time-format', - component: TimeFormatSelect, - isEdited: isSelectEntryEdited, - editField, - field - }); - } + entries.push({ + id: 'time-format', + component: TimeFormatSelect, + isEdited: isSelectEntryEdited, + editField, + field, + isDefaultVisible: (field) => field.type === 'datetime' && ( + field.subtype === DATETIME_SUBTYPES.TIME || field.subtype === DATETIME_SUBTYPES.DATETIME + ) + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DefaultValueEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DefaultValueEntry.js index d46e176cb..bce2d34ba 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/DefaultValueEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/DefaultValueEntry.js @@ -29,9 +29,17 @@ export default function DefaultOptionEntry(props) { const entries = []; - // Only make default values available when they are statically defined - if (!INPUTS.includes(type) || VALUES_INPUTS.includes(type) && !field.values) { - return entries; + function isDefaultVisible(matchers) { + return (field) => { + + // Only make default values available when they are statically defined + if (!INPUTS.includes(type) || VALUES_INPUTS.includes(type) && !field.values) { + return false; + } + + return matchers(field); + }; + } const defaultOptions = { @@ -41,47 +49,42 @@ export default function DefaultOptionEntry(props) { label: 'Default value' }; - if (type === 'checkbox') { - entries.push({ - ...defaultOptions, - component: DefaultValueCheckbox, - isEdited: isSelectEntryEdited - }); - } + entries.push({ + ...defaultOptions, + component: DefaultValueCheckbox, + isEdited: isSelectEntryEdited, + isDefaultVisible: isDefaultVisible((field) => field.type === 'checkbox') + }); - if (type === 'number') { - entries.push({ - ...defaultOptions, - component: DefaultValueNumber, - isEdited: isTextFieldEntryEdited - }); - } + entries.push({ + ...defaultOptions, + component: DefaultValueNumber, + isEdited: isTextFieldEntryEdited, + isDefaultVisible: isDefaultVisible((field) => field.type === 'number') + }); - if (type === 'radio' || type === 'select') { - entries.push({ - ...defaultOptions, - component: DefaultValueSingleSelect, - isEdited: isSelectEntryEdited - }); - } + entries.push({ + ...defaultOptions, + component: DefaultValueSingleSelect, + isEdited: isSelectEntryEdited, + isDefaultVisible: isDefaultVisible((field) => field.type === 'radio' || field.type === 'select') + }); // todo(Skaiir): implement a multiselect equivalent (cf. https://github.com/bpmn-io/form-js/issues/265) - if (type === 'textfield') { - entries.push({ - ...defaultOptions, - component: DefaultValueTextfield, - isEdited: isTextFieldEntryEdited - }); - } + entries.push({ + ...defaultOptions, + component: DefaultValueTextfield, + isEdited: isTextFieldEntryEdited, + isDefaultVisible: isDefaultVisible((field) => field.type === 'textfield') + }); - if (type === 'textarea') { - entries.push({ - ...defaultOptions, - component: DefaultValueTextarea, - isEdited: isTextAreaEntryEdited - }); - } + entries.push({ + ...defaultOptions, + component: DefaultValueTextarea, + isEdited: isTextAreaEntryEdited, + isDefaultVisible: isDefaultVisible((field) => field.type === 'textarea') + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js index 8634090d0..16925f2e8 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js @@ -13,21 +13,16 @@ export default function DescriptionEntry(props) { field } = props; - const { - type - } = field; - const entries = []; - if (INPUTS.includes(type)) { - entries.push({ - id: 'description', - component: Description, - editField: editField, - field: field, - isEdited: isFeelEntryEdited - }); - } + entries.push({ + id: 'description', + component: Description, + editField: editField, + field: field, + isEdited: isFeelEntryEdited, + isDefaultVisible: (field) => INPUTS.includes(field.type) + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DisabledEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DisabledEntry.js index 7333dbdb6..7a72872c4 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/DisabledEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/DisabledEntry.js @@ -11,21 +11,16 @@ export default function DisabledEntry(props) { field } = props; - const { - type - } = field; - const entries = []; - if (INPUTS.includes(type)) { - entries.push({ - id: 'disabled', - component: Disabled, - editField: editField, - field: field, - isEdited: isToggleSwitchEntryEdited - }); - } + entries.push({ + id: 'disabled', + component: Disabled, + editField: editField, + field: field, + isEdited: isToggleSwitchEntryEdited, + isDefaultVisible: (field) => INPUTS.includes(field.type) + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/IdEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/IdEntry.js index 739a8f760..e571e692a 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/IdEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/IdEntry.js @@ -13,15 +13,14 @@ export default function IdEntry(props) { const entries = []; - if (field.type === 'default') { - entries.push({ - id: 'id', - component: Id, - editField: editField, - field: field, - isEdited: isTextFieldEntryEdited - }); - } + entries.push({ + id: 'id', + component: Id, + editField: editField, + field: field, + isEdited: isTextFieldEntryEdited, + isDefaultVisible: (field) => field.type === 'default' + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/ImageSourceEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/ImageSourceEntry.js index 3b39f432d..4461f85f7 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/ImageSourceEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/ImageSourceEntry.js @@ -10,21 +10,15 @@ export default function SourceEntry(props) { field } = props; - const { - type - } = field; - const entries = []; - - if (type === 'image') { - entries.push({ - id: 'source', - component: Source, - editField: editField, - field: field, - isEdited: isFeelEntryEdited - }); - } + entries.push({ + id: 'source', + component: Source, + editField: editField, + field: field, + isEdited: isFeelEntryEdited, + isDefaultVisible: (field) => field.type === 'image' + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/KeyEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/KeyEntry.js index 469697a95..b733f22da 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/KeyEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/KeyEntry.js @@ -15,21 +15,16 @@ export default function KeyEntry(props) { field } = props; - const { - type - } = field; - const entries = []; - if (INPUTS.includes(type)) { - entries.push({ - id: 'key', - component: Key, - editField: editField, - field: field, - isEdited: isTextFieldEntryEdited - }); - } + entries.push({ + id: 'key', + component: Key, + editField: editField, + field: field, + isEdited: isTextFieldEntryEdited, + isDefaultVisible: (field) => INPUTS.includes(field.type) + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js index 0ffdf20f5..4b3d19f2c 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js @@ -10,48 +10,50 @@ export default function LabelEntry(props) { editField } = props; - const { - type, - subtype - } = field; - const entries = []; - if (type === 'datetime') { - if (subtype === DATETIME_SUBTYPES.DATE || subtype === DATETIME_SUBTYPES.DATETIME) { - entries.push( - { - id: 'date-label', - component: DateLabel, - editField, - field, - isEdited: isFeelEntryEdited - } - ); - } - if (subtype === DATETIME_SUBTYPES.TIME || subtype === DATETIME_SUBTYPES.DATETIME) { - entries.push( - { - id: 'time-label', - component: TimeLabel, - editField, - field, - isEdited: isFeelEntryEdited - } - ); + entries.push( + { + id: 'date-label', + component: DateLabel, + editField, + field, + isEdited: isFeelEntryEdited, + isDefaultVisible: function(field) { + return ( + field.type === 'datetime' && + (field.subtype === DATETIME_SUBTYPES.DATE || field.subtype === DATETIME_SUBTYPES.DATETIME) + ); + } } - } - else if (INPUTS.includes(type) || type === 'button') { - entries.push( - { - id: 'label', - component: Label, - editField, - field, - isEdited: isFeelEntryEdited + ); + + entries.push( + { + id: 'time-label', + component: TimeLabel, + editField, + field, + isEdited: isFeelEntryEdited, + isDefaultVisible: function(field) { + return ( + field.type === 'datetime' && + (field.subtype === DATETIME_SUBTYPES.TIME || field.subtype === DATETIME_SUBTYPES.DATETIME) + ); } - ); - } + } + ); + + entries.push( + { + id: 'label', + component: Label, + editField, + field, + isEdited: isFeelEntryEdited, + isDefaultVisible: (field) => INPUTS.includes(field.type) || field.type === 'button' + } + ); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/NumberEntries.js b/packages/form-js-editor/src/features/properties-panel/entries/NumberEntries.js index 531c974dd..e407e5c0c 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/NumberEntries.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/NumberEntries.js @@ -12,14 +12,6 @@ export default function NumberEntries(props) { id } = props; - const { - type - } = field; - - if (type !== 'number') { - return []; - } - const entries = []; entries.push({ @@ -27,7 +19,8 @@ export default function NumberEntries(props) { component: NumberDecimalDigits, isEdited: isNumberFieldEntryEdited, editField, - field + field, + isDefaultVisible: (field) => field.type === 'number' }); entries.push({ @@ -35,7 +28,8 @@ export default function NumberEntries(props) { component: NumberArrowStep, isEdited: isTextFieldEntryEdited, editField, - field + field, + isDefaultVisible: (field) => field.type === 'number' }); return entries; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/NumberSerializationEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/NumberSerializationEntry.js index f0de90c66..600417e13 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/NumberSerializationEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/NumberSerializationEntry.js @@ -10,14 +10,6 @@ export default function NumberSerializationEntry(props) { field } = props; - const { - type - } = field; - - if (type !== 'number') { - return []; - } - const entries = []; entries.push({ @@ -25,7 +17,8 @@ export default function NumberSerializationEntry(props) { component: SerializeToString, isEdited: isCheckboxEntryEdited, editField, - field + field, + isDefaultVisible: (field) => field.type === 'number' }); return entries; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/ReadonlyEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/ReadonlyEntry.js index 65f5bc1f9..eb15a30dc 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/ReadonlyEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/ReadonlyEntry.js @@ -13,21 +13,16 @@ export default function ReadonlyEntry(props) { field } = props; - const { - type - } = field; - const entries = []; - if (INPUTS.includes(type)) { - entries.push({ - id: 'readonly', - component: Readonly, - editField: editField, - field: field, - isEdited: isFeelEntryEdited - }); - } + entries.push({ + id: 'readonly', + component: Readonly, + editField: editField, + field: field, + isEdited: isFeelEntryEdited, + isDefaultVisible: (field) => INPUTS.includes(field.type) + }); return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/SelectEntries.js b/packages/form-js-editor/src/features/properties-panel/entries/SelectEntries.js index 879826464..9b3a45f01 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/SelectEntries.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/SelectEntries.js @@ -2,24 +2,13 @@ import { simpleBoolEntryFactory } from './factories'; export default function SelectEntries(props) { - const { - field, - } = props; - - const { - type - } = field; - - if (type !== 'select') { - return []; - } - const entries = [ simpleBoolEntryFactory({ id: 'searchable', path: [ 'searchable' ], label: 'Searchable', - props + props, + isDefaultVisible: (field) => field.type === 'select' }) ]; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/SpacerEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/SpacerEntry.js index d691db375..a2870534e 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/SpacerEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/SpacerEntry.js @@ -10,14 +10,6 @@ export default function SpacerEntry(props) { id } = props; - const { - type - } = field; - - if (type !== 'spacer') { - return []; - } - const entries = []; entries.push({ @@ -25,7 +17,8 @@ export default function SpacerEntry(props) { component: SpacerHeight, isEdited: isNumberFieldEntryEdited, editField, - field + field, + isDefaultVisible: (field) => field.type === 'spacer' }); return entries; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/TextEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/TextEntry.js index 7d3cf1d6a..aa0375495 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/TextEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/TextEntry.js @@ -11,28 +11,17 @@ import { useMemo } from 'preact/hooks'; export default function TextEntry(props) { const { editField, - - /* getService, */ field } = props; - const { - type - } = field; - - // const templating = getService('templating'); - - if (type !== 'text') { - return []; - } - const entries = [ { id: 'text', component: Text, editField: editField, field: field, - isEdited: isFeelEntryEdited + isEdited: isFeelEntryEdited, + isDefaultVisible: (field) => field.type === 'text' } ]; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleBoolEntryFactory.js b/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleBoolEntryFactory.js index a46c34615..640e65a43 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleBoolEntryFactory.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleBoolEntryFactory.js @@ -7,7 +7,8 @@ export default function simpleBoolEntryFactory(options) { label, description, path, - props + props, + isDefaultVisible } = options; const { @@ -23,7 +24,8 @@ export default function simpleBoolEntryFactory(options) { editField, description, component: SimpleBoolComponent, - isEdited: isCheckboxEntryEdited + isEdited: isCheckboxEntryEdited, + isDefaultVisible }; } diff --git a/packages/form-js-editor/src/features/properties-panel/groups/ValidationGroup.js b/packages/form-js-editor/src/features/properties-panel/groups/ValidationGroup.js index f0e246fe5..7c360cf32 100644 --- a/packages/form-js-editor/src/features/properties-panel/groups/ValidationGroup.js +++ b/packages/form-js-editor/src/features/properties-panel/groups/ValidationGroup.js @@ -37,10 +37,6 @@ export default function ValidationGroup(field, editField) { const validate = get(field, [ 'validate' ], {}); const isCustomValidation = [ undefined, VALIDATION_TYPE_OPTIONS.custom.value ].includes(validate.validationType); - if (!INPUTS.includes(type)) { - return null; - } - const onChange = (key) => { return (value) => { const validate = get(field, [ 'validate' ], {}); @@ -62,78 +58,83 @@ export default function ValidationGroup(field, editField) { getValue, field, isEdited: isCheckboxEntryEdited, - onChange + onChange, + isDefaultVisible: (field) => INPUTS.includes(field.type) } ]; - if (type === 'textfield') { - entries.push( - { - id: 'validationType', - component: ValidationType, - getValue, - field, - editField, - isEdited: isTextFieldEntryEdited, - onChange - } - ); - } - - if (type === 'textarea' || (type === 'textfield' && isCustomValidation)) { - entries.push( - { - id: 'minLength', - component: MinLength, - getValue, - field, - isEdited: isFeelEntryEdited, - onChange - }, - { - id: 'maxLength', - component: MaxLength, - getValue, - field, - isEdited: isFeelEntryEdited, - onChange - } - ); - } - - if (type === 'textfield' && isCustomValidation) { - entries.push( - { - id: 'pattern', - component: Pattern, - getValue, - field, - isEdited: isTextFieldEntryEdited, - onChange - } - ); - } - - if (type === 'number') { - entries.push( - { - id: 'min', - component: Min, - getValue, - field, - isEdited: isFeelEntryEdited, - onChange - }, - { - id: 'max', - component: Max, - getValue, - field, - isEdited: isFeelEntryEdited, - onChange - } - ); - } + entries.push( + { + id: 'validationType', + component: ValidationType, + getValue, + field, + editField, + isEdited: isTextFieldEntryEdited, + onChange, + isDefaultVisible: (field) => field.type === 'textfield' + } + ); + + entries.push( + { + id: 'minLength', + component: MinLength, + getValue, + field, + isEdited: isFeelEntryEdited, + onChange, + isDefaultVisible: (field) => INPUTS.includes(field.type) && ( + type === 'textarea' || (type === 'textfield' && isCustomValidation) + ) + }, + { + id: 'maxLength', + component: MaxLength, + getValue, + field, + isEdited: isFeelEntryEdited, + onChange, + isDefaultVisible: (field) => INPUTS.includes(field.type) && ( + type === 'textarea' || (type === 'textfield' && isCustomValidation) + ) + } + ); + + entries.push( + { + id: 'pattern', + component: Pattern, + getValue, + field, + isEdited: isTextFieldEntryEdited, + onChange, + isDefaultVisible: (field) => INPUTS.includes(field.type) && ( + type === 'textfield' && isCustomValidation + ) + } + ); + + entries.push( + { + id: 'min', + component: Min, + getValue, + field, + isEdited: isFeelEntryEdited, + onChange, + isDefaultVisible: (field) => field.type === 'number' + }, + { + id: 'max', + component: Max, + getValue, + field, + isEdited: isFeelEntryEdited, + onChange, + isDefaultVisible: (field) => field.type === 'number' + } + ); return { id: 'validation', diff --git a/packages/form-js-editor/src/features/properties-panel/index.js b/packages/form-js-editor/src/features/properties-panel/index.js index 31d72ca6c..bcffb51c5 100644 --- a/packages/form-js-editor/src/features/properties-panel/index.js +++ b/packages/form-js-editor/src/features/properties-panel/index.js @@ -1,6 +1,8 @@ import PropertiesPanelModule from './PropertiesPanelRenderer'; +import PropertiesProvider from './PropertiesProvider'; export default { - __init__: [ 'propertiesPanel' ], - propertiesPanel: [ 'type', PropertiesPanelModule ] + __init__: [ 'propertiesPanel', 'propertiesProvider' ], + propertiesPanel: [ 'type', PropertiesPanelModule ], + propertiesProvider: [ 'type', PropertiesProvider ] }; \ No newline at end of file diff --git a/packages/form-js-editor/src/render/components/FormEditor.js b/packages/form-js-editor/src/render/components/FormEditor.js index c72b89a72..98561f709 100644 --- a/packages/form-js-editor/src/render/components/FormEditor.js +++ b/packages/form-js-editor/src/render/components/FormEditor.js @@ -27,7 +27,7 @@ import { DragAndDropContext } from '../context'; import { DeleteIcon, DraggableIcon } from './icons'; import ModularSection from './ModularSection'; -import Palette, { PALETTE_ENTRIES } from '../../features/palette/components/Palette'; +import Palette, { collectPaletteEntries, getPaletteIcon } from '../../features/palette/components/Palette'; import InjectedRendersRoot from '../../features/render-injection/components/InjectedRendersRoot'; import { SlotFillRoot } from '../../features/render-injection/slot-fill'; @@ -50,8 +50,6 @@ import { import { set as setCursor, unset as unsetCursor } from '../util/Cursor'; -import { iconsByType } from './icons'; - function ContextPad(props) { if (!props.children) { return null; @@ -470,6 +468,8 @@ function CreatePreview(props) { const { drake } = useContext(DragAndDropContext); + const formFields = useService('formFields'); + function handleCloned(clone, original, type) { const fieldType = clone.dataset.fieldType; @@ -477,9 +477,15 @@ function CreatePreview(props) { // (1) field preview if (fieldType) { - const { label } = findPaletteEntry(fieldType); + const paletteEntry = findPaletteEntry(fieldType, formFields); + + if (!paletteEntry) { + return; + } + + const { label } = paletteEntry; - const Icon = iconsByType(fieldType); + const Icon = getPaletteIcon(paletteEntry); clone.innerHTML = ''; @@ -534,8 +540,8 @@ function CreatePreview(props) { // helper ////// -function findPaletteEntry(type) { - return PALETTE_ENTRIES.find(entry => entry.type === type); +function findPaletteEntry(type, formFields) { + return collectPaletteEntries(formFields).find(entry => entry.type === type); } function defaultPropertiesPanel(propertiesPanelConfig) { diff --git a/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js b/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js index e82730bd3..c445af9cd 100644 --- a/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js +++ b/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js @@ -12,6 +12,8 @@ import PropertiesPanel from '../../../../src/features/properties-panel/Propertie import { VALUES_SOURCES, VALUES_SOURCES_DEFAULTS } from '@bpmn-io/form-js-viewer'; import { removeKey } from '../../../../src/features/properties-panel/groups/CustomPropertiesGroup'; +import PropertiesProvider from '../../../../src/features/properties-panel/PropertiesProvider'; + import { EventBus as eventBusMock, FormEditor as formEditorMock, @@ -19,7 +21,8 @@ import { Selection as selectionMock, Modeling as modelingMock, Templating as templatingMock, - Injector as injectorMock + Injector as injectorMock, + PropertiesPanelMock as propertiesPanelMock } from './helper'; import schema from '../../form.json'; @@ -3671,7 +3674,16 @@ function createPropertiesPanel(options = {}, renderFn = render) { templating }); + const propertiesPanel = new propertiesPanelMock(); + + const getProviders = () => { + return [ + new PropertiesProvider(propertiesPanel, injector) + ]; + }; + return renderFn(, { diff --git a/packages/form-js-editor/test/spec/features/properties-panel/groups/AppearanceGroup.spec.js b/packages/form-js-editor/test/spec/features/properties-panel/groups/AppearanceGroup.spec.js index 99f4ac0e8..a9f3a08a5 100644 --- a/packages/form-js-editor/test/spec/features/properties-panel/groups/AppearanceGroup.spec.js +++ b/packages/form-js-editor/test/spec/features/properties-panel/groups/AppearanceGroup.spec.js @@ -20,10 +20,10 @@ describe('AppearanceGroup', function() { // given const field = { type: 'checkbox' }; - const group = AppearanceGroup(field); + renderAppearanceGroup({ field }); // then - expect(group).to.not.exist; + expect(findGroup('appearance', document.body)).to.not.exist; }); @@ -236,4 +236,8 @@ function findFeelers(id, container) { function findTextbox(id, container) { return container.querySelector(`[name=${id}] [role="textbox"]`); +} + +function findGroup(id, container) { + return container.querySelector(`.bio-properties-panel-group [data-group-id="group-${id}"]`); } \ No newline at end of file diff --git a/packages/form-js-editor/test/spec/features/properties-panel/groups/SerializationGroup.spec.js b/packages/form-js-editor/test/spec/features/properties-panel/groups/SerializationGroup.spec.js index 20e8ae560..2f716f4c2 100644 --- a/packages/form-js-editor/test/spec/features/properties-panel/groups/SerializationGroup.spec.js +++ b/packages/form-js-editor/test/spec/features/properties-panel/groups/SerializationGroup.spec.js @@ -21,8 +21,8 @@ describe('SerializationGroup', function() { // then for (const type of types) { - const group = SerializationGroup({ type }, () => {}); - expect(group).to.be.null; + renderSerializationGroup({ field : { type } }); + expect(findGroup('serialization', document.body)).to.be.null; } }); @@ -32,8 +32,8 @@ describe('SerializationGroup', function() { const field = { type: 'datetime', subtype: 'date' }; // then - const group = SerializationGroup(field, () => { }); - expect(group).to.be.null; + renderSerializationGroup({ field }); + expect(findGroup('serialization', document.body)).to.be.null; }); @@ -199,3 +199,7 @@ function findInput(id, container) { function findSelect(id, container) { return container.querySelector(`select[name="${id}"]`); } + +function findGroup(id, container) { + return container.querySelector(`.bio-properties-panel-group [data-group-id="group-${id}"]`); +} diff --git a/packages/form-js-editor/test/spec/features/properties-panel/helper/index.js b/packages/form-js-editor/test/spec/features/properties-panel/helper/index.js index 2a95a8add..c1033f12b 100644 --- a/packages/form-js-editor/test/spec/features/properties-panel/helper/index.js +++ b/packages/form-js-editor/test/spec/features/properties-panel/helper/index.js @@ -1,5 +1,9 @@ import { PropertiesPanel } from '@bpmn-io/properties-panel'; + +import { FormFields } from '@bpmn-io/form-js-viewer'; + import { FormEditorContext } from '../../../../../src/render/context'; + import { FormPropertiesPanelContext } from '../../../../../src/features/properties-panel/context'; @@ -146,6 +150,10 @@ export class Injector { if (type === 'formFieldRegistry') { return this._options.formFieldRegistry || new FormFieldRegistry(); } + + if (type === 'formFields') { + return this._options.formFields || new FormFields(); + } } } @@ -160,6 +168,10 @@ export class Templating { } } +export class PropertiesPanelMock { + registerProvider() {} +} + export function WithFormEditorContext(Component, services = {}) { const formEditorContext = { getService(type, strict) { @@ -212,6 +224,8 @@ export function WithFormEditorContext(Component, services = {}) { return { isExpression: () => false }; + } else if (type === 'formFields') { + return new FormFields(); } } }; @@ -275,6 +289,8 @@ export function WithPropertiesPanelContext(Component, services = {}) { return { isExpression: () => false }; + } else if (type === 'formFields') { + return new FormFields(); } } }; @@ -290,10 +306,15 @@ export function WithPropertiesPanel(options = {}) { const { field = noopField, - groups = [], headerProvider = noopHeaderProvider } = options; + let { + groups = [] + } = options; + + groups = applyDefaultVisible(field, groups); + return ( ); +} + +// helpers ////////////////////// + +function applyDefaultVisible(field, groups) { + + groups.forEach(group => { + const { + entries + } = group; + + if (!entries || !entries.length) { + return true; + } + + group.entries = entries.filter(entry => { + const { + isDefaultVisible + } = entry; + + if (!isDefaultVisible) { + return true; + } + + return isDefaultVisible(field); + }); + }); + + return groups.filter(group => group.entries && group.entries.length); } \ No newline at end of file diff --git a/packages/form-js-playground/karma.conf.js b/packages/form-js-playground/karma.conf.js index 42adc3351..1440a922e 100644 --- a/packages/form-js-playground/karma.conf.js +++ b/packages/form-js-playground/karma.conf.js @@ -1,3 +1,9 @@ +const path = require('path'); + +const { + NormalModuleReplacementPlugin +} = require('webpack'); + const coverage = process.env.COVERAGE; // configures browsers to run test against @@ -63,6 +69,10 @@ module.exports = function(karma) { 'css-loader' ] }, + { + test: /\.svg$/, + use: [ '@svgr/webpack' ] + }, { test: /\.m?js$/, exclude: /node_modules/, @@ -90,6 +100,30 @@ module.exports = function(karma) { } ] }, + plugins: [ + new NormalModuleReplacementPlugin( + /^(..\/preact|preact)(\/[^/]+)?$/, + function(resource) { + + const replMap = { + 'preact/hooks': path.resolve('../../node_modules/preact/hooks/dist/hooks.module.js'), + 'preact/jsx-runtime': path.resolve('../../node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js'), + 'preact': path.resolve('../../node_modules/preact/dist/preact.module.js'), + '../preact/hooks': path.resolve('../../node_modules/preact/hooks/dist/hooks.module.js'), + '../preact/jsx-runtime': path.resolve('../../node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js'), + '../preact': path.resolve('../../node_modules/preact/dist/preact.module.js') + }; + + const replacement = replMap[resource.request]; + + if (!replacement) { + return; + } + + resource.request = replacement; + } + ), + ], devtool: 'eval-source-map' } }; diff --git a/packages/form-js-playground/test/custom/editor.js b/packages/form-js-playground/test/custom/editor.js new file mode 100644 index 000000000..c913a77b4 --- /dev/null +++ b/packages/form-js-playground/test/custom/editor.js @@ -0,0 +1,152 @@ +import { get, set } from 'min-dash'; + +import { + NumberFieldEntry, + isNumberFieldEntryEdited +} from '@bpmn-io/properties-panel'; + + +class CustomPropertiesProvider { + constructor(propertiesPanel) { + propertiesPanel.registerProvider(this, 500); + } + + getGroups(field, editField) { + return (groups) => { + + if (field.type !== 'range') { + return groups; + } + + const generalIdx = findGroupIdx(groups, 'general'); + + groups.splice(generalIdx + 1, 0, { + id: 'range', + label: 'Range', + entries: RangeEntries(field, editField) + }); + + return groups; + }; + } +} + +CustomPropertiesProvider.$inject = [ 'propertiesPanel' ]; + +function RangeEntries(field, editField) { + + const onChange = (key) => { + return (value) => { + const range = get(field, [ 'range' ], {}); + + editField(field, [ 'range' ], set(range, [ key ], value)); + }; + }; + + const getValue = (key) => { + return () => { + return get(field, [ 'range', key ]); + }; + }; + + return [ + { + id: 'range-min', + component: Min, + getValue, + field, + isEdited: isNumberFieldEntryEdited, + onChange + }, + { + id: 'range-max', + component: Max, + getValue, + field, + isEdited: isNumberFieldEntryEdited, + onChange + }, + { + id: 'range-step', + component: Step, + getValue, + field, + isEdited: isNumberFieldEntryEdited, + onChange + } + ]; + +} + +function Min(props) { + const { + field, + getValue, + id, + onChange + } = props; + + const debounce = (fn) => fn; + + return NumberFieldEntry({ + debounce, + element: field, + getValue: getValue('min'), + id, + label: 'Minimum', + setValue: onChange('min') + }); +} + +function Max(props) { + const { + field, + getValue, + id, + onChange + } = props; + + const debounce = (fn) => fn; + + return NumberFieldEntry({ + debounce, + element: field, + getValue: getValue('max'), + id, + label: 'Maximum', + setValue: onChange('max') + }); +} + +function Step(props) { + const { + field, + getValue, + id, + onChange + } = props; + + const debounce = (fn) => fn; + + return NumberFieldEntry({ + debounce, + element: field, + getValue: getValue('step'), + id, + min: 0, + label: 'Step', + setValue: onChange('step') + }); +} + + +export default { + __init__: [ 'customPropertiesProvider' ], + customPropertiesProvider: [ 'type', CustomPropertiesProvider ] +}; + +// helper ////////////////////// + +function findGroupIdx(groups, id) { + return groups.findIndex(g => g.id === id); +} \ No newline at end of file diff --git a/packages/form-js-playground/test/custom/range.svg b/packages/form-js-playground/test/custom/range.svg new file mode 100644 index 000000000..a3dbe11f6 --- /dev/null +++ b/packages/form-js-playground/test/custom/range.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/form-js-playground/test/custom/styles.css b/packages/form-js-playground/test/custom/styles.css new file mode 100644 index 000000000..244247ef4 --- /dev/null +++ b/packages/form-js-playground/test/custom/styles.css @@ -0,0 +1,4 @@ +.range-group { + display: flex; + flex-direction: row; +} \ No newline at end of file diff --git a/packages/form-js-playground/test/custom/viewer.js b/packages/form-js-playground/test/custom/viewer.js new file mode 100644 index 000000000..8b31f5199 --- /dev/null +++ b/packages/form-js-playground/test/custom/viewer.js @@ -0,0 +1,122 @@ + +import { + Errors, + FormContext, + Numberfield, + Description, + Label +} from '@bpmn-io/form-js-viewer'; + +import { useContext } from 'preact/hooks'; + +import classNames from 'classnames'; + +import RangeIcon from './range.svg'; + +const rangeType = 'range'; + +function RangeRenderer(props) { + + const { + disabled, + errors = [], + field, + readonly, + value + } = props; + + const { + description, + range = {}, + id, + label + } = field; + + const { + min, + max, + step + } = range; + + const { formId } = useContext(FormContext); + + const errorMessageId = errors.length === 0 ? undefined : `${prefixId(id, formId)}-error-message`; + + const onChange = ({ target }) => { + props.onChange({ + field, + value: Number(target.value) + }); + }; + + return
+
; +} + +RangeRenderer.config = { + ...Numberfield.config, + type: rangeType, + keyed: true, + label: 'Range', + group: 'basic-input', + propertiesPanelEntries: [ + 'key', + 'label', + 'description', + 'min', + 'max' + ], + icon: RangeIcon +}; + +class CustomFormFields { + constructor(formFields) { + formFields.register(rangeType, RangeRenderer); + } +} + +export default { + __init__: [ 'customFormFields' ], + customFormFields: [ 'type', CustomFormFields ] +}; + + +// helper ////////////////////// + +function formFieldClasses(type, { errors = [], disabled = false, readonly = false } = {}) { + if (!type) { + throw new Error('type required'); + } + + return classNames('fjs-form-field', `fjs-form-field-${type}`, { + 'fjs-has-errors': errors.length > 0, + 'fjs-disabled': disabled, + 'fjs-readonly': readonly + }); +} + +function prefixId(id, formId) { + if (formId) { + return `fjs-form-${ formId }-${ id }`; + } + + return `fjs-form-${ id }`; +} \ No newline at end of file diff --git a/packages/form-js-playground/test/spec/Playground.spec.js b/packages/form-js-playground/test/spec/Playground.spec.js index a6f2b80a1..d67c1df9a 100644 --- a/packages/form-js-playground/test/spec/Playground.spec.js +++ b/packages/form-js-playground/test/spec/Playground.spec.js @@ -17,6 +17,11 @@ import { import schema from './form.json'; import otherSchema from './other-form.json'; import rowsSchema from './rows-form.json'; +import customSchema from './custom.json'; + +import customViewerModule from '../custom/viewer'; +import customEditorModule from '../custom/editor'; +import customStyles from '../custom/styles.css'; import { insertCSS, @@ -32,9 +37,12 @@ insertCSS('Test.css', ` } `); +insertCSS('custom.css', customStyles); + const singleStartBasic = isSingleStart('basic'); const singleStartRows = isSingleStart('rows'); -const singleStart = singleStartBasic || singleStartRows; +const singleStartCustom = isSingleStart('custom'); +const singleStart = singleStartBasic || singleStartRows || singleStartCustom; describe('playground', function() { @@ -128,6 +136,35 @@ describe('playground', function() { }); + (singleStartCustom ? it.only : it)('should support custom element', async function() { + + // given + const data = { + creditor: 'John Doe Company', + amount: 25 + }; + + // when + // todo(pinussilvestrus): make it more convenient to add modules to both + // viewer and editor + playground = new Playground({ + container, + schema: customSchema, + data, + viewerAdditionalModules: [ + customViewerModule + ], + editorAdditionalModules: [ + customViewerModule, + customEditorModule + ] + }); + + // then + expect(playground).to.exist; + }); + + it('should NOT attach to empty parent', async function() { // given diff --git a/packages/form-js-playground/test/spec/custom.json b/packages/form-js-playground/test/spec/custom.json new file mode 100644 index 000000000..3b98dbe42 --- /dev/null +++ b/packages/form-js-playground/test/spec/custom.json @@ -0,0 +1,28 @@ +{ + "components": [ + { + "key": "creditor", + "label": "Creditor", + "type": "textfield", + "validate": { + "required": true + } + }, + { + "key": "amount", + "type": "range", + "label": "Amount", + "range": { + "min": 0, + "max": 100, + "step": 5 + } + }, + { + "type": "button", + "action": "submit", + "label": "Submit" + } + ], + "type": "default" +} \ No newline at end of file diff --git a/packages/form-js-viewer/src/render/components/index.js b/packages/form-js-viewer/src/render/components/index.js index bc24c16a2..3f8ebaa26 100644 --- a/packages/form-js-viewer/src/render/components/index.js +++ b/packages/form-js-viewer/src/render/components/index.js @@ -14,6 +14,16 @@ import Text from './form-fields/Text'; import Textfield from './form-fields/Textfield'; import Textarea from './form-fields/Textarea'; +import Label from './Label'; +import Description from './Description'; +import Errors from './Errors'; + +export { + Label, + Description, + Errors +}; + export { Button, Checkbox, @@ -49,4 +59,5 @@ export const formFields = [ Textarea ]; -export * from './icons'; \ No newline at end of file +export * from './icons'; +export * from './Sanitizer'; \ No newline at end of file diff --git a/packages/form-js-viewer/test/spec/Form.spec.js b/packages/form-js-viewer/test/spec/Form.spec.js index f6a290221..9f46f04aa 100644 --- a/packages/form-js-viewer/test/spec/Form.spec.js +++ b/packages/form-js-viewer/test/spec/Form.spec.js @@ -23,6 +23,7 @@ import textSchema from './text.json'; import textTemplateSchema from './text-template.json'; import stress from './stress.json'; import rowsSchema from './rows.json'; +import customFieldSchema from './customField.json'; import { insertCSS, @@ -40,12 +41,14 @@ const singleStartStress = isSingleStart('stress'); const singleStartRows = isSingleStart('rows'); const singleStartTheme = isSingleStart('theme'); const singleStartNoTheme = isSingleStart('no-theme'); +const singleStartCustom = isSingleStart('custom'); const singleStart = singleStartBasic || singleStartStress || singleStartRows || singleStartTheme || - singleStartNoTheme; + singleStartNoTheme || + singleStartCustom; describe('Form', function() { @@ -841,33 +844,31 @@ describe('Form', function() { }); - it('should be customizable', async function() { + (singleStartCustom ? it.only : it)('should be customizable', async function() { // given const data = { creditor: 'John Doe Company', - amount: 456, - invoiceNumber: 'C-123', - approved: true, - approvedBy: 'John Doe', - mailto: [ 'regional-manager', 'approver' ], - product: 'camunda-cloud', - tags: [ 'tag1', 'tag2', 'tag3' ], - language: 'english' + amount: 25 }; // when - await createForm({ + const form = await createForm({ container, data, - schema, + schema: customFieldSchema, additionalModules: [ customModule ] }); + form.on('changed', event => { + console.log('Form ', event); + }); + // then expect(document.querySelector('.custom-button')).to.exist; + expect(document.querySelector('.fjs-form-field-range')).to.exist; }); diff --git a/packages/form-js-viewer/test/spec/custom/index.js b/packages/form-js-viewer/test/spec/custom/index.js index ef6ac65c7..ef65610e9 100644 --- a/packages/form-js-viewer/test/spec/custom/index.js +++ b/packages/form-js-viewer/test/spec/custom/index.js @@ -1,6 +1,16 @@ -import { formFieldClasses } from '../../../src/render/components/Util'; +import { + formFieldClasses, + prefixId +} from '../../../src/render/components/Util'; -const type = 'button'; +import { Numberfield, Button } from '../../../src'; + +import { FormContext } from '../../../src/render/context'; + +import { useContext } from 'preact/hooks'; + +const btnType = Button.config.type; +const rangeType = 'range'; function CustomButtonRenderer(props) { const { @@ -10,22 +20,71 @@ function CustomButtonRenderer(props) { const { action = 'submit' } = field; - return
+ return
; } -CustomButtonRenderer.label = 'Custom Button'; +CustomButtonRenderer.config = { + ...Button.config, + label: 'Custom Button', +}; + +function RangeRenderer(props) { + + const { + field, + value + } = props; + + const { + min, + max, + step, + id, + label + } = field; + + const { formId } = useContext(FormContext); + + const onChange = ({ target }) => { + props.onChange({ + field, + value: Number(target.value) + }); + }; + + return
+ + +
; +} -CustomButtonRenderer.type = type; +RangeRenderer.config = { + ...Numberfield.config, + type: rangeType, + keyed: true, + label: 'Range', + group: 'basic-input' +}; -class RegisterCustomButtonRenderer { +class CustomFormFields { constructor(formFields) { - formFields.register(type, CustomButtonRenderer); + formFields.register(btnType, CustomButtonRenderer); + formFields.register(rangeType, RangeRenderer); } } export default { - __init__: [ 'customButtonRenderer' ], - customButtonRenderer: [ 'type', RegisterCustomButtonRenderer ] + __init__: [ 'customFormFields' ], + customFormFields: [ 'type', CustomFormFields ] }; \ No newline at end of file diff --git a/packages/form-js-viewer/test/spec/customField.json b/packages/form-js-viewer/test/spec/customField.json new file mode 100644 index 000000000..635ccf621 --- /dev/null +++ b/packages/form-js-viewer/test/spec/customField.json @@ -0,0 +1,26 @@ +{ + "components": [ + { + "key": "creditor", + "label": "Creditor", + "type": "textfield", + "validate": { + "required": true + } + }, + { + "key": "amount", + "type": "range", + "label": "Amount", + "min": 0, + "max": 100, + "step": 5 + }, + { + "type": "button", + "action": "submit", + "label": "Submit" + } + ], + "type": "default" +} \ No newline at end of file diff --git a/packages/form-js/src/editor.js b/packages/form-js/src/editor.js index 15c10b048..14a68d651 100644 --- a/packages/form-js/src/editor.js +++ b/packages/form-js/src/editor.js @@ -1,5 +1,2 @@ -export { - createFormEditor, - FormEditor, - schemaVersion -} from '@bpmn-io/form-js-editor'; \ No newline at end of file +// todo(pinussilvestrus): is it okay to export everything? +export * from '@bpmn-io/form-js-editor'; \ No newline at end of file diff --git a/packages/form-js/src/viewer.js b/packages/form-js/src/viewer.js index c6ca12878..e03a85a48 100644 --- a/packages/form-js/src/viewer.js +++ b/packages/form-js/src/viewer.js @@ -1,6 +1,2 @@ -export { - createForm, - Form, - schemaVersion, - getSchemaVariables -} from '@bpmn-io/form-js-viewer'; \ No newline at end of file +// todo(pinussilvestrus): is it okay to export everything? +export * from '@bpmn-io/form-js-viewer'; \ No newline at end of file