@xeplr/ui-factory 1.1.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/README.md +189 -2
- package/bin/xeplr-factory.js +2 -1
- package/package.json +7 -2
- package/src/controls.js +62 -2
- package/src/designs/BuilderSample.jsx +47 -1
- package/src/designs/ControlView.jsx +36 -0
- package/src/designs/ListView.jsx +3 -1
- package/src/designs/PropertyPanel.jsx +218 -7
- package/src/designs/factory.css +50 -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 +14 -1
- package/src/remote.js +22 -2
- package/src/screenSource.js +85 -2
- package/src/tableSchema.js +16 -0
- package/src/urlState.js +71 -0
- package/src/useFactoryBuilder.js +134 -10
- package/src/useFactoryScreen.js +45 -14
- package/src/useFlowBuilder.js +7 -4
- package/src/useFlowRun.js +6 -3
- package/src/useScreenSource.js +8 -4
- package/src/useUrlPlaces.js +48 -0
- package/src/validateDocument.js +67 -1
- package/src/values.js +118 -2
package/README.md
CHANGED
|
@@ -44,10 +44,10 @@ Most internal apps are a long tail of forms, and every one of them is hand-built
|
|
|
44
44
|
## Install
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
npm i @xeplr/ui-factory @xeplr/ui-canvas @xeplr/ui-table @tanstack/react-table
|
|
47
|
+
npm i @xeplr/ui-factory @xeplr/ui-canvas @xeplr/ui-table @tanstack/react-table react-router-dom
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
Peer dependencies: `react ^18 || ^19`, [`@xeplr/ui-canvas`](https://www.npmjs.com/package/@xeplr/ui-canvas) (the canvas the builder places controls on)
|
|
50
|
+
Peer dependencies: `react ^18 || ^19`, [`@xeplr/ui-canvas`](https://www.npmjs.com/package/@xeplr/ui-canvas) (the canvas the builder places controls on), [`@xeplr/ui-table`](https://www.npmjs.com/package/@xeplr/ui-table) (lists of saved records) and `react-router-dom ^6 || ^7` (imported by [`@xeplr/ui-utils`](https://www.npmjs.com/package/@xeplr/ui-utils)' `useUrlState`, a dependency, for the URL state below; with no router above, everything stays in memory). Your bundler must compile JSX and import CSS from the package (Vite does both).
|
|
51
51
|
|
|
52
52
|
## The builder
|
|
53
53
|
|
|
@@ -197,6 +197,166 @@ import schemaHandler from '@xeplr/schema-handler' // CommonJS: default
|
|
|
197
197
|
const clean = schemaHandler.applySchema(formSchema(screen), req.body) // throws ValidationError listing each field
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
+
## Identifier — the number or code a record is known by
|
|
201
|
+
|
|
202
|
+
Every record already has a key: 25 characters, made on save, never an
|
|
203
|
+
auto-increment and never a sequence. It is unique, stable and completely
|
|
204
|
+
unquotable — nobody reads one over the phone.
|
|
205
|
+
|
|
206
|
+
So **Identifier** is a control, in the palette beside Text. A type rather
|
|
207
|
+
than a flag on a text box, because everything about it follows from choosing
|
|
208
|
+
it — the column, who fills it in, and whether it is on the form at all:
|
|
209
|
+
|
|
210
|
+
| How it is made | column | filled by | on the form |
|
|
211
|
+
|---|---|---|---|
|
|
212
|
+
| **A code** — `CMP-7K3QF9` | `varchar(24) UNIQUE` | the server, on first save | shown, read-only |
|
|
213
|
+
| **A number** — 1, 2, 3 | `integer GENERATED BY DEFAULT AS IDENTITY … UNIQUE` | the database, as the row is inserted | not drawn until it exists |
|
|
214
|
+
|
|
215
|
+
- **A counted number is not on a new form.** It does not exist until the row
|
|
216
|
+
does, so there is nothing to show but an empty box — and an empty box that
|
|
217
|
+
never fills in is a question the form appears to be asking and is not.
|
|
218
|
+
- **A code is random, not counted.** A sequential one needs a counter bumped
|
|
219
|
+
in the insert's transaction, or two saves take the same number, and it still
|
|
220
|
+
shows gaps when a save fails after the number is taken. Its alphabet drops
|
|
221
|
+
O, I and S — one of each confusable pair, so 0, 1 and 5 stay usable: the
|
|
222
|
+
point of a code is surviving a phone call.
|
|
223
|
+
- **Assigned by the server**, checked for a clash inside the insert's own
|
|
224
|
+
transaction — random is not unique, and a code that was free a moment ago is
|
|
225
|
+
not a promise.
|
|
226
|
+
- **Neither ever changes.** Both are left out of an update entirely, not
|
|
227
|
+
cleared: a record whose reference changes is a reference nobody can use.
|
|
228
|
+
- **Never demanded.** A rule on an identifier would refuse the very save that
|
|
229
|
+
produces it. A value that arrives with the record — an import carrying real
|
|
230
|
+
numbers — is kept, which is why the identity is `BY DEFAULT` and not
|
|
231
|
+
`ALWAYS`.
|
|
232
|
+
|
|
233
|
+
`GENERATED BY DEFAULT AS IDENTITY` is built on the **server**: the browser
|
|
234
|
+
sends a screen document and never SQL. `tableSchema.js` is part of
|
|
235
|
+
`@xeplr/ui-factory/model`, which `@xeplr/factory` imports in Node — sharing
|
|
236
|
+
the rules is not shipping SQL to a browser.
|
|
237
|
+
|
|
238
|
+
## A sub-table: rows that belong to one record
|
|
239
|
+
|
|
240
|
+
This material's characteristics, this order's lines. Put a **List** on the
|
|
241
|
+
form and give it two things:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
Records from material_characteristics [ New table… ]
|
|
245
|
+
Belonging to materialId
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**The link is what makes it a sub-table.** Without it, a list of another
|
|
249
|
+
table shows every row in the app and **New** saves a row belonging to
|
|
250
|
+
nothing. With it:
|
|
251
|
+
|
|
252
|
+
- the grid shows only rows whose `materialId` is this material — and **none**
|
|
253
|
+
while the record is unsaved, because a record that does not exist yet owns
|
|
254
|
+
nothing, and showing the whole table there is how somebody edits another
|
|
255
|
+
record's lines;
|
|
256
|
+
- **New** hands the child screen this material's id, as `params` — the same
|
|
257
|
+
mechanism a flow's Screen step uses — so the field is filled in and hidden,
|
|
258
|
+
and nobody is asked which material they are adding a line to.
|
|
259
|
+
|
|
260
|
+
**New table…** makes the child there and then — two screens, published, with
|
|
261
|
+
the link column on them and declared as an input — and **opens its designer
|
|
262
|
+
over the page**, because a sub-table is made for the fields it will hold and
|
|
263
|
+
`Name` / `Value` are only a guess at those. Publishing in there sets this
|
|
264
|
+
list's columns and closes it; closing without publishing leaves a working
|
|
265
|
+
table with the two fields it was made with.
|
|
266
|
+
|
|
267
|
+
It sets *Records from* and *Belonging to* together, because a table without
|
|
268
|
+
its link is a list of everybody's rows and a link without a table is nothing
|
|
269
|
+
— neither half is a state worth being able to reach. The table list is
|
|
270
|
+
re-fetched at the same time, or the picker would be blank for a table made a
|
|
271
|
+
moment ago, and a picker whose value is not among its options shows nothing
|
|
272
|
+
at all.
|
|
273
|
+
|
|
274
|
+
**Its columns follow its table.** A list of another table is described by
|
|
275
|
+
that table's rows, not by the screen it sits on — offering the material's
|
|
276
|
+
fields for a list of characteristics ticks four columns that are blank in
|
|
277
|
+
every row. Changing *Records from* clears the chosen columns and the link,
|
|
278
|
+
because a field name from the old table is a header over an empty column, and
|
|
279
|
+
leaving it there reads as the designer not having noticed the change.
|
|
280
|
+
|
|
281
|
+
**A list is not a dropdown.** Its rows are records people create, so there is
|
|
282
|
+
no "typed list" alternative the way there is for a dropdown's options. If
|
|
283
|
+
what you want is *tags* — several values picked from a fixed set — that is a
|
|
284
|
+
**Multi-select** pointing at a master, and the chosen ids live in one column
|
|
285
|
+
of the record. Making it a sub-table means a two-table design for what is
|
|
286
|
+
really one column.
|
|
287
|
+
|
|
288
|
+
## Masters — the lists the rest of the app points at
|
|
289
|
+
|
|
290
|
+
Departments, statuses, countries, categories. Mark a form as a master in its
|
|
291
|
+
design (select nothing on the canvas → **This is a master**), and say what a
|
|
292
|
+
picker should show for one of its records:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
{ master: true, labelField: 'name' } // on the form's own document
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Made, not designed.** Every application repeats the same three things —
|
|
299
|
+
a name, an order of its own, and the master it belongs to — so a master is
|
|
300
|
+
created outright rather than drawn:
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
import { createMaster } from '@xeplr/ui-factory'
|
|
304
|
+
|
|
305
|
+
await createMaster(factory, { name: 'States', parent: { table: 'countries', label: 'Country' } })
|
|
306
|
+
// → states_edit + states_list, marked as a master, published
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
- **Name** is required: a master row with no name is a blank line in every
|
|
310
|
+
dropdown that points at it.
|
|
311
|
+
- **Sort order** is not: Low / Medium / High is never alphabetical, but most
|
|
312
|
+
lists are fine, and a number somebody must invent for every row is a tax on
|
|
313
|
+
adding one.
|
|
314
|
+
- **Belongs to** is a dropdown on the parent master, required when there is
|
|
315
|
+
one — a state in no country is a row nothing can use. A master is never
|
|
316
|
+
offered as its own parent.
|
|
317
|
+
|
|
318
|
+
It is **published** on creation, which is the one place doing that on
|
|
319
|
+
somebody's behalf is right: a master that is a draft has no table, so nothing
|
|
320
|
+
can point at it and nobody can add a row, which is all a master is for.
|
|
321
|
+
|
|
322
|
+
Named in the plural, stored in the singular: *Departments* makes the entity
|
|
323
|
+
`department` and the table `departments`. Without that step `entityNames`
|
|
324
|
+
would make `departmentses`, which is noticed only once the table exists and
|
|
325
|
+
then cannot be renamed.
|
|
326
|
+
|
|
327
|
+
**A flag on a form, not a kind of form.** A master has a table, a list, an
|
|
328
|
+
edit screen and permissions like anything else; a separate type would be two
|
|
329
|
+
things that behave the same until they do not, and every feature after it
|
|
330
|
+
would have to ask which one it is holding. The flag adds a fact; a type adds
|
|
331
|
+
a fork.
|
|
332
|
+
|
|
333
|
+
`labelField` is said once, here, rather than on every dropdown that points at
|
|
334
|
+
this form — thirty dropdowns should not each be told that a department is
|
|
335
|
+
shown by its name.
|
|
336
|
+
|
|
337
|
+
`useScreenSource` returns `masters` alongside `screens`: one entry per FORM
|
|
338
|
+
(`{ key, name, labelField, edit, list, source }`), gathered by `masterForms`,
|
|
339
|
+
so an app's admin section renders a list and groups nothing itself. That is
|
|
340
|
+
what `@xeplr/cli` generates as Configure UI → **Masters**.
|
|
341
|
+
|
|
342
|
+
They are gathered in the admin section rather than scattered through the side
|
|
343
|
+
rail because that is what they are — reference data somebody maintains
|
|
344
|
+
occasionally. The rail stays for the work.
|
|
345
|
+
|
|
346
|
+
**And they are what a dropdown offers first.** A field's *where its options
|
|
347
|
+
come from* now reads:
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
Masters Departments, Statuses, Countries ← named, first
|
|
351
|
+
Other tables complaints, employees, pod_items ← still there, below
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`/factory/tables` marks and names them (`@xeplr/factory`'s
|
|
355
|
+
`tablesForPicker`), so the picker groups rather than guesses. Two `<optgroup>`s
|
|
356
|
+
in a plain `<select>`, not a combobox: typing jumps to an option in every
|
|
357
|
+
browser, which is the searching anybody wants here, and a widget of our own
|
|
358
|
+
would have to reimplement the keyboard to match.
|
|
359
|
+
|
|
200
360
|
## Your screens, inside someone else's designer
|
|
201
361
|
|
|
202
362
|
`@xeplr/ui-workflow`'s flow designer has a **Screen** step, and it asks its
|
|
@@ -486,6 +646,26 @@ npx xeplr-factory controls # controls, props and rules
|
|
|
486
646
|
|
|
487
647
|
Layout is **proportional**, like `@xeplr/ui-canvas` dashboards: `x` and `w` are fractions of the width, `y` and `h` fractions of a page (`width × aspect`). The screen is shown at its design `width`; on a narrower display everything — positions and text — scales down together.
|
|
488
648
|
|
|
649
|
+
## Where you are lives in the URL: `urlKey`
|
|
650
|
+
|
|
651
|
+
The control whose properties are open, the step a stepper is open at, and the flow step being edited are places a person navigates to. Give a controller (or its ready-made page) a `urlKey` and they are written to the URL's query, so a refresh or a shared link lands on them again:
|
|
652
|
+
|
|
653
|
+
| controller / page | key | holds | history |
|
|
654
|
+
|---|---|---|---|
|
|
655
|
+
| `useFactoryBuilder` / `<FactoryBuilder>` | `<urlKey>Selected` | the id of the one selected control | opening or closing its properties adds an entry (Back closes them); moving to another control replaces it |
|
|
656
|
+
| `useFactoryBuilder` / `<FactoryBuilder>` | `<urlKey>Steps` | the step each stepper is open at: `stepperId:index`, comma-separated, step 0 left out | replaces |
|
|
657
|
+
| `useFactoryScreen` / `<FactoryScreen>`, `<FlowRunner>` | `<urlKey>Steps` | as above | replaces |
|
|
658
|
+
| `useFlowBuilder` / `<FlowBuilder>` | `<urlKey>Selected` | the key of the selected flow step | as for a control |
|
|
659
|
+
|
|
660
|
+
```jsx
|
|
661
|
+
<FactoryBuilder urlKey="form" document={doc} onSave={save} /> // ?formSelected=email&formSteps=stepper:1
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
- **The key is the host's, never a default.** A builder, its Preview and a list's Edit popup can share a page, and the host has keys of its own, so nothing is written to the URL without a `urlKey` — give each its own. **Without one the state stays in memory**, as before. The Preview and the Edit popup are never given one.
|
|
665
|
+
- **A multi-selection is not a place.** Several controls selected is a working set for Delete and Duplicate, not somewhere a person goes, so it stays in memory and the URL then names nothing. Only a single selection — the one whose properties are open — is in the URL.
|
|
666
|
+
- **Ids, found again.** The selected control is looked up in the document; one that is not there selects nothing. A document handed in after mounting keeps the URL's control, so a host that loads the screen late still lands on it. A step past the last is pulled back to the last, and steppers not in the document (a screen still loading) are left alone.
|
|
667
|
+
- The rule is in `src/urlState.js` (`factoryUrlKeys`, `encodeSteps`, `decodeSteps`, `replacesHistory`, `selectionPlace`); the hooks that keep it are `src/useUrlPlaces.js`.
|
|
668
|
+
|
|
489
669
|
## Three ways to use it
|
|
490
670
|
|
|
491
671
|
1. **Ready-made** — `<FactoryBuilder>` and `<FactoryScreen>`.
|
|
@@ -496,6 +676,8 @@ Layout is **proportional**, like `@xeplr/ui-canvas` dashboards: `x` and `w` are
|
|
|
496
676
|
|
|
497
677
|
Styles are namespaced `.xeplr-factory-*` and read `@xeplr/ui-account`'s theme variables (theme.css), each with a light fallback: `--xeplr-accent`, `--xeplr-accent-text`, `--xeplr-text-primary`, `--xeplr-text-secondary`, `--xeplr-border-primary`, `--xeplr-bg-primary` (the builder's plane), `--xeplr-bg-tertiary` (sheets, panels, popups), `--xeplr-bg-hover`, `--xeplr-bg-overlay`, `--xeplr-input-bg`, `--xeplr-input-border`, `--xeplr-success`, `--xeplr-warning`, `--xeplr-danger`. The screen, builder and popup follow the app's light or dark theme with the @xeplr/ui-table inside them — never a white sheet in a dark app.
|
|
498
678
|
|
|
679
|
+
Every dropdown the factory draws — on a screen, in the builder's panels, in the flow designer — has the factory's own ∨ arrow, set in from the right edge with room kept for it, in `--xeplr-text-secondary`. It is styled here, inside the factory's own containers, so an app gets it without adding CSS of its own. A dropdown's **Background** is applied as a background colour, so it never removes the arrow.
|
|
680
|
+
|
|
499
681
|
Buttons, steps and list actions all show hover, a visible press, and a keyboard-only focus ring.
|
|
500
682
|
|
|
501
683
|
## Try it
|
|
@@ -523,6 +705,9 @@ src/
|
|
|
523
705
|
model.js ─ everything above, React-free
|
|
524
706
|
useFactoryBuilder.js ─ builder controller
|
|
525
707
|
useFactoryScreen.js ─ screen controller
|
|
708
|
+
useFlowBuilder.js ─ flow designer controller
|
|
709
|
+
urlState.js ─ the URL keys and values for selection and steps (React-free)
|
|
710
|
+
useUrlPlaces.js ─ useUrlSteps, useUrlSelection: those places, in the URL or in memory
|
|
526
711
|
useScreenSource.js ─ { screens, screenEditor } for @xeplr/ui-workflow's Screen step
|
|
527
712
|
designs/ ─ BuilderSample, ScreenSample, ScreenEditorSample, ControlView, ListView, Palette, PropertyPanel, styles.js, factory.css
|
|
528
713
|
pages.jsx ─ FactoryBuilder, FactoryScreen
|
|
@@ -537,6 +722,8 @@ dev/ ─ the stand-in app for `npm run dev` (not published)
|
|
|
537
722
|
npm test
|
|
538
723
|
```
|
|
539
724
|
|
|
725
|
+
`test/urlState.test.js` covers the URL state: the key format, and the three controllers reading their places back from a real `MemoryRouter` and being clicked through in memory, rendered with `react-dom/server`.
|
|
726
|
+
|
|
540
727
|
## License
|
|
541
728
|
|
|
542
729
|
MIT
|
package/bin/xeplr-factory.js
CHANGED
|
@@ -151,7 +151,8 @@ switch (cmd) {
|
|
|
151
151
|
Object.values(CONTROLS).forEach((c) => {
|
|
152
152
|
process.stdout.write(`${c.type.padEnd(9)} ${c.label}${c.input ? ` — value: ${c.valueType || 'option id'}` : ''}\n`)
|
|
153
153
|
process.stdout.write(` props: ${c.props.join(', ')}\n`)
|
|
154
|
-
|
|
154
|
+
// Not every control has rules to break — an identifier has none.
|
|
155
|
+
if (c.validation && c.validation.length) process.stdout.write(` validation: ${c.validation.join(', ')}\n`)
|
|
155
156
|
if (c.styles && c.styles.length) process.stdout.write(` style: ${c.styles.join(', ')}\n`)
|
|
156
157
|
})
|
|
157
158
|
break
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/ui-factory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Design data-entry screens on @xeplr/ui-canvas, save them as JSON, run them as auto-saving forms with lists of saved records — built for Claude to draft and people to refine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"@tanstack/react-table": "^8.0.0",
|
|
49
49
|
"@xeplr/ui-canvas": "^1.0.2",
|
|
50
50
|
"@xeplr/ui-table": "^1.0.6",
|
|
51
|
-
"react": "^18.0.0 || ^19.0.0"
|
|
51
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
52
|
+
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@tanstack/react-table": "^8.21.3",
|
|
@@ -58,6 +59,10 @@
|
|
|
58
59
|
"@xeplr/ui-table": "^1.0.6",
|
|
59
60
|
"react": "^19.2.4",
|
|
60
61
|
"react-dom": "^19.2.4",
|
|
62
|
+
"react-router-dom": "^7.18.4",
|
|
61
63
|
"vite": "^7.3.1"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@xeplr/ui-utils": "^1.1.2"
|
|
62
67
|
}
|
|
63
68
|
}
|
package/src/controls.js
CHANGED
|
@@ -99,6 +99,8 @@ const FIELD_BASICS = (extra) => ({
|
|
|
99
99
|
]
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
+
|
|
103
|
+
|
|
102
104
|
const PLACEHOLDER = { path: 'props.placeholder', label: 'Placeholder', type: 'text' }
|
|
103
105
|
|
|
104
106
|
/**
|
|
@@ -237,6 +239,57 @@ export const CONTROLS = {
|
|
|
237
239
|
]
|
|
238
240
|
},
|
|
239
241
|
|
|
242
|
+
/**
|
|
243
|
+
* AN IDENTIFIER — the number or code this record is known by.
|
|
244
|
+
*
|
|
245
|
+
* A TYPE, not a flag on a text box, because everything about it follows
|
|
246
|
+
* from choosing it: what the column is, who fills it in, and whether it is
|
|
247
|
+
* on the form at all. Drop it on, say how it is made, and the rest is
|
|
248
|
+
* decided.
|
|
249
|
+
*
|
|
250
|
+
* auto the database counts — 1, 2, 3. Nobody types it and nobody sees
|
|
251
|
+
* it while the record is new, because it does not exist yet: the
|
|
252
|
+
* database assigns it as the row is inserted. Not drawn on a live
|
|
253
|
+
* form at all.
|
|
254
|
+
* code the server assigns CMP-7K3QF9 on the first save. Shown, because
|
|
255
|
+
* this is the one people quote to each other — and read-only,
|
|
256
|
+
* because changing it later would break every quote of it.
|
|
257
|
+
*
|
|
258
|
+
* The record's own 25-character key is untouched and still what everything
|
|
259
|
+
* points at. This is the identifier PEOPLE use.
|
|
260
|
+
*/
|
|
261
|
+
id: {
|
|
262
|
+
type: 'id',
|
|
263
|
+
label: 'Identifier',
|
|
264
|
+
group: 'Inputs',
|
|
265
|
+
input: true,
|
|
266
|
+
valueType: 'string',
|
|
267
|
+
defaultSize: { w: 0.28, h: 0.08 },
|
|
268
|
+
defaults: { label: 'Reference', idKind: 'code' },
|
|
269
|
+
props: ['name', 'label', 'idKind', 'codePrefix', 'style'],
|
|
270
|
+
// Nothing to break: nobody types an identifier, so there is no rule it
|
|
271
|
+
// could fail. Declared anyway, because every control answers the same
|
|
272
|
+
// questions (see the contract in test/model.test.js).
|
|
273
|
+
validation: [],
|
|
274
|
+
styles: INPUT_STYLES,
|
|
275
|
+
properties: [
|
|
276
|
+
{
|
|
277
|
+
key: 'field',
|
|
278
|
+
title: 'Field',
|
|
279
|
+
fields: [
|
|
280
|
+
{ path: 'props.label', label: 'Label', type: 'text' },
|
|
281
|
+
{ path: 'props.name', label: 'Field name', type: 'text', help: 'The key this value is saved under' },
|
|
282
|
+
{ path: 'props.idKind', label: 'How it is made', type: 'select', options: [
|
|
283
|
+
{ value: 'code', label: 'A code — CMP-7K3QF9' },
|
|
284
|
+
{ value: 'auto', label: 'A number, counted by the database — 1, 2, 3' }
|
|
285
|
+
], help: 'A counted number is never shown on the form: it exists only once the row does.' },
|
|
286
|
+
{ path: 'props.codePrefix', label: 'Starts with', type: 'text', help: 'CMP → CMP-7K3QF9. Codes only, and optional.' }
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
...styleGroups(INPUT_STYLES)
|
|
290
|
+
]
|
|
291
|
+
},
|
|
292
|
+
|
|
240
293
|
date: {
|
|
241
294
|
type: 'date',
|
|
242
295
|
label: 'Date',
|
|
@@ -488,7 +541,7 @@ CONTROLS.list = {
|
|
|
488
541
|
// PAGE of the app's own (openIn: 'page', which calls onOpenRecord and lets
|
|
489
542
|
// the app navigate). Without one, Edit opens the row in this screen's own
|
|
490
543
|
// fields (a list on a form).
|
|
491
|
-
props: ['title', 'source', 'editScreen', 'openIn', 'columns', 'pageSize', 'actions', 'style'],
|
|
544
|
+
props: ['title', 'source', 'belongsTo', 'editScreen', 'openIn', 'columns', 'pageSize', 'actions', 'style'],
|
|
492
545
|
validation: [],
|
|
493
546
|
styles: ['fontFamily', 'fontSize', 'color', 'background', 'borderColor', 'borderWidth', 'borderRadius'],
|
|
494
547
|
properties: [
|
|
@@ -497,7 +550,14 @@ CONTROLS.list = {
|
|
|
497
550
|
title: 'List',
|
|
498
551
|
fields: [
|
|
499
552
|
{ path: 'props.title', label: 'Title', type: 'text' },
|
|
500
|
-
{ path: 'props.source', label: 'Records from', type: '
|
|
553
|
+
{ path: 'props.source', label: 'Records from', type: 'listTable', help: 'Blank → the table this screen saves to' },
|
|
554
|
+
// THE LINK BACK. A sub-table on a form — this material's
|
|
555
|
+
// characteristics, this order's lines — is rows of another table that
|
|
556
|
+
// belong to THIS record. Without it a list of another table shows
|
|
557
|
+
// every row in the app, and New saves a row belonging to nothing.
|
|
558
|
+
{ path: 'props.belongsTo', label: 'Belonging to', type: 'text',
|
|
559
|
+
help: 'The column on those records that points back at this one, e.g. materialId. ' +
|
|
560
|
+
'It is filled in automatically on New and never shown.' },
|
|
501
561
|
{ path: 'props.editScreen', label: 'Edit in', type: 'screen', help: 'The screen Edit and New open' },
|
|
502
562
|
{ path: 'props.openIn', label: 'Opens in', type: 'select', options: [
|
|
503
563
|
{ value: 'popup', label: 'A popup' },
|
|
@@ -3,6 +3,8 @@ import { XeplrCanvas } from '@xeplr/ui-canvas'
|
|
|
3
3
|
import ControlView from './ControlView.jsx'
|
|
4
4
|
import Palette, { DRAG_TYPE } from './Palette.jsx'
|
|
5
5
|
import PropertyPanel from './PropertyPanel.jsx'
|
|
6
|
+
import { ScreenEditorSample } from './ScreenEditorSample.jsx'
|
|
7
|
+
import { useFactoryBuilder } from '../useFactoryBuilder.js'
|
|
6
8
|
import { screenStyle } from './styles.js'
|
|
7
9
|
import { presetFor } from '../presets.js'
|
|
8
10
|
|
|
@@ -118,6 +120,37 @@ export default function BuilderSample({ ctrl, renderPreview, className, style })
|
|
|
118
120
|
)}
|
|
119
121
|
</header>
|
|
120
122
|
|
|
123
|
+
{/* THE CHILD, DESIGNED WHERE IT WAS MADE.
|
|
124
|
+
A sub-table is made for the fields it will hold, and the two it is
|
|
125
|
+
created with are a guess at those. Going to find the new form
|
|
126
|
+
afterwards means going with the thought already lost. Publishing in
|
|
127
|
+
here sets this list's columns and closes it. */}
|
|
128
|
+
{ctrl.designingChild && screenApi && (
|
|
129
|
+
<div className="xeplr-factory-modal-backdrop" onMouseDown={() => ctrl.closeChild(null)}>
|
|
130
|
+
<div
|
|
131
|
+
className="xeplr-factory-modal xeplr-factory-modal--wide"
|
|
132
|
+
role="dialog"
|
|
133
|
+
aria-modal="true"
|
|
134
|
+
aria-label={'Design ' + ctrl.designingChild.name}
|
|
135
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
136
|
+
>
|
|
137
|
+
<header className="xeplr-factory-modal-head">
|
|
138
|
+
<strong>{ctrl.designingChild.name}</strong>
|
|
139
|
+
<span className="xeplr-factory-modal-spacer" />
|
|
140
|
+
<button type="button" className="xeplr-factory-secondary" onClick={() => ctrl.closeChild(null)}>Done</button>
|
|
141
|
+
</header>
|
|
142
|
+
<div className="xeplr-factory-modal-body">
|
|
143
|
+
<ScreenEditorSample
|
|
144
|
+
api={screenApi}
|
|
145
|
+
Builder={NestedBuilder}
|
|
146
|
+
screenKey={ctrl.designingChild.screenKey}
|
|
147
|
+
onDone={(screen) => ctrl.closeChild(screen)}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
121
154
|
{ctrl.publishResult && ctrl.publishResult.confirm && (
|
|
122
155
|
<div className="xeplr-factory-publish is-confirm" role="alertdialog" aria-label="Confirm changes to saved data">
|
|
123
156
|
<div className="xeplr-factory-confirm-text">
|
|
@@ -240,7 +273,20 @@ export default function BuilderSample({ ctrl, renderPreview, className, style })
|
|
|
240
273
|
tables={ctrl.tables}
|
|
241
274
|
screens={ctrl.screens}
|
|
242
275
|
lockedNames={ctrl.lockedNames}
|
|
243
|
-
onChange={(path, value) =>
|
|
276
|
+
onChange={(path, value) => (
|
|
277
|
+
// A list's table is not an ordinary property: changing it makes
|
|
278
|
+
// the chosen columns and the link meaningless, and leaving them
|
|
279
|
+
// behind is what reads as the panel not noticing. See
|
|
280
|
+
// setListSource.
|
|
281
|
+
ctrl.selectedNode.type === 'list' && path === 'props.source'
|
|
282
|
+
? ctrl.setListSource(ctrl.selectedNode.id, value)
|
|
283
|
+
: ctrl.setProperty(ctrl.selectedNode.id, path, value)
|
|
284
|
+
)}
|
|
285
|
+
// A list pointed at a table that does not exist yet can make one
|
|
286
|
+
// — with the link back to this screen's record already on it —
|
|
287
|
+
// so nobody has to leave the designer, go and build a form, and
|
|
288
|
+
// come back to remember what they were doing.
|
|
289
|
+
onMakeChildTable={ctrl.makeChildTable}
|
|
244
290
|
onConvert={(target) => ctrl.convertControl(ctrl.selectedNode.id, target)}
|
|
245
291
|
hint={ctrl.selectedNode ? ctrl.hints.find((h) => h.nodeId === ctrl.selectedNode.id) : null}
|
|
246
292
|
onScreenChange={ctrl.setScreen}
|
|
@@ -73,6 +73,41 @@ const TEXT_INPUT = {
|
|
|
73
73
|
url: { type: 'url', autoComplete: 'url', inputMode: 'url' }
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* AN IDENTIFIER — the number or code this record is known by.
|
|
78
|
+
*
|
|
79
|
+
* Never typed, either way, so there is nothing to type into. What differs is
|
|
80
|
+
* whether it is on the form at all:
|
|
81
|
+
*
|
|
82
|
+
* code shown. This is the one people quote to each other, so they need to
|
|
83
|
+
* see it — read-only, because changing it later breaks every quote.
|
|
84
|
+
* auto NOT SHOWN on a live form. The database assigns it as the row is
|
|
85
|
+
* inserted, so on a new record there is nothing to show but an empty
|
|
86
|
+
* box, and an empty box that never fills in is a question the form
|
|
87
|
+
* appears to be asking and is not. Once the row exists it is worth
|
|
88
|
+
* seeing, so a saved record shows it.
|
|
89
|
+
*
|
|
90
|
+
* On the DESIGN canvas both are drawn, or there would be nothing to select
|
|
91
|
+
* and nothing to configure.
|
|
92
|
+
*/
|
|
93
|
+
function IdView({ node, p, value, design }) {
|
|
94
|
+
const counted = p.idKind === 'auto'
|
|
95
|
+
if (counted && !design && (value === undefined || value === null || value === '')) return null
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Field node={node} p={p}>
|
|
99
|
+
<input
|
|
100
|
+
className="xeplr-factory-input"
|
|
101
|
+
type="text"
|
|
102
|
+
value={value ?? ''}
|
|
103
|
+
readOnly
|
|
104
|
+
disabled
|
|
105
|
+
placeholder={counted ? 'Numbered when saved' : 'Given when saved'}
|
|
106
|
+
/>
|
|
107
|
+
</Field>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
76
111
|
function TextView({ node, p, value, error, onChange, onBlur, disabled }) {
|
|
77
112
|
const kind = TEXT_INPUT[p.inputType] || { type: 'text' }
|
|
78
113
|
return (
|
|
@@ -395,6 +430,7 @@ function ListControl({ node, design, list }) {
|
|
|
395
430
|
}
|
|
396
431
|
|
|
397
432
|
export const VIEWS = {
|
|
433
|
+
id: IdView,
|
|
398
434
|
text: TextView,
|
|
399
435
|
textarea: TextareaView,
|
|
400
436
|
number: NumberView,
|
package/src/designs/ListView.jsx
CHANGED
|
@@ -11,7 +11,9 @@ import { boxStyle, fieldStyle } from './styles.js'
|
|
|
11
11
|
|
|
12
12
|
export default function ListView({ node, doc, design, list, onEdit, onDelete, onNew, currentId, recordKey = 'id', canDelete, fieldNodes, optionsFor, extraActions = [] }) {
|
|
13
13
|
const p = node.props || {}
|
|
14
|
-
|
|
14
|
+
// The rows are passed in because a list of ANOTHER table has no other
|
|
15
|
+
// description of itself — see listColumns.
|
|
16
|
+
const columns = listColumns(doc, node, undefined, (list && list.rows) || [])
|
|
15
17
|
const actions = p.actions || ['new', 'edit', 'delete']
|
|
16
18
|
const style = { ...fieldStyle(p.style), ...boxStyle(p.style) }
|
|
17
19
|
|