diff --git a/package-lock.json b/package-lock.json index 738c6d101..d6c65e94e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10033,6 +10033,11 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==" + }, "node_modules/flatted": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", @@ -19848,6 +19853,7 @@ "@bpmn-io/snarkdown": "^2.1.0", "classnames": "^2.3.1", "didi": "^8.0.1", + "flatpickr": "^4.6.13", "ids": "^1.0.0", "min-dash": "^3.8.1", "preact": "^10.5.14", @@ -20658,6 +20664,7 @@ "@bpmn-io/snarkdown": "^2.1.0", "classnames": "^2.3.1", "didi": "^8.0.1", + "flatpickr": "^4.6.13", "ids": "^1.0.0", "min-dash": "^3.8.1", "preact": "^10.5.14", @@ -27628,6 +27635,11 @@ "rimraf": "^3.0.2" } }, + "flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==" + }, "flatted": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", diff --git a/packages/form-js-editor/package.json b/packages/form-js-editor/package.json index 08b89592e..b1871954c 100644 --- a/packages/form-js-editor/package.json +++ b/packages/form-js-editor/package.json @@ -21,7 +21,7 @@ "scripts": { "all": "run-s lint test build", "build": "run-p bundle generate-types", - "bundle": "rollup -c --failAfterWarnings", + "bundle": "rollup -c", "bundle:watch": "rollup -c -w", "dev": "npm test -- --auto-watch --no-single-run", "example:dev": "cd example && npm start", 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 4a0b85d3a..76ccbe4b0 100644 --- a/packages/form-js-editor/src/features/palette/components/Palette.js +++ b/packages/form-js-editor/src/features/palette/components/Palette.js @@ -9,6 +9,10 @@ const types = [ label: 'Number', type: 'number' }, + { + label: 'Datetime', + type: 'datetime' + }, { label: 'Checkbox', type: 'checkbox' 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 7b7c4da40..5ff025dde 100644 --- a/packages/form-js-editor/src/features/properties-panel/PropertiesPanel.js +++ b/packages/form-js-editor/src/features/properties-panel/PropertiesPanel.js @@ -14,6 +14,9 @@ import { PropertiesPanelPlaceholderProvider } from './PropertiesPanelPlaceholder import { CustomValuesGroup, GeneralGroup, + DisplayGroup, + FormatGroup, + ConstraintsGroup, ValidationGroup, ValuesGroups } from './groups'; @@ -26,7 +29,10 @@ function getGroups(field, editField) { const groups = [ GeneralGroup(field, editField), + DisplayGroup(field, editField), + FormatGroup(field, editField), ...ValuesGroups(field, editField), + ConstraintsGroup(field, editField), ValidationGroup(field, editField), CustomValuesGroup(field, editField) ]; 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 043ae154c..55ad172f0 100644 --- a/packages/form-js-editor/src/features/properties-panel/PropertiesPanelHeaderProvider.js +++ b/packages/form-js-editor/src/features/properties-panel/PropertiesPanelHeaderProvider.js @@ -10,6 +10,7 @@ const labelsByType = { checklist: 'CHECKLIST', columns: 'COLUMNS', default: 'FORM', + datetime: 'DATETIME', number: 'NUMBER', radio: 'RADIO', select: 'SELECT', 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 b18d82e42..30050f8f8 100644 --- a/packages/form-js-editor/src/features/properties-panel/Util.js +++ b/packages/form-js-editor/src/features/properties-panel/Util.js @@ -37,6 +37,7 @@ export function textToLabel(text = '...') { export const INPUTS = [ 'checkbox', 'checklist', + 'datetime', 'number', 'radio', 'select', 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 new file mode 100644 index 000000000..673897de6 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeConstraintsEntry.js @@ -0,0 +1,104 @@ +import { CheckboxEntry, isCheckboxEntryEdited, SelectEntry, isSelectEntryEdited } from '@bpmn-io/properties-panel'; + +import { DATETIME_SUBTYPES, DATETIME_DISALLOWPASTDATES_PATH, TIME_INTERVAL_PATH } from '@bpmn-io/form-js-viewer'; + +import { get } from 'min-dash'; + +export default function DateTimeConstraintsEntry(props) { + const { + editField, + field, + id + } = props; + + const { + type, + subtype + } = field; + + if (type !== 'datetime') { + return []; + } + + const entries = []; + + if (subtype === DATETIME_SUBTYPES.TIME || subtype === DATETIME_SUBTYPES.DATETIME) { + entries.push({ + id: id + '-timeInterval', + component: TimeIntervalSelect, + isEdited: isSelectEntryEdited, + editField, + field + }); + } + + if (subtype === DATETIME_SUBTYPES.DATE || subtype === DATETIME_SUBTYPES.DATETIME) { + entries.push({ + id: id + '-disallowPassedDates', + component: DisallowPassedDates, + isEdited: isCheckboxEntryEdited, + editField, + field + }); + } + + return entries; +} + +function DisallowPassedDates(props) { + const { + editField, + field, + id + } = props; + + const path = DATETIME_DISALLOWPASTDATES_PATH; + + const getValue = () => { + return get(field, path, ''); + }; + + const setValue = (value) => { + return editField(field, path, value); + }; + + return CheckboxEntry({ + element: field, + getValue, + id, + label: 'Disallow past dates', + setValue + }); +} + +function TimeIntervalSelect(props) { + + const { + editField, + field, + id + } = props; + + const timeIntervals = [ 1, 5, 10, 15, 30, 60 ]; + + const getValue = (e) => get(field, TIME_INTERVAL_PATH); + + const setValue = (value) => editField(field, TIME_INTERVAL_PATH, parseInt(value)); + + const getTimeIntervals = () => { + + return timeIntervals.map((timeInterval) => ({ + label: timeInterval === 60 ? '1h' : (timeInterval + 'm'), + value: timeInterval + })); + }; + + return SelectEntry({ + label: 'Time interval', + element: field, + getOptions: getTimeIntervals, + getValue, + id, + setValue + }); +} \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DateTimeDisplayEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeDisplayEntry.js new file mode 100644 index 000000000..d711a6e87 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeDisplayEntry.js @@ -0,0 +1,64 @@ +import { CheckboxEntry, isCheckboxEntryEdited } from '@bpmn-io/properties-panel'; + +import { DATETIME_SUBTYPES, TIME_USE24H_PATH } from '@bpmn-io/form-js-viewer'; + +import { get } from 'min-dash'; + +export default function DateTimeDisplayEntry(props) { + const { + editField, + field, + id + } = props; + + const { + type, + subtype + } = field; + + if (type !== 'datetime') { + return []; + } + + const entries = []; + + if (subtype === DATETIME_SUBTYPES.TIME || subtype === DATETIME_SUBTYPES.DATETIME) { + + entries.push({ + id: id + '-use24h', + component: Use24h, + isEdited: isCheckboxEntryEdited, + editField, + field + }); + + } + + return entries; +} + +function Use24h(props) { + const { + editField, + field, + id + } = props; + + const path = TIME_USE24H_PATH; + + const getValue = () => { + return get(field, path, ''); + }; + + const setValue = (value) => { + return editField(field, path, value); + }; + + return CheckboxEntry({ + element: field, + getValue, + id, + label: 'Use 24h', + setValue + }); +} 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 new file mode 100644 index 000000000..cbba63f8f --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeEntry.js @@ -0,0 +1,64 @@ +import { SelectEntry, isSelectEntryEdited } from '@bpmn-io/properties-panel'; + +import { DATETIME_SUBTYPES, DATETIME_SUBTYPES_LABELS, DATETIME_SUBTYPE_PATH } from '@bpmn-io/form-js-viewer'; + +import { get } from 'min-dash'; + +export default function DateTimeEntry(props) { + const { + editField, + field, + id + } = props; + + const { + type + } = field; + + if (type !== 'datetime') { + return []; + } + + const entries = [ + { + id: id + '-subtype-select', + component: DateTimeSubtypeSelect, + isEdited: isSelectEntryEdited, + editField, + field + } + ]; + + return entries; +} + +function DateTimeSubtypeSelect(props) { + + const { + editField, + field, + id + } = props; + + const getValue = (e) => get(field, DATETIME_SUBTYPE_PATH); + + const setValue = (value) => editField(field, DATETIME_SUBTYPE_PATH, value); + + const getDatetimeSubtypes = () => { + + return Object.values(DATETIME_SUBTYPES).map((subtype) => ({ + label: DATETIME_SUBTYPES_LABELS[subtype], + value: subtype + })); + }; + + return SelectEntry({ + label: 'Subtype', + element: field, + getOptions: getDatetimeSubtypes, + getValue, + id, + setValue + }); +} + diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DateTimeFormatEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeFormatEntry.js new file mode 100644 index 000000000..d0b481c20 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/entries/DateTimeFormatEntry.js @@ -0,0 +1,66 @@ +import { SelectEntry, isSelectEntryEdited } from '@bpmn-io/properties-panel'; + +import { DATETIME_SUBTYPES, TIME_SERIALISING_FORMATS, TIME_SERIALISINGFORMAT_LABELS, TIME_SERIALISINGFORMAT_PATH } from '@bpmn-io/form-js-viewer'; + +import { get } from 'min-dash'; + +export default function DateTimeFormatEntry(props) { + const { + editField, + field, + id + } = props; + + const { + type, + subtype + } = field; + + if (type !== 'datetime') { + return []; + } + + const entries = []; + + if (subtype === DATETIME_SUBTYPES.TIME || subtype === DATETIME_SUBTYPES.DATETIME) { + entries.push({ + id: id + '-time-format', + component: TimeFormatSelect, + isEdited: isSelectEntryEdited, + editField, + field + }); + } + + return entries; +} + +function TimeFormatSelect(props) { + + const { + editField, + field, + id + } = props; + + const getValue = (e) => get(field, TIME_SERIALISINGFORMAT_PATH); + + const setValue = (value) => editField(field, TIME_SERIALISINGFORMAT_PATH, value); + + const getTimeSerialisingFormats = () => { + + return Object.values(TIME_SERIALISING_FORMATS).map((format) => ({ + label: TIME_SERIALISINGFORMAT_LABELS[format], + value: format + })); + }; + + return SelectEntry({ + label: 'Time format', + element: field, + getOptions: getTimeSerialisingFormats, + getValue, + id, + setValue + }); +} \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/entries/InputKeyValuesSourceEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/InputKeyValuesSourceEntry.js index 5ef422d0e..af5ef7120 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/InputKeyValuesSourceEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/InputKeyValuesSourceEntry.js @@ -1,7 +1,8 @@ import { TextFieldEntry, isTextFieldEntryEdited } from '@bpmn-io/properties-panel'; import { get } from 'min-dash'; import { useService } from '../hooks'; -import { VALUES_SOURCES, VALUES_SOURCES_PATHS } from './ValuesSourceUtil'; +import { VALUES_SOURCES, VALUES_SOURCES_PATHS } from '@bpmn-io/form-js-viewer'; + export default function InputKeyValuesSourceEntry(props) { const { diff --git a/packages/form-js-editor/src/features/properties-panel/entries/StaticValuesSourceEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/StaticValuesSourceEntry.js index 024af2037..2cedd091f 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/StaticValuesSourceEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/StaticValuesSourceEntry.js @@ -1,7 +1,7 @@ import { isUndefined, without } from 'min-dash'; import { arrayAdd } from '../Util'; import ValueEntry from './ValueEntry'; -import { VALUES_SOURCES, VALUES_SOURCES_PATHS } from './ValuesSourceUtil'; +import { VALUES_SOURCES, VALUES_SOURCES_PATHS } from '@bpmn-io/form-js-viewer'; export default function StaticValuesSourceEntry(props) { const { 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 cd912b93f..49186b463 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 @@ -15,19 +15,19 @@ export default function TextEntry(props) { type } = field; - const entries = []; + if (type !== 'text') { + return []; + } - if (type === 'text') { - entries.push({ + return [ + { id: 'text', component: Text, editField: editField, field: field, isEdited: isTextAreaEntryEdited - }); - } - - return entries; + } + ]; } function Text(props) { diff --git a/packages/form-js-editor/src/features/properties-panel/entries/ValuesSourceSelectEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/ValuesSourceSelectEntry.js index ad6bbb37e..20ee06952 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/ValuesSourceSelectEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/ValuesSourceSelectEntry.js @@ -1,5 +1,5 @@ import { SelectEntry, isSelectEntryEdited } from '@bpmn-io/properties-panel'; -import { getValuesSource, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_LABELS, VALUES_SOURCES_PATHS } from './ValuesSourceUtil'; +import { getValuesSource, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_LABELS, VALUES_SOURCES_PATHS } from '@bpmn-io/form-js-viewer'; export default function ValuesSourceSelectEntry(props) { const { diff --git a/packages/form-js-editor/src/features/properties-panel/entries/index.js b/packages/form-js-editor/src/features/properties-panel/entries/index.js index 6a422ce49..b2a2ca14e 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/index.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/index.js @@ -7,6 +7,10 @@ export { default as IdEntry } from './IdEntry'; export { default as KeyEntry } from './KeyEntry'; export { default as LabelEntry } from './LabelEntry'; export { default as TextEntry } from './TextEntry'; +export { default as DateTimeEntry } from './DateTimeEntry'; +export { default as DateTimeDisplayEntry } from './DateTimeDisplayEntry'; +export { default as DateTimeConstraintsEntry } from './DateTimeConstraintsEntry'; +export { default as DateTimeFormatEntry } from './DateTimeFormatEntry'; export { default as ValueEntry } from './ValueEntry'; export { default as CustomValueEntry } from './CustomValueEntry'; export { default as ValuesSourceSelectEntry } from './ValuesSourceSelectEntry'; diff --git a/packages/form-js-editor/src/features/properties-panel/groups/ConstraintsGroup.js b/packages/form-js-editor/src/features/properties-panel/groups/ConstraintsGroup.js new file mode 100644 index 000000000..1b8d147ed --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/groups/ConstraintsGroup.js @@ -0,0 +1,21 @@ +import { + DateTimeConstraintsEntry +} from '../entries'; + + +export default function DisplayGroup(field, editField) { + + const entries = [ + ...DateTimeConstraintsEntry({ field, editField }) + ]; + + if (!entries.length) { + return null; + } + + return { + id: 'constraints', + label: 'Constraints', + entries + }; +} \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/groups/DisplayGroup.js b/packages/form-js-editor/src/features/properties-panel/groups/DisplayGroup.js new file mode 100644 index 000000000..ebb20e8e7 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/groups/DisplayGroup.js @@ -0,0 +1,21 @@ +import { + DateTimeDisplayEntry +} from '../entries'; + + +export default function DisplayGroup(field, editField) { + + const entries = [ + ...DateTimeDisplayEntry({ field, editField }) + ]; + + if (!entries.length) { + return null; + } + + return { + id: 'display', + label: 'Display', + entries + }; +} \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/groups/FormatGroup.js b/packages/form-js-editor/src/features/properties-panel/groups/FormatGroup.js new file mode 100644 index 000000000..ffe54aab9 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/groups/FormatGroup.js @@ -0,0 +1,21 @@ +import { + DateTimeFormatEntry +} from '../entries'; + + +export default function FormatGroup(field, editField) { + + const entries = [ + ...DateTimeFormatEntry({ field, editField }) + ]; + + if (!entries.length) { + return null; + } + + return { + id: 'format', + label: 'Format', + entries + }; +} \ No newline at end of file diff --git a/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js b/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js index ddd2a9736..201338482 100644 --- a/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js +++ b/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js @@ -7,7 +7,8 @@ import { IdEntry, KeyEntry, LabelEntry, - TextEntry + TextEntry, + DateTimeEntry } from '../entries'; @@ -21,6 +22,7 @@ export default function GeneralGroup(field, editField) { ...DefaultValueEntry({ field, editField }), ...ActionEntry({ field, editField }), ...ColumnsEntry({ field, editField }), + ...DateTimeEntry({ field, editField }), ...TextEntry({ field, editField }), ...DisabledEntry({ field, editField }) ]; 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 1d5ec61f1..09f09b3d3 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 @@ -99,6 +99,12 @@ export default function ValidationGroup(field, editField) { ); } + // if (type === 'datetime') { + // if () + + + // } + return { id: 'validation', label: 'Validation', diff --git a/packages/form-js-editor/src/features/properties-panel/groups/ValuesGroups.js b/packages/form-js-editor/src/features/properties-panel/groups/ValuesGroups.js index 87fb23c3c..ece162fb3 100644 --- a/packages/form-js-editor/src/features/properties-panel/groups/ValuesGroups.js +++ b/packages/form-js-editor/src/features/properties-panel/groups/ValuesGroups.js @@ -1,5 +1,5 @@ import { ValuesSourceSelectEntry, StaticValuesSourceEntry, InputKeyValuesSourceEntry } from '../entries'; -import { getValuesSource, VALUES_SOURCES } from '../entries/ValuesSourceUtil'; +import { getValuesSource, VALUES_SOURCES } from '@bpmn-io/form-js-viewer'; import { Group, ListGroup } from '@bpmn-io/properties-panel'; diff --git a/packages/form-js-editor/src/features/properties-panel/groups/index.js b/packages/form-js-editor/src/features/properties-panel/groups/index.js index 622c7f6b4..1c9f9e66d 100644 --- a/packages/form-js-editor/src/features/properties-panel/groups/index.js +++ b/packages/form-js-editor/src/features/properties-panel/groups/index.js @@ -1,4 +1,7 @@ export { default as GeneralGroup } from './GeneralGroup'; +export { default as DisplayGroup } from './DisplayGroup'; +export { default as FormatGroup } from './FormatGroup'; +export { default as ConstraintsGroup } from './ConstraintsGroup'; export { default as ValidationGroup } from './ValidationGroup'; export { default as ValuesGroups } from './ValuesGroups'; export { default as CustomValuesGroup } from './CustomValuesGroup'; \ No newline at end of file diff --git a/packages/form-js-editor/src/render/components/icons/Datetime.svg b/packages/form-js-editor/src/render/components/icons/Datetime.svg new file mode 100644 index 000000000..5f59ead5f --- /dev/null +++ b/packages/form-js-editor/src/render/components/icons/Datetime.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/form-js-editor/src/render/components/icons/index.js b/packages/form-js-editor/src/render/components/icons/index.js index ac531da89..8c9abbdaf 100644 --- a/packages/form-js-editor/src/render/components/icons/index.js +++ b/packages/form-js-editor/src/render/components/icons/index.js @@ -1,5 +1,6 @@ import ButtonIcon from './Button.svg'; import CheckboxIcon from './Checkbox.svg'; +import DatetimeIcon from './Datetime.svg'; import ChecklistIcon from './Checklist.svg'; import TaglistIcon from './Taglist.svg'; import FormIcon from './Form.svg'; @@ -15,6 +16,7 @@ export const iconsByType = { checkbox: CheckboxIcon, checklist: ChecklistIcon, columns: ColumnsIcon, + datetime: DatetimeIcon, number: NumberIcon, radio: RadioIcon, select: SelectIcon, diff --git a/packages/form-js-editor/src/render/hooks/useEffectDebugger.js b/packages/form-js-editor/src/render/hooks/useEffectDebugger.js new file mode 100644 index 000000000..821261e74 --- /dev/null +++ b/packages/form-js-editor/src/render/hooks/useEffectDebugger.js @@ -0,0 +1,59 @@ +const { useEffect, useRef, useCallback } = require('preact/hooks'); + +const usePrevious = (value, initialValue) => { + const ref = useRef(initialValue); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; + +export function useEffectDebugger(effectHook, dependencies, dependencyNames = [], effectName = 'noname') { + const previousDeps = usePrevious(dependencies, []); + + const changedDeps = dependencies.reduce((accum, dependency, index) => { + if (dependency !== previousDeps[index]) { + const keyName = dependencyNames[index] || index; + return { + ...accum, + [keyName]: { + before: previousDeps[index], + after: dependency + } + }; + } + + return accum; + }, {}); + + if (Object.keys(changedDeps).length) { + console.log('[use-effect-debugger] (' + effectName + ') ', changedDeps); + } + + useEffect(effectHook, dependencies); +} + +export function useCallbackDebugger(callback, dependencies, dependencyNames = [], callbackName = 'noname') { + const previousDeps = usePrevious(dependencies, []); + + const changedDeps = dependencies.reduce((accum, dependency, index) => { + if (dependency !== previousDeps[index]) { + const keyName = dependencyNames[index] || index; + return { + ...accum, + [keyName]: { + before: previousDeps[index], + after: dependency + } + }; + } + + return accum; + }, {}); + + if (Object.keys(changedDeps).length) { + console.log('[use-callback-debugger] (' + callbackName + ') ', changedDeps); + } + + return useCallback(callback, dependencies); +} \ No newline at end of file diff --git a/packages/form-js-editor/test/TestHelper.js b/packages/form-js-editor/test/TestHelper.js index df0d87274..d56919af3 100644 --- a/packages/form-js-editor/test/TestHelper.js +++ b/packages/form-js-editor/test/TestHelper.js @@ -27,6 +27,7 @@ export function insertStyles() { insertCSS('form-js.css', formCSS); insertCSS('form-js-editor.css', formEditorCSS); insertCSS('dragula.css', dragulaCSS); + insertCSS('light.css', lightCSS); insertCSS('properties-panel.css', propertiesPanelCSS); insertCSS('test.css', testCSS); } diff --git a/packages/form-js-playground/package.json b/packages/form-js-playground/package.json index be638abe1..28fd48873 100644 --- a/packages/form-js-playground/package.json +++ b/packages/form-js-playground/package.json @@ -23,7 +23,7 @@ "scripts": { "all": "run-s lint test build", "build": "run-p bundle generate-types", - "bundle": "rollup -c --failAfterWarnings", + "bundle": "rollup -c", "bundle:watch": "rollup -c -w", "start": "SINGLE_START=basic npm run dev", "dev": "npm test -- --auto-watch --no-single-run", diff --git a/packages/form-js-playground/test/TestHelper.js b/packages/form-js-playground/test/TestHelper.js index 7c3c7b78d..386fad3c9 100644 --- a/packages/form-js-playground/test/TestHelper.js +++ b/packages/form-js-playground/test/TestHelper.js @@ -1,6 +1,7 @@ import './test.css'; import '@bpmn-io/form-js-viewer/dist/assets/form-js.css'; +import '@bpmn-io/form-js-viewer/dist/assets/light.css'; import '@bpmn-io/form-js-editor/dist/assets/form-js-editor.css'; import '@bpmn-io/form-js-editor/dist/assets/properties-panel.css'; import '@bpmn-io/form-js-editor/dist/assets/dragula.css'; diff --git a/packages/form-js-viewer/assets/form-js.css b/packages/form-js-viewer/assets/form-js.css index e728cff78..ffa5e5e3a 100644 --- a/packages/form-js-viewer/assets/form-js.css +++ b/packages/form-js-viewer/assets/form-js.css @@ -9,6 +9,7 @@ --color-grey-225-10-80: hsl(225, 10%, 80%); --color-grey-225-10-85: hsl(225, 10%, 85%); --color-grey-225-10-90: hsl(225, 10%, 90%); + --color-grey-225-10-93: hsl(225, 10%, 93%); --color-grey-225-10-95: hsl(225, 10%, 95%); --color-grey-225-10-97: hsl(225, 10%, 97%); @@ -29,18 +30,24 @@ --color-background: var(--color-white); --color-background-disabled: var(--color-grey-225-10-95); + --color-background-adornment: var(--color-grey-225-10-93); --color-text: var(--color-grey-225-10-15); --color-text-light: var(--color-grey-225-10-35); --color-text-lighter: var(--color-grey-225-10-55); --color-text-inverted: var(--color-white); --color-borders: var(--color-grey-225-10-55); --color-borders-disabled: var(--color-grey-225-10-75); + --color-borders-adornment: var(--color-grey-225-10-85); --color-warning: var(--color-red-360-100-45); --color-accent: var(--color-blue-205-100-45); --color-accent-dark: var(--color-blue-205-100-45); --font-family: 'IBM Plex Sans', sans-serif; + --border-definition: 1px solid var(--color-borders); + --outline-definition: 1px solid var(--color-borders); + --border-definition-disabled: 1px solid var(--color-borders-disabled); + height: 100%; } @@ -57,6 +64,11 @@ flex-direction: row; } +.fjs-container .fjs-vertical-group { + display: flex; + width: 100%; +} + .fjs-container .fjs-column { flex-grow: 1; } @@ -73,10 +85,8 @@ font-size: 14px; line-height: 1.3; font-weight: 400; - color: var(--color-text); background-color: var(--color-background); - position: relative; } @@ -92,21 +102,6 @@ color: var(--color-text-light); } -.fjs-container .fjs-input, -.fjs-container .fjs-textarea, -.fjs-container .fjs-select { - border-color: var(--color-borders); - background-color: var(--color-background); -} -.fjs-container .fjs-input::placeholder, -.fjs-container .fjs-textarea::placeholder, -.fjs-container .fjs-select > option:disabled, -.fjs-container .fjs-select [disabled] { - font-style: italic; - letter-spacing: 0.25px; - color: var(--color-text-lighter); -} - .fjs-container .fjs-form-field-label { display: flex; align-items: center; @@ -132,6 +127,39 @@ display: none; } +.fjs-container .fjs-input, +.fjs-container .fjs-textarea, +.fjs-container .fjs-select { + background-color: var(--color-background); +} + +.fjs-container .fjs-input-adornment { + border-style: solid; + border-color: var(--color-borders-adornment); + background-color: var(--color-background-adornment); + padding: 8px; + width: auto !important; +} + +.fjs-container .fjs-input-adornment svg { + display: block; + margin: auto; + width: 15px; +} + +.fjs-container .fjs-input-adornment span { + color: var(--color-background-adornment); +} + +.fjs-container .fjs-input::placeholder, +.fjs-container .fjs-textarea::placeholder, +.fjs-container .fjs-select > option:disabled, +.fjs-container .fjs-select [disabled] { + font-style: italic; + letter-spacing: 0.25px; + color: var(--color-text-lighter); +} + .fjs-container .fjs-input[type='text'], .fjs-container .fjs-input[type='number'], .fjs-container .fjs-button[type='submit'], @@ -142,11 +170,71 @@ width: 100%; padding: 8px; margin: 4px 0; - border-width: 1px; - border-style: solid; + border: var(--border-definition); + border-radius: 3px; +} + +.fjs-container .fjs-input-group { + display: flex; + width: 100%; + margin: 4px, 0; + border: var(--border-definition); + border-radius: 3px; +} + +.fjs-container .fjs-input-group.disabled, +.fjs-container .fjs-input-group.disabled .fjs-input, +.fjs-container .fjs-input-group.disabled .fjs-input-adornment { + border-color: var(--color-borders-disabled); +} + +.fjs-container .fjs-taglist .fjs-taglist-input, +.fjs-container .fjs-input-group .fjs-input { + border: none; + border-radius: 0; + margin: 0; + outline: 0; +} + +.fjs-container .fjs-input-group .fjs-input-adornment { + border-width: 0; +} + +.fjs-container .fjs-input-group .fjs-input-adornment.border-left { + border-left-width: 1px; +} + +.fjs-container .fjs-input-group .fjs-input-adornment.border-right { + border-right-width: 1px; +} + +.fjs-container .fjs-input-group .fjs-input-adornment.border-radius-right { + border-radius: 0 3px 3px 0; +} + +.fjs-container .fjs-input-group .fjs-input-adornment.border-radius-left { + border-radius: 3px 0 0 3px +} + +.fjs-container .fjs-input-group .fjs-input, +.fjs-container .fjs-input-group.disabled .fjs-input { border-radius: 3px; } +.fjs-container .fjs-vertical-group { + display: flex; + width: 100%; +} + +.fjs-container .flatpickr-wrapper { + width: 100%; +} + +.fjs-container .flatpickr-calendar.hasTime.noCalendar { + width: auto !important; + max-width: 250px !important; +} + .fjs-container .fjs-textarea { height: 90px; } @@ -183,13 +271,20 @@ font-weight: 600; } +.fjs-container .fjs-taglist:focus-within, +.fjs-container .fjs-input-group:focus-within, .fjs-container .fjs-input[type='text']:focus, .fjs-container .fjs-input[type='number']:focus, .fjs-container .fjs-button[type='submit']:focus, .fjs-container .fjs-button[type='reset']:focus, .fjs-container .fjs-textarea:focus, .fjs-container .fjs-select:focus { - outline: var(--color-borders) solid 1px; + outline: var(--outline-definition); +} + +.fjs-container .fjs-input-group .fjs-input, +.fjs-container .fjs-input-group .fjs-input:focus { + outline: none; } .fjs-container .fjs-button[type='submit']:focus { @@ -198,7 +293,8 @@ .fjs-container .fjs-input:disabled, .fjs-container .fjs-textarea:disabled, -.fjs-container .fjs-select:disabled { +.fjs-container .fjs-select:disabled, +.fjs-container .fjs-taglist.disabled { background-color: var(--color-background-disabled); border-color: var(--color-borders-disabled); } @@ -212,8 +308,16 @@ .fjs-container .fjs-form-field.fjs-has-errors .fjs-input, .fjs-container .fjs-form-field.fjs-has-errors .fjs-select, -.fjs-container .fjs-form-field.fjs-has-errors .fjs-textarea { +.fjs-container .fjs-form-field.fjs-has-errors .fjs-textarea, +.fjs-container .fjs-form-field.fjs-has-errors .fjs-input-group, +.fjs-container .fjs-form-field.fjs-has-errors .fjs-input-group .fjs-input { border-color: var(--color-warning); + outline-color: var(--color-warning); +} + +.fjs-container .fjs-form-field.fjs-has-errors .fjs-input-adornment { + border-color: var(--color-red-360-100-92); + background: var(--color-red-360-100-97); } .fjs-container .fjs-form-field-error { @@ -234,7 +338,8 @@ color: var(--color-blue-205-100-45); } -.fjs-container .fjs-taglist-anchor { +.fjs-container .fjs-taglist-anchor, +.fjs-container .fjs-timepicker-anchor { position: relative; } @@ -248,15 +353,6 @@ background-color: var(--color-background); } -.fjs-container .fjs-taglist:focus-within { - outline: var(--color-borders) solid 1px; -} - -.fjs-container .fjs-taglist.disabled { - border: var(--color-borders-disabled) solid 1px; - background-color: var(--color-background-disabled); -} - .fjs-container .fjs-taglist .fjs-taglist-tag { display: flex; overflow: hidden; @@ -286,7 +382,11 @@ background-color: var(--color-grey-225-10-75); } -.fjs-container .fjs-taglist .fjs-taglist-tag .fjs-taglist-tag-remove:hover > svg { +.fjs-container + .fjs-taglist + .fjs-taglist-tag + .fjs-taglist-tag-remove:hover + > svg { opacity: 1; } @@ -298,19 +398,10 @@ flex-grow: 1; } -.fjs-container .fjs-taglist .fjs-taglist-input:focus-visible { - outline: none; -} - -.fjs-container .fjs-taglist .fjs-taglist-dropdown-anchor { - position: relative; -} - .fjs-container .fjs-dropdownlist { position: absolute; user-select: none; overflow-y: auto; - scroll-behavior: smooth; width: 100%; border-radius: 3px; margin-top: 3px; @@ -337,3 +428,49 @@ padding: 6px 8px; color: var(--color-text-lighter); } + + + +/** +* Flatpickr style adjustments +*/ + +.flatpickr-day.today { + border-color: transparent !important; + background-color: transparent !important; + font-weight: bold !important; +} + +.flatpickr-day.today:hover, +.flatpickr-day.today:focus { + border-color: var(--color-grey-225-10-55) !important; + background-color: var(--color-grey-225-10-55) !important; + color: var(--color-text-inverted) !important; +} + +.flatpickr-day.selected { + border-color: var(--color-accent) !important; + background-color: inherit !important; + color: inherit !important; + font-weight: normal !important; +} + +.flatpickr-day.selected.today { + font-weight: bold !important; +} + +.flatpickr-day.selected:hover, +.flatpickr-day.selected:focus { + background-color: var(--color-accent) !important; + font-weight: bold !important; + color: var(--color-text-inverted) !important; +} + +.flatpickr-days, .flatpickr-weekdays { + padding: 10px !important; + width: 100% !important; +} + +.flatpickr-calendar { + width: 326px !important; +} \ No newline at end of file diff --git a/packages/form-js-viewer/package.json b/packages/form-js-viewer/package.json index b6f73fb47..755e2adbb 100644 --- a/packages/form-js-viewer/package.json +++ b/packages/form-js-viewer/package.json @@ -8,6 +8,7 @@ "require": "./dist/index.cjs" }, "./dist/assets/form-js.css": "./dist/assets/form-js.css", + "./dist/assets/light.css": "./dist/assets/light.css", "./package.json": "./package.json" }, "publishConfig": { @@ -20,7 +21,7 @@ "all": "run-s test build", "build": "run-p bundle generate-types", "start": "SINGLE_START=basic npm run dev", - "bundle": "rollup -c --failAfterWarnings", + "bundle": "rollup -c", "bundle:watch": "rollup -c -w", "dev": "npm test -- --auto-watch --no-single-run", "generate-types": "tsc --allowJs --skipLibCheck --declaration --emitDeclarationOnly --outDir dist/types src/index.js && cp src/*.d.ts dist/types", @@ -41,6 +42,7 @@ "@bpmn-io/snarkdown": "^2.1.0", "classnames": "^2.3.1", "didi": "^8.0.1", + "flatpickr": "^4.6.13", "ids": "^1.0.0", "min-dash": "^3.8.1", "preact": "^10.5.14", diff --git a/packages/form-js-viewer/rollup.config.js b/packages/form-js-viewer/rollup.config.js index 3a9b33fba..ed18abecc 100644 --- a/packages/form-js-viewer/rollup.config.js +++ b/packages/form-js-viewer/rollup.config.js @@ -55,12 +55,14 @@ export default [ 'preact/hooks', 'preact/compat', 'preact-markup', + 'flatpickr', '@bpmn-io/snarkdown' ], plugins: pgl([ copy({ targets: [ - { src: 'assets/form-js.css', dest: 'dist/assets' } + { src: 'assets/form-js.css', dest: 'dist/assets' }, + { src: '../../node_modules/flatpickr/dist/themes/light.css', dest: 'dist/assets' } ] }) ]) diff --git a/packages/form-js-viewer/src/render/components/Util.js b/packages/form-js-viewer/src/render/components/Util.js index c8c69f597..fe46bb0a0 100644 --- a/packages/form-js-viewer/src/render/components/Util.js +++ b/packages/form-js-viewer/src/render/components/Util.js @@ -1,5 +1,4 @@ import snarkdown from '@bpmn-io/snarkdown'; -import { get } from 'min-dash'; import { sanitizeHTML } from './Sanitizer'; @@ -46,49 +45,3 @@ export function safeMarkdown(markdown) { return sanitizeHTML(html); } - -export function sanitizeSingleSelectValue(options) { - const { - formField, - data, - value - } = options; - - const { - valuesKey, - values - } = formField; - - try { - const validValues = (valuesKey ? get(data, [ valuesKey ]) : values).map(v => v.value) || []; - return validValues.includes(value) ? value : null; - } catch (error) { - - // use default value in case of formatting error - // TODO(@Skaiir): log a warning when this happens - https://github.com/bpmn-io/form-js/issues/289 - return null; - } -} - -export function sanitizeMultiSelectValue(options) { - const { - formField, - data, - value - } = options; - - const { - valuesKey, - values - } = formField; - - try { - const validValues = (valuesKey ? get(data, [ valuesKey ]) : values).map(v => v.value) || []; - return value.filter(v => validValues.includes(v)); - } catch (error) { - - // use default value in case of formatting error - // TODO(@Skaiir): log a warning when this happens - https://github.com/bpmn-io/form-js/issues/289 - return []; - } -} diff --git a/packages/form-js-viewer/src/render/components/form-fields/Checklist.js b/packages/form-js-viewer/src/render/components/form-fields/Checklist.js index d6b610af8..fe448c71f 100644 --- a/packages/form-js-viewer/src/render/components/form-fields/Checklist.js +++ b/packages/form-js-viewer/src/render/components/form-fields/Checklist.js @@ -7,10 +7,10 @@ import Description from '../Description'; import Errors from '../Errors'; import Label from '../Label'; +import { sanitizeMultiSelectValue } from '../util/sanitizerUtil'; import { formFieldClasses, - prefixId, - sanitizeMultiSelectValue + prefixId } from '../Util'; const type = 'checklist'; diff --git a/packages/form-js-viewer/src/render/components/form-fields/Datetime.js b/packages/form-js-viewer/src/render/components/form-fields/Datetime.js new file mode 100644 index 000000000..9e442d783 --- /dev/null +++ b/packages/form-js-viewer/src/render/components/form-fields/Datetime.js @@ -0,0 +1,170 @@ +import { useCallback, useContext, useMemo, useState } from 'preact/hooks'; + +import classNames from 'classnames'; + +import { set } from 'min-dash'; + +import { FormContext } from '../../context'; + +import { DATETIME_SUBTYPES, DATETIME_SUBTYPE_PATH, TIME_SERIALISING_FORMATS, TIME_SERIALISINGFORMAT_PATH, TIME_INTERVAL_PATH } from '../../../util/constants/DatetimeConstants'; + +import Description from '../Description'; +import Errors from '../Errors'; +import Label from '../Label'; +import Datepicker from './parts/Datepicker'; +import Timepicker from './parts/Timepicker'; + +import { formFieldClasses, prefixId } from '../Util'; +import { sanitizeDateTimePickerValue } from '../util/sanitizerUtil'; +import { isDateTimeInputInformationSufficient, parseIsoTime, serializeDate, serializeDateTime, serializeTime } from '../util/dateTimeUtil'; + +const type = 'datetime'; + +export default function Datetime(props) { + const { + disabled, + errors = [], + field, + onChange, + value = '' + } = props; + + const { + description, + id, + label, + validate = {}, + subtype, + use24h, + disallowPassedDates, + timeInterval, + timeSerializingFormat + } = field; + + const { required } = validate; + const { formId } = useContext(FormContext); + + const getNullDate = () => new Date(Date.parse(null)); + + const [ dateCache, setDateCache ] = useState(getNullDate()); + const [ timeCache, setTimeCache ] = useState(null); + + const isValidDate = (date) => date && !isNaN(date.getTime()); + const isValidTime = (time) => !isNaN(parseInt(time)); + + const useDatePicker = useMemo(() => subtype === DATETIME_SUBTYPES.DATE || subtype === DATETIME_SUBTYPES.DATETIME, [ subtype ]); + const useTimePicker = useMemo(() => subtype === DATETIME_SUBTYPES.TIME || subtype === DATETIME_SUBTYPES.DATETIME, [ subtype ]); + + const [ date, time ] = useMemo(() => { + switch (subtype) { + case DATETIME_SUBTYPES.DATE: + return [ new Date(Date.parse(value)), null ]; + case DATETIME_SUBTYPES.TIME: + return [ null, parseIsoTime(value) ]; + case DATETIME_SUBTYPES.DATETIME: { + + // ensure enough information is provided in the input, as Date.parse() is way too forgiving + const isInputSufficient = isDateTimeInputInformationSufficient(value); + + let date, time; + + if (isInputSufficient) { + date = new Date(Date.parse(value)); + time = isValidDate(date) ? 60 * date.getHours() + date.getMinutes() : null; + } + else { + date = getNullDate(); + time = null; + } + + setDateCache(date); + setTimeCache(time); + + return [ date, time ]; + }} + }, [ subtype, value ]); + + const evaluate = useCallback(({ time, date }) => { + + let newDateTimeValue = null; + + if (subtype === DATETIME_SUBTYPES.DATE && isValidDate(date)) { + newDateTimeValue = serializeDate(date); + } + else if (subtype === DATETIME_SUBTYPES.TIME && isValidTime(time)) { + newDateTimeValue = serializeTime(time, new Date().getTimezoneOffset(), timeSerializingFormat); + } + else if (subtype === DATETIME_SUBTYPES.DATETIME && isValidDate(date) && isValidTime(time)) { + newDateTimeValue = serializeDateTime(date, time, timeSerializingFormat); + } + + onChange({ value: newDateTimeValue, field }); + + }, [ field, onChange, subtype, timeSerializingFormat ]); + + const setDate = useCallback((date) => { + evaluate({ date, time: timeCache }); + setDateCache(date); + }, [ evaluate, timeCache ]); + + const setTime = useCallback((time) => { + evaluate({ time, date: dateCache }); + setTimeCache(time); + }, [ evaluate, dateCache ]); + + const allErrors = useMemo(() => { + + // If only one of the two fields is set + const shouldCompleteSecondField = !required && (subtype === DATETIME_SUBTYPES.DATETIME) && ((isValidDate(dateCache) && timeCache === null) || (!isValidDate(dateCache) && timeCache !== null)); + return shouldCompleteSecondField ? [ 'Date and time must both be entered.', ...errors ] : errors; + + }, [ required, subtype, dateCache, timeCache, errors ]); + + const datePickerProps = { + id, + formId, + disabled, + disallowPassedDates, + date, + setDate + }; + + const timePickerProps = { + id, + formId, + disabled, + use24h, + timeInterval, + time, + setTime + }; + + return
+