@xeplr/ui-factory 1.2.0 → 1.4.0
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/README.md +201 -0
- package/bin/xeplr-factory.js +2 -1
- package/package.json +1 -1
- package/src/controls.js +97 -2
- package/src/designs/BuilderSample.jsx +47 -1
- package/src/designs/ControlView.jsx +73 -2
- package/src/designs/ListView.jsx +3 -1
- package/src/designs/PropertyPanel.jsx +237 -7
- package/src/designs/ScreenSample.jsx +15 -9
- package/src/designs/factory.css +64 -0
- package/src/designs/styles.js +1 -1
- package/src/document.js +51 -0
- package/src/index.js +3 -1
- package/src/masters.js +193 -0
- package/src/model.js +5 -0
- package/src/pages.jsx +37 -1
- package/src/remote.js +22 -2
- package/src/screenSource.js +85 -2
- package/src/tableSchema.js +16 -0
- package/src/useFactoryBuilder.js +103 -2
- package/src/useFactoryScreen.js +202 -29
- package/src/useFlowRun.js +6 -3
- package/src/useScreenSource.js +8 -4
- package/src/validateDocument.js +82 -1
- package/src/values.js +118 -2
package/src/values.js
CHANGED
|
@@ -46,12 +46,46 @@ export function formSchema(doc, controls = CONTROLS) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/** Each input's starting value: the caller's, else the control's default. */
|
|
49
|
+
/**
|
|
50
|
+
* A VALUE WITH A TOKEN IS A BINDING; a plain value is a literal.
|
|
51
|
+
*
|
|
52
|
+
* `{params.complaintId}` in a field's default means "whatever this screen was
|
|
53
|
+
* called with" — the same brace this product uses in a workflow step, a
|
|
54
|
+
* condition and a report. Nothing is marked on the field: the brace says it.
|
|
55
|
+
*
|
|
56
|
+
* Unknown, or called with nothing, resolves to undefined — the field is then
|
|
57
|
+
* empty and the person fills it in, which is what the same form does when
|
|
58
|
+
* somebody opens it on its own. A form that will not open is worse than a
|
|
59
|
+
* question nobody expected.
|
|
60
|
+
*
|
|
61
|
+
* Whole-value only. `{params.a}-{params.b}` is not a value a form field takes,
|
|
62
|
+
* and splicing text is a formula's job, not a form's.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveBinding(value, params) {
|
|
65
|
+
if (typeof value !== 'string') return value
|
|
66
|
+
const m = /^\{params\.([A-Za-z_][\w]*)\}$/.exec(value.trim())
|
|
67
|
+
if (!m) return value
|
|
68
|
+
return params ? params[m[1]] : undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Is this what somebody typed, or a reference to an input? */
|
|
72
|
+
export function isBinding(value) {
|
|
73
|
+
return typeof value === 'string' && /^\{params\.[A-Za-z_][\w]*\}$/.test(value.trim())
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
export function initialValues(doc, provided = {}, controls = CONTROLS) {
|
|
50
77
|
const out = {}
|
|
51
78
|
inputNodes(doc, controls).forEach((node) => {
|
|
52
79
|
const name = node.props.name
|
|
53
80
|
if (provided[name] !== undefined) out[name] = provided[name]
|
|
54
|
-
else if (node.props.default !== undefined)
|
|
81
|
+
else if (node.props.default !== undefined) {
|
|
82
|
+
// A default may be a binding to one of the screen's own inputs.
|
|
83
|
+
const resolved = resolveBinding(node.props.default, provided)
|
|
84
|
+
if (resolved !== undefined) out[name] = resolved
|
|
85
|
+
else if (!isBinding(node.props.default)) out[name] = node.props.default
|
|
86
|
+
else if (node.type === 'checkbox') out[name] = false
|
|
87
|
+
else if (node.type === 'multiselect') out[name] = []
|
|
88
|
+
}
|
|
55
89
|
else if (node.type === 'checkbox') out[name] = false
|
|
56
90
|
else if (node.type === 'multiselect') out[name] = []
|
|
57
91
|
})
|
|
@@ -98,6 +132,11 @@ export function fieldError(node, value, loadedOptions) {
|
|
|
98
132
|
const label = p.label || p.name
|
|
99
133
|
const v = p.validation || {}
|
|
100
134
|
|
|
135
|
+
// AN IDENTIFIER IS NOT SOMEBODY'S TO FILL IN. The server assigns a code on
|
|
136
|
+
// first save and the database counts a number, so a rule here would refuse
|
|
137
|
+
// the very save that produces the value.
|
|
138
|
+
if (node.type === 'id') return null
|
|
139
|
+
|
|
101
140
|
if (node.type === 'checkbox') {
|
|
102
141
|
return p.required && value !== true ? `${label} must be ticked` : null
|
|
103
142
|
}
|
|
@@ -234,8 +273,28 @@ export function listSource(doc, node) {
|
|
|
234
273
|
}
|
|
235
274
|
|
|
236
275
|
/** A list's columns: the ones chosen, else the screen's fields in reading order. */
|
|
237
|
-
export function listColumns(doc, node, controls = CONTROLS) {
|
|
276
|
+
export function listColumns(doc, node, controls = CONTROLS, rows) {
|
|
238
277
|
if (node && Array.isArray(node.props?.columns) && node.props.columns.length) return node.props.columns
|
|
278
|
+
|
|
279
|
+
// A LIST OF ANOTHER TABLE HAS NOTHING TO DO WITH THIS SCREEN'S FIELDS.
|
|
280
|
+
//
|
|
281
|
+
// A sub-table — this material's characteristics — holds rows of a different
|
|
282
|
+
// shape entirely, so falling back to this screen's fields shows four empty
|
|
283
|
+
// columns named after the material. The rows themselves are the only
|
|
284
|
+
// description to hand, so they are what it reads.
|
|
285
|
+
const foreign = node && node.props && node.props.source && doc && node.props.source !== doc.source
|
|
286
|
+
if (foreign) {
|
|
287
|
+
const first = (rows || []).find((r) => r && typeof r === 'object')
|
|
288
|
+
if (first) {
|
|
289
|
+
return Object.keys(first)
|
|
290
|
+
// Neither is a column anybody asked to see: one is the row's handle,
|
|
291
|
+
// the other is the record it belongs to, which is the list itself.
|
|
292
|
+
.filter((k) => k !== 'id' && k !== (node.props.belongsTo || ''))
|
|
293
|
+
.map((k) => ({ field: k, label: k }))
|
|
294
|
+
}
|
|
295
|
+
return []
|
|
296
|
+
}
|
|
297
|
+
|
|
239
298
|
return inputNodes(doc, controls).map((n) => ({ field: n.props.name, label: n.props.label || n.props.name }))
|
|
240
299
|
}
|
|
241
300
|
|
|
@@ -299,3 +358,60 @@ export function fromDbValue(node, value) {
|
|
|
299
358
|
}
|
|
300
359
|
return value
|
|
301
360
|
}
|
|
361
|
+
|
|
362
|
+
// ── a code people can read out ──────────────────────────────────────────
|
|
363
|
+
//
|
|
364
|
+
// The record's own id is a 25-character key made on save. It is unique,
|
|
365
|
+
// stable and completely unquotable: nobody reads one over the phone, writes
|
|
366
|
+
// one on a form, or finds a complaint by it. So a field can be marked
|
|
367
|
+
// GENERATED, and gets a short code instead — CMP-7K3QF9.
|
|
368
|
+
//
|
|
369
|
+
// RANDOM, not sequential. A counted number (CMP-0001) needs a counter bumped
|
|
370
|
+
// in the same transaction as the insert, or two people saving at once take
|
|
371
|
+
// the same one; and it still shows gaps whenever a save fails after the
|
|
372
|
+
// number was taken, which somebody then has to explain. A random code has
|
|
373
|
+
// neither problem, at the cost of not telling you the fifth from the five
|
|
374
|
+
// hundredth — which is what the record's date is for.
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The alphabet, minus the characters people mistake for each other.
|
|
378
|
+
*
|
|
379
|
+
* ONE OF EACH PAIR SURVIVES, which is the whole trick: O and I and S are
|
|
380
|
+
* gone, so 0, 1 and 5 are unambiguous and can stay. Dropping both halves
|
|
381
|
+
* would cost a third of the alphabet to solve a problem already solved.
|
|
382
|
+
*
|
|
383
|
+
* The point of this code is that it survives being read aloud, written down
|
|
384
|
+
* and typed back in.
|
|
385
|
+
*/
|
|
386
|
+
var CODE_ALPHABET = '23456789ABCDEFGHJKLMNPQRTUVWXYZ'
|
|
387
|
+
|
|
388
|
+
/** The fields a screen generates a code for. */
|
|
389
|
+
export function codeFields(doc, controls = CONTROLS) {
|
|
390
|
+
return inputNodes(doc, controls).filter((n) => n.type === 'id' && (n.props || {}).idKind !== 'auto')
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The identifiers the DATABASE fills in — a counted number. They are never
|
|
395
|
+
* sent on an insert: the column is an identity, so mentioning it at all would
|
|
396
|
+
* be claiming a number nobody chose.
|
|
397
|
+
*/
|
|
398
|
+
export function countedFields(doc, controls = CONTROLS) {
|
|
399
|
+
return inputNodes(doc, controls).filter((n) => n.type === 'id' && (n.props || {}).idKind === 'auto')
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* One code. `prefix` is what makes it recognisable at a glance — a complaint
|
|
404
|
+
* is CMP-…, a purchase order is PO-… — and is optional.
|
|
405
|
+
*
|
|
406
|
+
* Six characters from 31 is about 900 million codes, which is not a
|
|
407
|
+
* guarantee: whoever writes one is expected to check it is free and try
|
|
408
|
+
* again, which is cheap and has to be done in the database anyway.
|
|
409
|
+
*/
|
|
410
|
+
export function generateCode(prefix, length = 6) {
|
|
411
|
+
let out = ''
|
|
412
|
+
for (let i = 0; i < length; i++) {
|
|
413
|
+
out += CODE_ALPHABET[Math.floor(Math.random() * CODE_ALPHABET.length)]
|
|
414
|
+
}
|
|
415
|
+
const head = String(prefix || '').trim().toUpperCase().replace(/[^A-Z0-9]+/g, '')
|
|
416
|
+
return head ? head + '-' + out : out
|
|
417
|
+
}
|