@xeplr/ui-factory 1.2.0 → 1.3.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/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) out[name] = node.props.default
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
+ }