@xeplr/ui-factory 1.8.3 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +21 -1
- package/README.md +169 -6
- package/bin/xeplr-factory.js +27 -1
- package/package.json +3 -1
- package/src/buttons.js +142 -0
- package/src/controls.js +48 -1
- package/src/designs/BuilderSample.jsx +1 -0
- package/src/designs/ButtonsView.jsx +107 -0
- package/src/designs/ControlView.jsx +4 -1
- package/src/designs/FedList.jsx +248 -0
- package/src/designs/ListView.jsx +6 -1
- package/src/designs/PropertyPanel.jsx +336 -4
- package/src/designs/ScreenSample.jsx +12 -1
- package/src/designs/factory.css +139 -0
- package/src/events.js +21 -0
- package/src/model.js +8 -1
- package/src/pagesInventory.js +91 -0
- package/src/remote.js +17 -2
- package/src/urlState.js +2 -2
- package/src/useFactoryBuilder.js +9 -1
- package/src/useFactoryScreen.js +103 -8
- package/src/useUrlPlaces.js +13 -0
- package/src/validateDocument.js +147 -3
|
@@ -4,6 +4,7 @@ import { FIELD_PRESETS, presetFor } from '../presets.js'
|
|
|
4
4
|
import { slugify, inputNodes, paramsText, screenParams } from '../document.js'
|
|
5
5
|
import { isBinding, previousBinding } from '../values.js'
|
|
6
6
|
import { FONT_FAMILIES, STYLE_KEYS, SCREEN_STYLE_KEYS, LIST_ACTIONS, FILE_KINDS } from '../controls.js'
|
|
7
|
+
import { BUTTON_VARIANTS, COLUMN_DISPLAYS, BADGE_TONES, CONDITION_OPERATORS, NOTE_INPUT, optionValues } from '../buttons.js'
|
|
7
8
|
|
|
8
9
|
// The selected control's properties, GENERATED from its entry in controls.js:
|
|
9
10
|
// each field there names a path and an editor type, and this file owns only
|
|
@@ -59,7 +60,7 @@ function FieldType({ node, controls, hint, onConvert, onKeepPlain }) {
|
|
|
59
60
|
)
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
export default function PropertyPanel({ doc, node, control, controls, errors, tables, screens, forms, lockedNames, onChange, onConvert, hint, onScreenChange, onRemove, onMakeChildTable, selectionCount }) {
|
|
63
|
+
export default function PropertyPanel({ doc, node, control, controls, errors, tables, functions, screens, forms, lockedNames, onChange, onConvert, hint, onScreenChange, onRemove, onMakeChildTable, selectionCount }) {
|
|
63
64
|
if (selectionCount > 1) {
|
|
64
65
|
return (
|
|
65
66
|
<aside className="xeplr-factory-panel">
|
|
@@ -103,13 +104,14 @@ export default function PropertyPanel({ doc, node, control, controls, errors, ta
|
|
|
103
104
|
const value = field.path === 'props.default' && field.type !== 'fillFrom' && isBinding(raw) ? undefined : raw
|
|
104
105
|
return (
|
|
105
106
|
<div key={field.path + ':' + field.type} className={`xeplr-factory-prop${errs.length ? ' has-error' : ''}`}>
|
|
106
|
-
{
|
|
107
|
+
{!SELF_LABELLED.includes(field.type) && <label className="xeplr-factory-prop-label" htmlFor={id}>{field.label}</label>}
|
|
107
108
|
<Editor
|
|
108
109
|
id={id}
|
|
109
110
|
field={field}
|
|
110
111
|
value={value}
|
|
111
112
|
onChange={(v) => onChange(field.path, v)}
|
|
112
113
|
tables={tables}
|
|
114
|
+
functions={functions}
|
|
113
115
|
screens={screens}
|
|
114
116
|
forms={forms}
|
|
115
117
|
node={node}
|
|
@@ -494,7 +496,10 @@ function TableEditor({ id, value, onChange, tables }) {
|
|
|
494
496
|
* are this screen's own — or, for a list screen, those of the screen it edits
|
|
495
497
|
* in. Nothing ticked means all of them.
|
|
496
498
|
*/
|
|
497
|
-
function ColumnsEditor({ value, onChange, doc, node, screens }) {
|
|
499
|
+
function ColumnsEditor({ value, onChange, doc, node, screens, functions }) {
|
|
500
|
+
// A list fed by a function has no form describing its rows: its columns
|
|
501
|
+
// are written out, each saying what it shows and how.
|
|
502
|
+
if (node && node.props && node.props.rowsFrom) return <FedColumnsEditor value={value} onChange={onChange} functions={functions} />
|
|
498
503
|
const editDoc = node && node.props.editScreen ? (screens || []).find((s) => s.id === node.props.editScreen)?.document : null
|
|
499
504
|
|
|
500
505
|
// A LIST OF ANOTHER TABLE IS NOT DESCRIBED BY THIS SCREEN. Pointing a list
|
|
@@ -549,6 +554,9 @@ function ScreenEditor({ id, value, onChange, screens, doc }) {
|
|
|
549
554
|
return (
|
|
550
555
|
<select id={id} className="xeplr-factory-prop-input" value={value ?? ''} onChange={(e) => onChange(e.target.value || undefined)}>
|
|
551
556
|
<option value="">— open rows in this screen's fields</option>
|
|
557
|
+
{/* A screen not in the list (not loaded, another app's) is still the
|
|
558
|
+
setting — shown by its id, not as if it had been cleared. */}
|
|
559
|
+
{value && !others.some((s) => s.id === value) && <option value={value}>{value}</option>}
|
|
552
560
|
{others.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
|
|
553
561
|
</select>
|
|
554
562
|
)
|
|
@@ -778,6 +786,327 @@ function uniqueStepKey(steps, base) {
|
|
|
778
786
|
return `${base}_${n}`
|
|
779
787
|
}
|
|
780
788
|
|
|
789
|
+
/** Editors that draw their own label (a toggle's is beside it; a list of rows has a heading per row). */
|
|
790
|
+
const SELF_LABELLED = ['toggle', 'toggleValue', 'columns', 'actions', 'filters', 'buttons']
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* One of the app's registered functions, for one purpose (`field.event`):
|
|
794
|
+
* picked from those registered for it. A name that is no longer registered
|
|
795
|
+
* stays shown, marked — not silently blanked.
|
|
796
|
+
*/
|
|
797
|
+
function FunctionEditor({ id, field, value, onChange, functions }) {
|
|
798
|
+
const pick = functions && functions.forEvent ? functions.forEvent(field.event) : []
|
|
799
|
+
if (!functions || (!pick.length && !functions.loading)) {
|
|
800
|
+
return (
|
|
801
|
+
<>
|
|
802
|
+
<input id={id} className="xeplr-factory-prop-input" value={value || ''} placeholder="functionName" onChange={(e) => onChange(e.target.value.trim() || undefined)} />
|
|
803
|
+
{functions && !functions.loading && <div className="xeplr-factory-prop-help">No function is registered for “{field.event}” — the app registers one with @xeplr/actions.</div>}
|
|
804
|
+
</>
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
const known = !value || pick.some((f) => f.name === value)
|
|
808
|
+
const chosen = pick.find((f) => f.name === value)
|
|
809
|
+
return (
|
|
810
|
+
<>
|
|
811
|
+
<select id={id} className="xeplr-factory-prop-input" value={value || ''} onChange={(e) => onChange(e.target.value || undefined)}>
|
|
812
|
+
<option value="">— none —</option>
|
|
813
|
+
{!known && <option value={value}>{value} (not registered)</option>}
|
|
814
|
+
{pick.map((f) => <option key={f.name} value={f.name}>{f.name}</option>)}
|
|
815
|
+
</select>
|
|
816
|
+
{chosen && chosen.description && <div className="xeplr-factory-prop-help">{chosen.description}</div>}
|
|
817
|
+
</>
|
|
818
|
+
)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** A list's filter chips: key (what the function is asked for) and the chip's text. */
|
|
822
|
+
function FiltersEditor({ value, onChange }) {
|
|
823
|
+
const chips = Array.isArray(value) ? value : []
|
|
824
|
+
const set = (next) => onChange(next.length ? next : undefined)
|
|
825
|
+
const edit = (i, patch) => set(chips.map((c, j) => (j === i ? { ...c, ...patch } : c)))
|
|
826
|
+
return (
|
|
827
|
+
<div className="xeplr-factory-rows-editor">
|
|
828
|
+
<div className="xeplr-factory-prop-label">Filter chips</div>
|
|
829
|
+
{chips.map((c, i) => (
|
|
830
|
+
<div key={i} className="xeplr-factory-rows-editor-row">
|
|
831
|
+
<input className="xeplr-factory-prop-input" aria-label={`Chip ${i + 1} text`} placeholder="Packed" value={c.label || ''}
|
|
832
|
+
onChange={(e) => edit(i, { label: e.target.value, ...(c.key && c.key !== slugify(c.label || '', '-') ? {} : { key: slugify(e.target.value, '-') }) })} />
|
|
833
|
+
<input className="xeplr-factory-prop-input xeplr-factory-prop-input--key" aria-label={`Chip ${i + 1} key`} placeholder="packed" value={c.key || ''}
|
|
834
|
+
onChange={(e) => edit(i, { key: e.target.value.trim() })} />
|
|
835
|
+
<RowTools i={i} count={chips.length} onMove={(to) => set(move(chips, i, to))} onRemove={() => set(chips.filter((_, j) => j !== i))} />
|
|
836
|
+
</div>
|
|
837
|
+
))}
|
|
838
|
+
<button type="button" className="xeplr-factory-secondary" onClick={() => set([...chips, { key: chips.length ? `chip-${chips.length + 1}` : 'all', label: chips.length ? '' : 'All' }])}>+ Chip</button>
|
|
839
|
+
</div>
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** Each column of a list fed by a function: what it sorts by, what it shows, how. */
|
|
844
|
+
function FedColumnsEditor({ value, onChange, functions }) {
|
|
845
|
+
const cols = Array.isArray(value) ? value : []
|
|
846
|
+
const set = (next) => onChange(next.length ? next : undefined)
|
|
847
|
+
const edit = (i, patch) => set(cols.map((c, j) => {
|
|
848
|
+
if (j !== i) return c
|
|
849
|
+
const next = { ...c, ...patch }
|
|
850
|
+
Object.keys(next).forEach((k) => { if (next[k] === undefined || next[k] === '') delete next[k] })
|
|
851
|
+
return next
|
|
852
|
+
}))
|
|
853
|
+
const details = functions && functions.forEvent ? functions.forEvent('rowDetails') : []
|
|
854
|
+
return (
|
|
855
|
+
<div className="xeplr-factory-rows-editor">
|
|
856
|
+
<div className="xeplr-factory-prop-label">Columns</div>
|
|
857
|
+
{cols.map((c, i) => (
|
|
858
|
+
<fieldset key={i} className="xeplr-factory-rows-editor-card">
|
|
859
|
+
<legend>{c.label || `Column ${i + 1}`}</legend>
|
|
860
|
+
<RowTools i={i} count={cols.length} onMove={(to) => set(move(cols, i, to))} onRemove={() => set(cols.filter((_, j) => j !== i))} />
|
|
861
|
+
<MiniField label="Heading"><input className="xeplr-factory-prop-input" value={c.label || ''} onChange={(e) => edit(i, { label: e.target.value })} /></MiniField>
|
|
862
|
+
<MiniField label="Sorts by (field)"><input className="xeplr-factory-prop-input" value={c.field || ''} placeholder="placedAt" onChange={(e) => edit(i, { field: e.target.value.trim() })} /></MiniField>
|
|
863
|
+
<MiniField label="Shows (field)" help="Blank → the value itself"><input className="xeplr-factory-prop-input" value={c.text || ''} placeholder="placedText" onChange={(e) => edit(i, { text: e.target.value.trim() })} /></MiniField>
|
|
864
|
+
<MiniField label="Second line (field)"><input className="xeplr-factory-prop-input" value={c.sub || ''} onChange={(e) => edit(i, { sub: e.target.value.trim() })} /></MiniField>
|
|
865
|
+
<MiniField label="Drawn as">
|
|
866
|
+
<select className="xeplr-factory-prop-input" value={c.display || 'text'} onChange={(e) => edit(i, { display: e.target.value === 'text' ? undefined : e.target.value, ...(e.target.value !== 'images' ? { more: undefined } : {}), ...(e.target.value !== 'activity' ? { details: undefined } : {}) })}>
|
|
867
|
+
{COLUMN_DISPLAYS.map((d) => <option key={d} value={d}>{DISPLAY_LABELS[d]}</option>)}
|
|
868
|
+
</select>
|
|
869
|
+
</MiniField>
|
|
870
|
+
{c.display === 'images' && <MiniField label="How many more (field)"><input className="xeplr-factory-prop-input" value={c.more || ''} onChange={(e) => edit(i, { more: e.target.value.trim() })} /></MiniField>}
|
|
871
|
+
{c.display === 'activity' && (
|
|
872
|
+
<MiniField label="Hover box from">
|
|
873
|
+
<select className="xeplr-factory-prop-input" value={c.details || ''} onChange={(e) => edit(i, { details: e.target.value || undefined })}>
|
|
874
|
+
<option value="">— none —</option>
|
|
875
|
+
{c.details && !details.some((f) => f.name === c.details) && <option value={c.details}>{c.details} (not registered)</option>}
|
|
876
|
+
{details.map((f) => <option key={f.name} value={f.name}>{f.name}</option>)}
|
|
877
|
+
</select>
|
|
878
|
+
</MiniField>
|
|
879
|
+
)}
|
|
880
|
+
<MiniField label="Badges" help="field:Text:tone, … — shown where the field is true. Tones: accent, success, warning, danger, muted">
|
|
881
|
+
<input className="xeplr-factory-prop-input" value={badgesText(c.badges)} placeholder="isNew:New, cancelled:Cancelled:danger"
|
|
882
|
+
onChange={(e) => edit(i, { badges: parseBadges(e.target.value) })} />
|
|
883
|
+
</MiniField>
|
|
884
|
+
<label className="xeplr-factory-toggle">
|
|
885
|
+
<input type="checkbox" checked={c.sortable !== false} onChange={(e) => edit(i, { sortable: e.target.checked ? undefined : false })} />
|
|
886
|
+
<span>Sortable</span>
|
|
887
|
+
</label>
|
|
888
|
+
</fieldset>
|
|
889
|
+
))}
|
|
890
|
+
<button type="button" className="xeplr-factory-secondary" onClick={() => set([...cols, { field: `column${cols.length + 1}`, label: `Column ${cols.length + 1}` }])}>+ Column</button>
|
|
891
|
+
</div>
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const DISPLAY_LABELS = { text: 'Text', images: 'Pictures', activity: 'Latest activity (with hover box)' }
|
|
896
|
+
|
|
897
|
+
function badgesText(badges) {
|
|
898
|
+
return (Array.isArray(badges) ? badges : []).map((b) => [b.field, b.label, b.tone].filter(Boolean).join(':')).join(', ')
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function parseBadges(text) {
|
|
902
|
+
const list = String(text || '').split(',').map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
903
|
+
const [field, label, tone] = part.split(':').map((x) => x.trim())
|
|
904
|
+
const b = { field, label: label || field }
|
|
905
|
+
if (tone && BADGE_TONES.includes(tone)) b.tone = tone
|
|
906
|
+
return b
|
|
907
|
+
})
|
|
908
|
+
return list.length ? list : undefined
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* A PANEL'S BUTTONS. Each is a function of the app and the values it is given:
|
|
913
|
+
* picking the function lists the parameters it takes — a dropdown where it
|
|
914
|
+
* names the values allowed. Then what to ask first, and when it shows or is
|
|
915
|
+
* greyed out.
|
|
916
|
+
*/
|
|
917
|
+
function ButtonsEditor({ value, onChange, functions }) {
|
|
918
|
+
const buttons = Array.isArray(value) ? value : []
|
|
919
|
+
const set = (next) => onChange(next)
|
|
920
|
+
const edit = (i, patch) => set(buttons.map((b, j) => {
|
|
921
|
+
if (j !== i) return b
|
|
922
|
+
const next = { ...b, ...patch }
|
|
923
|
+
Object.keys(next).forEach((k) => { if (next[k] === undefined) delete next[k] })
|
|
924
|
+
return next
|
|
925
|
+
}))
|
|
926
|
+
const pick = functions && functions.forEvent ? functions.forEvent('button') : []
|
|
927
|
+
const takenIds = (except) => new Set(buttons.filter((_, j) => j !== except).map((b) => b.id))
|
|
928
|
+
const add = () => {
|
|
929
|
+
const ids = takenIds(-1)
|
|
930
|
+
let n = buttons.length + 1
|
|
931
|
+
while (ids.has(`button-${n}`)) n += 1
|
|
932
|
+
set([...buttons, { id: `button-${n}`, label: `Button ${n}`, action: pick[0] ? pick[0].name : '' }])
|
|
933
|
+
}
|
|
934
|
+
return (
|
|
935
|
+
<div className="xeplr-factory-rows-editor">
|
|
936
|
+
<div className="xeplr-factory-prop-label">Buttons</div>
|
|
937
|
+
{!pick.length && functions && !functions.loading && (
|
|
938
|
+
<div className="xeplr-factory-prop-help">No function is registered for buttons — the app registers one with @xeplr/actions and event “button”.</div>
|
|
939
|
+
)}
|
|
940
|
+
{buttons.map((b, i) => {
|
|
941
|
+
const fn = pick.find((f) => f.name === b.action) || null
|
|
942
|
+
const schema = (fn && fn.inputSchema) || []
|
|
943
|
+
const takesNote = schema.some((f) => f.name === NOTE_INPUT)
|
|
944
|
+
return (
|
|
945
|
+
<fieldset key={i} className="xeplr-factory-rows-editor-card">
|
|
946
|
+
<legend>{b.label || `Button ${i + 1}`}</legend>
|
|
947
|
+
<RowTools i={i} count={buttons.length} onMove={(to) => set(move(buttons, i, to))} onRemove={() => set(buttons.filter((_, j) => j !== i))} />
|
|
948
|
+
<MiniField label="Label">
|
|
949
|
+
<input className="xeplr-factory-prop-input" value={b.label || ''} onChange={(e) => {
|
|
950
|
+
const label = e.target.value
|
|
951
|
+
const auto = !b.id || /^button-\d+$/.test(b.id) || b.id === slugify(b.label || '', '-')
|
|
952
|
+
const id = auto ? uniqueId(slugify(label, '-') || `button-${i + 1}`, takenIds(i)) : b.id
|
|
953
|
+
edit(i, { label, id })
|
|
954
|
+
}} />
|
|
955
|
+
</MiniField>
|
|
956
|
+
<MiniField label="Runs">
|
|
957
|
+
<select className="xeplr-factory-prop-input" value={b.action || ''} onChange={(e) => edit(i, { action: e.target.value, params: undefined })}>
|
|
958
|
+
<option value="">— choose a function —</option>
|
|
959
|
+
{b.action && !fn && <option value={b.action}>{b.action} (not registered)</option>}
|
|
960
|
+
{pick.map((f) => <option key={f.name} value={f.name}>{f.name}</option>)}
|
|
961
|
+
</select>
|
|
962
|
+
</MiniField>
|
|
963
|
+
{fn && fn.description && <div className="xeplr-factory-prop-help">{fn.description}</div>}
|
|
964
|
+
{schema.filter((f) => f.name !== NOTE_INPUT).map((f) => (
|
|
965
|
+
<MiniField key={f.name} label={(f.label || f.name) + (f.required && f.default === undefined ? ' *' : '')} help={f.description}>
|
|
966
|
+
<ParamInput field={f} value={b.params ? b.params[f.name] : undefined} onChange={(v) => {
|
|
967
|
+
const params = { ...(b.params || {}) }
|
|
968
|
+
if (v === undefined || v === '') delete params[f.name]; else params[f.name] = v
|
|
969
|
+
edit(i, { params: Object.keys(params).length ? params : undefined })
|
|
970
|
+
}} />
|
|
971
|
+
</MiniField>
|
|
972
|
+
))}
|
|
973
|
+
<label className="xeplr-factory-toggle">
|
|
974
|
+
<input type="checkbox" checked={Boolean(b.ask)} onChange={(e) => edit(i, { ask: e.target.checked ? { title: `${b.label || 'Continue'}?` } : undefined })} />
|
|
975
|
+
<span>Ask first</span>
|
|
976
|
+
</label>
|
|
977
|
+
{b.ask && (
|
|
978
|
+
<>
|
|
979
|
+
<MiniField label="Question"><input className="xeplr-factory-prop-input" value={b.ask.title || ''} onChange={(e) => edit(i, { ask: clean({ ...b.ask, title: e.target.value }) })} /></MiniField>
|
|
980
|
+
<MiniField label="Explanation"><input className="xeplr-factory-prop-input" value={b.ask.message || ''} onChange={(e) => edit(i, { ask: clean({ ...b.ask, message: e.target.value }) })} /></MiniField>
|
|
981
|
+
{takesNote
|
|
982
|
+
? <MiniField label="Note box label" help="Blank → no note box"><input className="xeplr-factory-prop-input" value={b.ask.note || ''} placeholder="Reason (optional)" onChange={(e) => edit(i, { ask: clean({ ...b.ask, note: e.target.value }) })} /></MiniField>
|
|
983
|
+
: fn && <div className="xeplr-factory-prop-help">{fn.name} takes no note.</div>}
|
|
984
|
+
</>
|
|
985
|
+
)}
|
|
986
|
+
<ConditionsEditor label="Shows when" value={b.when} onChange={(v) => edit(i, { when: v })} />
|
|
987
|
+
<ConditionsEditor label="Greyed out when" value={b.disabledWhen} onChange={(v) => edit(i, { disabledWhen: v })} />
|
|
988
|
+
<MiniField label="Look">
|
|
989
|
+
<select className="xeplr-factory-prop-input" value={b.variant || 'default'} onChange={(e) => edit(i, { variant: e.target.value === 'default' ? undefined : e.target.value })}>
|
|
990
|
+
{BUTTON_VARIANTS.map((v) => <option key={v} value={v}>{VARIANT_LABELS[v]}</option>)}
|
|
991
|
+
</select>
|
|
992
|
+
</MiniField>
|
|
993
|
+
</fieldset>
|
|
994
|
+
)
|
|
995
|
+
})}
|
|
996
|
+
<button type="button" className="xeplr-factory-secondary" onClick={add}>+ Button</button>
|
|
997
|
+
</div>
|
|
998
|
+
)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const VARIANT_LABELS = { default: 'Ordinary', primary: 'Main', danger: 'Undo / remove' }
|
|
1002
|
+
|
|
1003
|
+
/** One parameter's value: a dropdown when the function names the values allowed. */
|
|
1004
|
+
function ParamInput({ field, value, onChange }) {
|
|
1005
|
+
const allowed = optionValues(field.options)
|
|
1006
|
+
if (allowed.length) {
|
|
1007
|
+
const labelOf = (v) => {
|
|
1008
|
+
const o = (field.options || []).find((x) => x && typeof x === 'object' && x.value === v)
|
|
1009
|
+
return o && o.label ? o.label : String(v)
|
|
1010
|
+
}
|
|
1011
|
+
return (
|
|
1012
|
+
<select className="xeplr-factory-prop-input" value={value === undefined ? '' : String(value)} onChange={(e) => {
|
|
1013
|
+
const hit = allowed.find((v) => String(v) === e.target.value)
|
|
1014
|
+
onChange(hit === undefined ? undefined : hit)
|
|
1015
|
+
}}>
|
|
1016
|
+
<option value="">—</option>
|
|
1017
|
+
{allowed.map((v) => <option key={String(v)} value={String(v)}>{labelOf(v)}</option>)}
|
|
1018
|
+
</select>
|
|
1019
|
+
)
|
|
1020
|
+
}
|
|
1021
|
+
if (field.type === 'boolean') {
|
|
1022
|
+
return (
|
|
1023
|
+
<select className="xeplr-factory-prop-input" value={value === undefined ? '' : String(value)} onChange={(e) => onChange(e.target.value === '' ? undefined : e.target.value === 'true')}>
|
|
1024
|
+
<option value="">{field.default === undefined ? '—' : `Default (${field.default ? 'yes' : 'no'})`}</option>
|
|
1025
|
+
<option value="true">Yes</option>
|
|
1026
|
+
<option value="false">No</option>
|
|
1027
|
+
</select>
|
|
1028
|
+
)
|
|
1029
|
+
}
|
|
1030
|
+
if (field.type === 'number') {
|
|
1031
|
+
return <input type="number" className="xeplr-factory-prop-input" value={value ?? ''} onChange={(e) => onChange(e.target.value === '' ? undefined : Number(e.target.value))} />
|
|
1032
|
+
}
|
|
1033
|
+
return <input className="xeplr-factory-prop-input" value={value ?? ''} onChange={(e) => onChange(e.target.value)} />
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/** Conditions on the record: every one must match. */
|
|
1037
|
+
function ConditionsEditor({ label, value, onChange }) {
|
|
1038
|
+
const rules = Array.isArray(value) ? value : []
|
|
1039
|
+
const set = (next) => onChange(next.length ? next : undefined)
|
|
1040
|
+
const edit = (i, patch) => set(rules.map((r, j) => {
|
|
1041
|
+
if (j !== i) return r
|
|
1042
|
+
const next = { ...r, ...patch }
|
|
1043
|
+
const op = CONDITION_OPERATORS.find((o) => o.key === next.op)
|
|
1044
|
+
if (op && op.noValue) { delete next.value; delete next.value2 }
|
|
1045
|
+
if (op && !op.second) delete next.value2
|
|
1046
|
+
return next
|
|
1047
|
+
}))
|
|
1048
|
+
return (
|
|
1049
|
+
<div className="xeplr-factory-conditions">
|
|
1050
|
+
<div className="xeplr-factory-prop-label">{label}{rules.length > 1 ? ' — all of these' : ''}</div>
|
|
1051
|
+
{rules.map((r, i) => {
|
|
1052
|
+
const op = CONDITION_OPERATORS.find((o) => o.key === r.op)
|
|
1053
|
+
return (
|
|
1054
|
+
<div key={i} className="xeplr-factory-rows-editor-row">
|
|
1055
|
+
<input className="xeplr-factory-prop-input" aria-label="Field" placeholder="customStatus" value={r.field || ''} onChange={(e) => edit(i, { field: e.target.value.trim() })} />
|
|
1056
|
+
<select className="xeplr-factory-prop-input" aria-label="Comparison" value={r.op || 'eq'} onChange={(e) => edit(i, { op: e.target.value })}>
|
|
1057
|
+
{CONDITION_OPERATORS.map((o) => <option key={o.key} value={o.key}>{o.label}</option>)}
|
|
1058
|
+
</select>
|
|
1059
|
+
{!(op && op.noValue) && <input className="xeplr-factory-prop-input" aria-label="Value" value={r.value ?? ''} onChange={(e) => edit(i, { value: e.target.value })} />}
|
|
1060
|
+
{op && op.second && <input className="xeplr-factory-prop-input" aria-label="And" value={r.value2 ?? ''} onChange={(e) => edit(i, { value2: e.target.value })} />}
|
|
1061
|
+
<button type="button" className="xeplr-factory-link-danger" aria-label="Remove condition" onClick={() => set(rules.filter((_, j) => j !== i))}>×</button>
|
|
1062
|
+
</div>
|
|
1063
|
+
)
|
|
1064
|
+
})}
|
|
1065
|
+
<button type="button" className="xeplr-factory-link" onClick={() => set([...rules, { field: '', op: 'eq', value: '' }])}>+ Condition</button>
|
|
1066
|
+
</div>
|
|
1067
|
+
)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function MiniField({ label, help, children }) {
|
|
1071
|
+
return (
|
|
1072
|
+
<label className="xeplr-factory-mini">
|
|
1073
|
+
<span className="xeplr-factory-mini-label">{label}</span>
|
|
1074
|
+
{children}
|
|
1075
|
+
{help && <span className="xeplr-factory-prop-help">{help}</span>}
|
|
1076
|
+
</label>
|
|
1077
|
+
)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function RowTools({ i, count, onMove, onRemove }) {
|
|
1081
|
+
return (
|
|
1082
|
+
<span className="xeplr-factory-rows-editor-tools">
|
|
1083
|
+
<button type="button" className="xeplr-factory-link" aria-label="Move up" disabled={i === 0} onClick={() => onMove(i - 1)}>↑</button>
|
|
1084
|
+
<button type="button" className="xeplr-factory-link" aria-label="Move down" disabled={i === count - 1} onClick={() => onMove(i + 1)}>↓</button>
|
|
1085
|
+
<button type="button" className="xeplr-factory-link-danger" aria-label="Remove" onClick={onRemove}>×</button>
|
|
1086
|
+
</span>
|
|
1087
|
+
)
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function move(list, from, to) {
|
|
1091
|
+
const next = list.slice()
|
|
1092
|
+
const [item] = next.splice(from, 1)
|
|
1093
|
+
next.splice(to, 0, item)
|
|
1094
|
+
return next
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function uniqueId(base, taken) {
|
|
1098
|
+
if (!taken.has(base)) return base
|
|
1099
|
+
let n = 2
|
|
1100
|
+
while (taken.has(`${base}-${n}`)) n += 1
|
|
1101
|
+
return `${base}-${n}`
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function clean(obj) {
|
|
1105
|
+
const out = {}
|
|
1106
|
+
Object.keys(obj).forEach((k) => { if (obj[k] !== undefined && obj[k] !== '') out[k] = obj[k] })
|
|
1107
|
+
return out
|
|
1108
|
+
}
|
|
1109
|
+
|
|
781
1110
|
export const EDITORS = {
|
|
782
1111
|
text: TextEditor,
|
|
783
1112
|
textarea: TextareaEditor,
|
|
@@ -798,5 +1127,8 @@ export const EDITORS = {
|
|
|
798
1127
|
columns: ColumnsEditor,
|
|
799
1128
|
listTable: ListTableEditor,
|
|
800
1129
|
actions: ActionsEditor,
|
|
801
|
-
dataSource: DataSourceEditor
|
|
1130
|
+
dataSource: DataSourceEditor,
|
|
1131
|
+
function: FunctionEditor,
|
|
1132
|
+
filters: FiltersEditor,
|
|
1133
|
+
buttons: ButtonsEditor
|
|
802
1134
|
}
|
|
@@ -183,6 +183,11 @@ export default function ScreenSample({ ctrl, className, style, renderPopup, rend
|
|
|
183
183
|
// Nor a <form> of its own — one form inside another is not valid HTML.
|
|
184
184
|
const Page = ctrl.embedded ? 'div' : 'form'
|
|
185
185
|
const steppers = ctrl.steps.nodes
|
|
186
|
+
// A list that grows with its rows, or a panel of buttons (wrapping on a
|
|
187
|
+
// narrow screen) — its screen's one control: the page is as tall as it is,
|
|
188
|
+
// rather than it scrolling or spilling out of a fixed box.
|
|
189
|
+
const only = shown.length === 1 ? shown[0] : null
|
|
190
|
+
const grows = Boolean(only && ((only.type === 'list' && only.props && only.props.grow) || only.type === 'buttons'))
|
|
186
191
|
|
|
187
192
|
return (
|
|
188
193
|
<div
|
|
@@ -190,7 +195,7 @@ export default function ScreenSample({ ctrl, className, style, renderPopup, rend
|
|
|
190
195
|
style={{ maxWidth: doc.width, '--xf-margin': `${MARGIN * 100}%`, ...screenStyle(doc), ...style }}
|
|
191
196
|
>
|
|
192
197
|
<Page
|
|
193
|
-
className=
|
|
198
|
+
className={'xeplr-factory-page' + (grows ? ' xeplr-factory-page--grows' : '')}
|
|
194
199
|
style={{ '--xf-page-h': `${pageHeight}cqw` }}
|
|
195
200
|
// Never a submit: Enter posts nothing and saves nothing. Save is a button.
|
|
196
201
|
{...(ctrl.embedded ? { role: 'group' } : { onSubmit: (e) => e.preventDefault(), noValidate: true })}
|
|
@@ -232,6 +237,11 @@ export default function ScreenSample({ ctrl, className, style, renderPopup, rend
|
|
|
232
237
|
recordKey: ctrl.recordKey,
|
|
233
238
|
canDelete: ctrl.canDelete,
|
|
234
239
|
extraActions: ctrl.actionsFor ? ctrl.actionsFor(node) : [],
|
|
240
|
+
// Rows a function works out: what they are narrowed to, a
|
|
241
|
+
// cell's hover box, and a row opened at one part of it.
|
|
242
|
+
query: ctrl.listQuery,
|
|
243
|
+
rowDetails: ctrl.rowDetails,
|
|
244
|
+
onOpen: (rec, where) => ctrl.openEditor(node, rec, where),
|
|
235
245
|
// A list with an edit screen opens it in a popup; one on a form
|
|
236
246
|
// opens the row in the form's own fields.
|
|
237
247
|
// A list on the form itself swaps the record in the fields — so
|
|
@@ -243,6 +253,7 @@ export default function ScreenSample({ ctrl, className, style, renderPopup, rend
|
|
|
243
253
|
} : undefined}
|
|
244
254
|
// A form inside this one: the page draws it (renderSubform).
|
|
245
255
|
subform={node.type === 'subform' ? { sub: ctrl.subformFor(node), render: renderSubform } : undefined}
|
|
256
|
+
buttons={node.type === 'buttons' && ctrl.buttonsFor ? ctrl.buttonsFor(node) : undefined}
|
|
246
257
|
/>
|
|
247
258
|
</div>
|
|
248
259
|
))}
|
package/src/designs/factory.css
CHANGED
|
@@ -70,6 +70,12 @@
|
|
|
70
70
|
position: absolute; box-sizing: border-box;
|
|
71
71
|
left: var(--xf-x); top: var(--xf-y); width: var(--xf-w); height: var(--xf-h);
|
|
72
72
|
}
|
|
73
|
+
/* A list that grows with its rows (props.grow), or a panel of buttons — its
|
|
74
|
+
screen's one control: in the flow, where it was placed across, as tall as
|
|
75
|
+
it needs. */
|
|
76
|
+
.xeplr-factory-page--grows { height: auto; }
|
|
77
|
+
.xeplr-factory-page--grows > .xeplr-factory-node { position: static; margin: var(--xf-y) 0 0 var(--xf-x); height: auto; }
|
|
78
|
+
.xeplr-factory-page--grows .xeplr-factory-list { max-height: none; }
|
|
73
79
|
|
|
74
80
|
.xeplr-factory-invalid {
|
|
75
81
|
border: 1px solid var(--xeplr-danger, #c2372e); border-radius: 8px; padding: 12px 16px;
|
|
@@ -288,6 +294,104 @@
|
|
|
288
294
|
.xeplr-factory-list .xeplr-table-row.xeplr-factory-row-open td,
|
|
289
295
|
.xeplr-factory-list tr.xeplr-factory-row-open td { background: color-mix(in srgb, var(--xf-accent) 10%, transparent); }
|
|
290
296
|
|
|
297
|
+
/* ── a list whose rows a function works out ────────────────────────────────
|
|
298
|
+
Search and chips over the rows; every cell is text the server wrote. */
|
|
299
|
+
.xeplr-factory-list--fed { overflow: hidden; }
|
|
300
|
+
.xeplr-factory-list--fed .xeplr-table-container { border: none; border-radius: 0; background: transparent; }
|
|
301
|
+
.xeplr-factory-list-tools {
|
|
302
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 12px 14px;
|
|
303
|
+
border-bottom: 1px solid var(--xf-border);
|
|
304
|
+
}
|
|
305
|
+
.xeplr-factory-list-tools .xeplr-factory-list-title { flex: 0 1 auto; }
|
|
306
|
+
.xeplr-factory-list-search { flex: 1 1 220px; max-width: 360px; min-width: 0; }
|
|
307
|
+
.xeplr-factory-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
308
|
+
.xeplr-factory-chip {
|
|
309
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
310
|
+
font: inherit; font-size: .9em; padding: 4px 10px; border-radius: 999px; cursor: pointer; white-space: nowrap;
|
|
311
|
+
color: var(--xf-ink); background: transparent; border: 1px solid var(--xf-border);
|
|
312
|
+
}
|
|
313
|
+
.xeplr-factory-chip:hover { background: var(--xf-hover); }
|
|
314
|
+
.xeplr-factory-chip.is-on { color: var(--xf-accent-ink); background: var(--xf-accent); border-color: var(--xf-accent); }
|
|
315
|
+
.xeplr-factory-chip:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--xf-accent) 35%, transparent); }
|
|
316
|
+
.xeplr-factory-chip-count { font-size: .85em; opacity: .75; font-variant-numeric: tabular-nums; }
|
|
317
|
+
.xeplr-factory-fed-table.is-openable tbody tr { cursor: pointer; }
|
|
318
|
+
.xeplr-factory-list tr.xeplr-factory-row-marked td { background: color-mix(in srgb, var(--xf-accent) 6%, transparent); }
|
|
319
|
+
.xeplr-factory-list tr.xeplr-factory-row-marked td:first-child { box-shadow: inset 3px 0 0 var(--xf-accent); }
|
|
320
|
+
.xeplr-factory-cell { display: inline-flex; flex-wrap: wrap; align-items: baseline; column-gap: 6px; row-gap: 2px; min-width: 0; }
|
|
321
|
+
.xeplr-factory-cell-open { font: inherit; color: inherit; background: none; border: none; padding: 0; text-align: left; cursor: pointer; }
|
|
322
|
+
.xeplr-factory-cell-open .xeplr-factory-cell-main { font-weight: 600; color: var(--xf-accent); }
|
|
323
|
+
.xeplr-factory-cell-open:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--xf-accent) 35%, transparent); border-radius: 4px; }
|
|
324
|
+
.xeplr-factory-cell-sub { flex-basis: 100%; font-size: .88em; color: var(--xf-muted); }
|
|
325
|
+
.xeplr-factory-badge {
|
|
326
|
+
font-size: .75em; font-weight: 600; padding: 1px 7px; border-radius: 999px; white-space: nowrap;
|
|
327
|
+
color: var(--xf-accent); background: color-mix(in srgb, var(--xf-accent) 14%, transparent);
|
|
328
|
+
}
|
|
329
|
+
.xeplr-factory-badge--success { color: var(--xf-good); background: color-mix(in srgb, var(--xf-good) 14%, transparent); }
|
|
330
|
+
.xeplr-factory-badge--warning { color: var(--xf-warn); background: color-mix(in srgb, var(--xf-warn) 16%, transparent); }
|
|
331
|
+
.xeplr-factory-badge--danger { color: var(--xf-danger); background: color-mix(in srgb, var(--xf-danger) 14%, transparent); }
|
|
332
|
+
.xeplr-factory-badge--muted { color: var(--xf-muted); background: var(--xf-hover); }
|
|
333
|
+
.xeplr-factory-thumbs { display: inline-flex; align-items: center; gap: 4px; }
|
|
334
|
+
.xeplr-factory-thumbs img { width: 36px; height: 36px; object-fit: cover; border-radius: 6px; border: 1px solid var(--xf-border); background: var(--xf-plane); }
|
|
335
|
+
.xeplr-factory-thumbs small { color: var(--xf-muted); }
|
|
336
|
+
.xeplr-factory-activity { max-width: 280px; white-space: normal; }
|
|
337
|
+
.xeplr-factory-activity[tabindex]:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--xf-accent) 35%, transparent); border-radius: 4px; }
|
|
338
|
+
/* At the page's root (a portal), so it reads the app's theme directly. */
|
|
339
|
+
.xeplr-factory-activity-box {
|
|
340
|
+
position: fixed; z-index: 1200; box-sizing: border-box; overflow-y: auto; padding: 6px;
|
|
341
|
+
max-width: calc(100vw - 16px);
|
|
342
|
+
font-size: 13px; color: var(--xeplr-text-primary, #1a1a19);
|
|
343
|
+
background: var(--xeplr-bg-tertiary, #ffffff); border: 1px solid var(--xeplr-border-primary, #d9d8d0);
|
|
344
|
+
border-radius: 10px; box-shadow: 0 12px 32px rgba(0, 0, 0, .22);
|
|
345
|
+
}
|
|
346
|
+
.xeplr-factory-activity-box .xeplr-factory-hint { padding: 8px; }
|
|
347
|
+
.xeplr-factory-activity-item {
|
|
348
|
+
display: flex; flex-direction: column; gap: 2px; width: 100%; box-sizing: border-box;
|
|
349
|
+
padding: 7px 8px; border-radius: 6px; font: inherit; color: inherit; text-align: left; background: none; border: none;
|
|
350
|
+
}
|
|
351
|
+
button.xeplr-factory-activity-item { cursor: pointer; }
|
|
352
|
+
button.xeplr-factory-activity-item:hover, button.xeplr-factory-activity-item:focus-visible { outline: none; background: var(--xeplr-bg-hover, #f0f0ee); }
|
|
353
|
+
.xeplr-factory-activity-sub { font-size: .9em; color: var(--xeplr-text-secondary, #6b6a63); }
|
|
354
|
+
|
|
355
|
+
/* ── a panel of buttons ─────────────────────────────────────────────────── */
|
|
356
|
+
.xeplr-factory-buttons {
|
|
357
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 8px; width: 100%; box-sizing: border-box;
|
|
358
|
+
font-size: max(10px, calc(var(--xf-font, 14) * var(--xf-u)));
|
|
359
|
+
}
|
|
360
|
+
.xeplr-factory-buttons-title { font-weight: 600; color: var(--xf-muted); margin-right: 4px; }
|
|
361
|
+
.xeplr-factory-button {
|
|
362
|
+
font: inherit; font-weight: 600; font-size: .92em; padding: 6px 13px; border-radius: 6px; cursor: pointer; white-space: nowrap;
|
|
363
|
+
color: var(--xf-ink); background: var(--xf-surface); border: 1px solid var(--xf-border);
|
|
364
|
+
transition: background .12s, transform .08s, box-shadow .12s;
|
|
365
|
+
}
|
|
366
|
+
.xeplr-factory-button:hover:not(:disabled) { background: var(--xf-hover); }
|
|
367
|
+
.xeplr-factory-button:active:not(:disabled) { transform: scale(.96); }
|
|
368
|
+
.xeplr-factory-button:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--xf-accent) 35%, transparent); }
|
|
369
|
+
.xeplr-factory-button:disabled { opacity: .5; cursor: default; }
|
|
370
|
+
.xeplr-factory-button--primary { color: var(--xf-accent-ink); background: var(--xf-accent); border-color: var(--xf-accent); }
|
|
371
|
+
.xeplr-factory-button--primary:hover:not(:disabled) { background: var(--xf-accent); filter: brightness(1.08); }
|
|
372
|
+
.xeplr-factory-button--danger { color: var(--xf-danger); border-color: color-mix(in srgb, var(--xf-danger) 55%, var(--xf-border)); }
|
|
373
|
+
.xeplr-factory-button--danger:hover:not(:disabled) { background: color-mix(in srgb, var(--xf-danger) 10%, transparent); }
|
|
374
|
+
.xeplr-factory-buttons--design .xeplr-factory-button { cursor: inherit; }
|
|
375
|
+
.xeplr-factory-buttons-note {
|
|
376
|
+
display: inline-flex; align-items: center; gap: 6px; font-size: .9em; padding: 4px 6px 4px 10px; border-radius: 6px;
|
|
377
|
+
color: var(--xf-good); background: color-mix(in srgb, var(--xf-good) 10%, transparent);
|
|
378
|
+
}
|
|
379
|
+
.xeplr-factory-buttons-note--error { color: var(--xf-danger); background: color-mix(in srgb, var(--xf-danger) 10%, transparent); }
|
|
380
|
+
.xeplr-factory-buttons-note-close { font: inherit; font-size: 1.1em; line-height: 1; color: inherit; background: none; border: none; cursor: pointer; padding: 2px 4px; }
|
|
381
|
+
.xeplr-factory-ask { --xf-modal-w: min(440px, 96vw); }
|
|
382
|
+
.xeplr-factory-ask .xeplr-factory-modal-body { padding: 4px 18px 8px; display: flex; flex-direction: column; gap: 10px; }
|
|
383
|
+
.xeplr-factory-ask-message { margin: 0; color: var(--xf-muted); }
|
|
384
|
+
.xeplr-factory-ask-field { display: flex; flex-direction: column; gap: 4px; }
|
|
385
|
+
.xeplr-factory-ask-label { font-weight: 600; font-size: .92em; }
|
|
386
|
+
.xeplr-factory-ask-note, .xeplr-factory-list-search {
|
|
387
|
+
box-sizing: border-box; width: 100%; font: inherit; padding: 7px 10px; border-radius: 6px;
|
|
388
|
+
color: var(--xf-ink); background: var(--xf-field-bg); border: 1px solid var(--xf-field-border);
|
|
389
|
+
}
|
|
390
|
+
.xeplr-factory-ask-note { resize: vertical; min-height: 80px; }
|
|
391
|
+
.xeplr-factory-ask-note:focus, .xeplr-factory-list-search:focus { outline: none; border-color: var(--xf-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--xf-accent) 25%, transparent); }
|
|
392
|
+
.xeplr-factory-ask-error { margin: 0; color: var(--xf-danger); }
|
|
393
|
+
.xeplr-factory-ask-foot { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 18px 16px; }
|
|
394
|
+
|
|
291
395
|
/* ── the builder ────────────────────────────────────────────────────────── */
|
|
292
396
|
.xeplr-factory-builder {
|
|
293
397
|
display: flex; flex-direction: column; height: 100%; min-height: 560px;
|
|
@@ -397,6 +501,27 @@
|
|
|
397
501
|
.xeplr-factory-panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
|
398
502
|
.xeplr-factory-panel-type { font-weight: 700; font-size: 14px; }
|
|
399
503
|
.xeplr-factory-link-danger { background: none; border: none; color: var(--xf-danger); cursor: pointer; font: inherit; padding: 2px 4px; }
|
|
504
|
+
.xeplr-factory-link { background: none; border: none; color: var(--xf-accent); cursor: pointer; font: inherit; padding: 2px 4px; }
|
|
505
|
+
.xeplr-factory-link:disabled { color: var(--xf-muted); opacity: .5; cursor: default; }
|
|
506
|
+
/* Lists of things edited in place: chips, columns, buttons, conditions. */
|
|
507
|
+
.xeplr-factory-rows-editor { display: flex; flex-direction: column; gap: 8px; }
|
|
508
|
+
.xeplr-factory-rows-editor > .xeplr-factory-secondary { align-self: flex-start; padding: 4px 10px; }
|
|
509
|
+
.xeplr-factory-rows-editor-row { display: flex; align-items: center; gap: 4px; }
|
|
510
|
+
.xeplr-factory-rows-editor-row .xeplr-factory-prop-input { flex: 1 1 0; min-width: 0; }
|
|
511
|
+
.xeplr-factory-prop-input--key { flex: 0 1 38% !important; font-family: ui-monospace, monospace; font-size: 11px; }
|
|
512
|
+
.xeplr-factory-rows-editor-card {
|
|
513
|
+
position: relative; display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 8px 10px 10px;
|
|
514
|
+
border: 1px solid var(--xf-border); border-radius: 8px;
|
|
515
|
+
}
|
|
516
|
+
.xeplr-factory-rows-editor-card > legend { font-weight: 600; font-size: 12px; padding: 0 4px; max-width: 60%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
517
|
+
.xeplr-factory-rows-editor-card > .xeplr-factory-rows-editor-tools { position: absolute; top: -9px; right: 6px; background: var(--xf-surface); padding: 0 2px; }
|
|
518
|
+
.xeplr-factory-mini { display: flex; flex-direction: column; gap: 2px; }
|
|
519
|
+
.xeplr-factory-mini-label { font-size: 11px; font-weight: 600; color: var(--xf-muted); }
|
|
520
|
+
.xeplr-factory-conditions { display: flex; flex-direction: column; gap: 4px; }
|
|
521
|
+
.xeplr-factory-conditions > .xeplr-factory-link { align-self: flex-start; padding-left: 0; }
|
|
522
|
+
/* The field on a line of its own (names are long); the comparison and value under it. */
|
|
523
|
+
.xeplr-factory-conditions .xeplr-factory-rows-editor-row { flex-wrap: wrap; padding-bottom: 6px; border-bottom: 1px dashed var(--xf-border); }
|
|
524
|
+
.xeplr-factory-conditions .xeplr-factory-rows-editor-row > input:first-child { flex: 1 1 100%; }
|
|
400
525
|
.xeplr-factory-group { border: none; border-top: 1px solid var(--xf-border); margin: 0; padding: 10px 0 4px; }
|
|
401
526
|
.xeplr-factory-group legend { padding: 0 4px 0 0; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--xf-muted); }
|
|
402
527
|
.xeplr-factory-prop { margin-bottom: 10px; }
|
|
@@ -703,3 +828,17 @@
|
|
|
703
828
|
.xeplr-factory-modal-body { padding: 0; }
|
|
704
829
|
.xeplr-factory-modal-close { min-width: 44px; min-height: 44px; }
|
|
705
830
|
}
|
|
831
|
+
|
|
832
|
+
/* Function-fed lists and buttons on a phone: the search takes the width, chips
|
|
833
|
+
scroll in a row, and every button is a thumb-sized target. */
|
|
834
|
+
@media (max-width: 640px) {
|
|
835
|
+
.xeplr-factory-list-search, .xeplr-factory-ask-note { font-size: max(16px, 1em); min-height: 44px; }
|
|
836
|
+
.xeplr-factory-list-search { flex-basis: 100%; max-width: none; }
|
|
837
|
+
.xeplr-factory-ask-foot button { flex: 1; min-height: 44px; }
|
|
838
|
+
.xeplr-factory-chips { flex-wrap: nowrap; overflow-x: auto; max-width: 100%; padding-bottom: 2px; }
|
|
839
|
+
.xeplr-factory-chip { min-height: 36px; }
|
|
840
|
+
.xeplr-factory-button { min-height: 44px; flex: 1 1 auto; }
|
|
841
|
+
.xeplr-factory-buttons-note { flex-basis: 100%; }
|
|
842
|
+
.xeplr-factory-ask .xeplr-factory-modal-body { padding: 4px 16px 8px; }
|
|
843
|
+
.xeplr-factory-ask { min-height: 0; align-self: flex-end; }
|
|
844
|
+
}
|
package/src/events.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// What each is called with, and what it may return, is the contract a
|
|
14
14
|
// developer writes to. `@xeplr/factory` runs them; the designer lists them.
|
|
15
15
|
|
|
16
|
+
import { nodeFunctions, paramProblems } from './buttons.js'
|
|
17
|
+
|
|
16
18
|
export const SCREEN_EVENTS = [
|
|
17
19
|
{
|
|
18
20
|
key: 'beforeLoad',
|
|
@@ -83,6 +85,25 @@ export function missingFunctions(doc, registered) {
|
|
|
83
85
|
})
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
/**
|
|
89
|
+
* EVERYTHING THAT STOPS A SCREEN RUNNING ITS FUNCTIONS — its events, and the
|
|
90
|
+
* functions its controls name (a list's rows, a column's hover box, each
|
|
91
|
+
* button), plus each button's parameters against what its function takes.
|
|
92
|
+
* `registered` is the registry's list ([{ name, event, inputSchema }]).
|
|
93
|
+
* What publishing refuses on. Empty when the screen can run.
|
|
94
|
+
*/
|
|
95
|
+
export function functionProblems(doc, registered) {
|
|
96
|
+
const byName = new Map((registered || []).map((f) => [f.name, f]))
|
|
97
|
+
const fromNodes = nodeFunctions(doc).flatMap(({ event, name, at, button }) => {
|
|
98
|
+
const f = byName.get(name)
|
|
99
|
+
if (!f) return [{ event, name, at, problem: `"${name}" is not a registered function` }]
|
|
100
|
+
if (f.event !== event) return [{ event, name, at, problem: `"${name}" is written for ${f.event ? `"${f.event}"` : 'a workflow step'}, not "${event}"` }]
|
|
101
|
+
if (!button) return []
|
|
102
|
+
return paramProblems(button, f.inputSchema).map((p) => ({ event, name, at, problem: `button "${button.label || button.id}": "${name}" ${p}` }))
|
|
103
|
+
})
|
|
104
|
+
return [...missingFunctions(doc, registered), ...fromNodes]
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
/** The screen with one event set to a function — or cleared, given nothing. */
|
|
87
108
|
export function setEvent(doc, key, name) {
|
|
88
109
|
if (!SCREEN_EVENT_KEYS.includes(key)) throw new Error(`"${key}" is not an event — one of: ${SCREEN_EVENT_KEYS.join(', ')}`)
|
package/src/model.js
CHANGED
|
@@ -13,7 +13,14 @@ export {
|
|
|
13
13
|
} from './document.js'
|
|
14
14
|
export { validateDocument, assertValidDocument } from './validateDocument.js'
|
|
15
15
|
// A screen's events: functions picked by name, run on the server.
|
|
16
|
-
export { SCREEN_EVENTS, SCREEN_EVENT_KEYS, DECIDE_EVENT, FUNCTION_NAME, screenFunctions, missingFunctions, setEvent, functionsFor } from './events.js'
|
|
16
|
+
export { SCREEN_EVENTS, SCREEN_EVENT_KEYS, DECIDE_EVENT, FUNCTION_NAME, screenFunctions, missingFunctions, functionProblems, setEvent, functionsFor } from './events.js'
|
|
17
|
+
// Buttons and lists whose rows a function works out — names and values only.
|
|
18
|
+
export {
|
|
19
|
+
BUTTON_EVENT, LIST_ROWS_EVENT, ROW_DETAILS_EVENT, BUTTON_VARIANTS, COLUMN_DISPLAYS, BADGE_TONES, NOTE_INPUT, CONDITION_OPERATORS,
|
|
20
|
+
conditionMet, buttonShown, buttonDisabled, screenButtons, buttonById, buttonInput, nodeFunctions, paramProblems, optionValues
|
|
21
|
+
} from './buttons.js'
|
|
22
|
+
// Which pages are drawn from documents and which are web only (ui/pages.json).
|
|
23
|
+
export { checkInventory, routesIn, pageWarnings, webOnlyMenus } from './pagesInventory.js'
|
|
17
24
|
export { formSchema, initialValues, parseInput, validateValues, fieldError, sameId, optionValue, chooseable, saveState, recordValues, displayValue, fileLabel, toDbValue, fromDbValue, listColumns, listSource } from './values.js'
|
|
18
25
|
export { screenFromSpec, screensFromSpec, entityNames, SPEC_HEIGHTS, ROW_GAP } from './generate.js'
|
|
19
26
|
export { scaffoldEntity } from './scaffold.js'
|