Skip to content

Commit

Permalink
wip wip custom form fields
Browse files Browse the repository at this point in the history
  • Loading branch information
Niklas Kiefer committed Aug 17, 2023
1 parent a2c4d0d commit 3a117d8
Show file tree
Hide file tree
Showing 46 changed files with 1,165 additions and 508 deletions.
58 changes: 51 additions & 7 deletions packages/form-js-editor/src/features/palette/components/Palette.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
useState
} from 'preact/hooks';

import { useService } from '../../../render/hooks';

import {
Slot
} from '../../render-injection/slot-fill';
Expand All @@ -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 {
Expand All @@ -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',
Expand All @@ -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
Expand All @@ -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 => {
Expand Down Expand Up @@ -120,8 +127,9 @@ export default function Palette(props) {
<span class="fjs-palette-group-title">{ label }</span>
<div class="fjs-palette-fields fjs-drag-container fjs-no-drop">
{
entries.map(({ label, type }) => {
const Icon = iconsByType(type);
entries.map(({ label, type, icon, iconUrl }) => {

const Icon = getPaletteIcon({ icon, iconUrl, label, type });

return (
<div
Expand Down Expand Up @@ -184,4 +192,40 @@ function getIndefiniteArticle(type) {
}

return 'a';
}

export function collectPaletteEntries(formFields) {
return Object.entries(formFields._formFields).map(([ type, formField ]) => {

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 = () => <img width={ 36 } style={ { margin: 'auto' } } alt={ label } src={ sanitizeImageSource(iconUrl) } />;
} else {
Icon = icon || iconsByType(type);
}

return Icon;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 (
<div
class="fjs-properties-panel"
Expand All @@ -111,7 +98,7 @@ export default function FormPropertiesPanel(props) {
<PropertiesPanel
element={ selectedFormField }
eventBus={ eventBus }
groups={ getGroups(selectedFormField, editField, getService) }
groups={ groups }
headerProvider={ PropertiesPanelHeaderProvider }
placeholderProvider={ PropertiesPanelPlaceholderProvider }
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {

Expand Down Expand Up @@ -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 () => <Icon width="36" height="36" viewBox="0 0 54 54" />;
} else if (fieldDefinition.iconUrl) {
return getPaletteIcon({ iconUrl: fieldDefinition.iconUrl, label: fieldDefinition.label });
}
},

Expand All @@ -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;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/

/**
Expand Down Expand Up @@ -82,6 +85,7 @@ export default class PropertiesPanelRenderer {
_render() {
render(
<PropertiesPanel
getProviders={ this._getProviders.bind(this) }
eventBus={ this._eventBus }
injector={ this._injector }
/>,
Expand All @@ -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' ];
Loading

0 comments on commit 3a117d8

Please sign in to comment.