@xeplr/ui-factory 1.7.0 → 1.7.2
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 +4 -4
- package/README.md +11 -4
- package/bin/xeplr-factory.js +4 -4
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/src/pages.jsx +51 -1
- package/src/scaffold.js +36 -40
package/AUTHORING.md
CHANGED
|
@@ -15,7 +15,7 @@ A request for "a form for employees" is an **entity**. Every entity is the same
|
|
|
15
15
|
| `employee-edit.screen.json` | the **add / edit form** the list opens in a popup | browser |
|
|
16
16
|
| `EditEmployee.jsx` | the form's page, holding the **front-end hooks** class (`EmployeeHooks extends FactoryHooks`) | browser |
|
|
17
17
|
| `EmployeeList.jsx` | the list's page — uses `EditEmployee.jsx`'s hooks | browser |
|
|
18
|
-
| `employee.
|
|
18
|
+
| `employee.functions.js` | **server functions**, registered with `@xeplr/actions` by event (`beforeLoad`, `beforeSave`, `afterSave`, `beforeDelete`, `onError`) and named in the screen's `events` | server |
|
|
19
19
|
| `employee.model.js` | **server model**: `EmployeeModel extends FactoryModel` — getters / setters for values | server |
|
|
20
20
|
|
|
21
21
|
`npx xeplr-factory screens employee.entity.json -o <dir>` writes all of them from the spec (`--no-pages` leaves out the two `.jsx`). The hooks and the model start as defaults — every method calling `super` — and stay that way unless the request needs more.
|
|
@@ -26,7 +26,7 @@ A request for "a form for employees" is an **entity**. Every entity is the same
|
|
|
26
26
|
|---|---|
|
|
27
27
|
| a field, a label, a rule, a dropdown, the layout, a colour | the screen JSON (then validate) |
|
|
28
28
|
| "show / hide / filter rows on screen", "an extra button on each row", "fill in X before saving" (convenience) | front-end hooks in `EditEmployee.jsx` |
|
|
29
|
-
| "must", "only if", "check against…", "email when…", "only managers see…" | server
|
|
29
|
+
| "must", "only if", "check against…", "email when…", "only managers see…" | a server function in `employee.functions.js`, named in the screen's `events` |
|
|
30
30
|
| "store as…", "convert…", "comma-separated / array", "work out X from Y" (data shape) | server model in `employee.model.js` |
|
|
31
31
|
| a query in your own server code | `factory.table('employees')` — this company, active rows, the model applied |
|
|
32
32
|
|
|
@@ -37,7 +37,7 @@ A request for "a form for employees" is an **entity**. Every entity is the same
|
|
|
37
37
|
```sh
|
|
38
38
|
npx xeplr-factory screens employee.entity.json -o src/screens/employee
|
|
39
39
|
```
|
|
40
|
-
This writes `employee-list.screen.json`, `employee-edit.screen.json`, `EmployeeList.jsx`, `EditEmployee.jsx` (which holds the front-end hooks — see below) and
|
|
40
|
+
This writes `employee-list.screen.json`, `employee-edit.screen.json`, `EmployeeList.jsx`, `EditEmployee.jsx` (which holds the front-end hooks — see below) and `employee.functions.js` (the server's functions, none registered yet). Wire the two pages into the app's menu next to two `<FactoryBuilder>` pages for designing them (Screen · List, Screen · Edit, Designer · List, Designer · Edit). It refuses to overwrite existing files — they may have been refined in the designer — unless given `--force`, which replaces the screen JSON only: the `.jsx` pages, the functions and the model are the app's code and are always kept.
|
|
41
41
|
|
|
42
42
|
A single screen that is not an entity (a settings form, say) uses `npx xeplr-factory generate spec.json -o screen.json` with the screen spec below.
|
|
43
43
|
3. **The table comes from Publish.** Records live in a **real table** — `employees`, one column per field — never in JSON, and there are no migration files to write: **publishing** the edit screen creates its table (`CREATE TABLE`, a column per field, real foreign keys for table dropdowns, the standard `id` / `isActive` / `mtId1–4` / audit columns), and publishing a later version changes it:
|
|
@@ -89,7 +89,7 @@ A request for "a form for employees" is an **entity**. Every entity is the same
|
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
Setters run on every write (after `save.before`, before the screen's rules), getters on every read, on the routes and in `factory.table()`. Register it: `factory.init({ knex, hooks, models: [require('./employee.model')] })`.
|
|
92
|
-
8. **Server
|
|
92
|
+
8. **Server functions, if the request needs them.** Anything beyond storing what the form shows — a value worked out on save, a check against other records, an email afterwards, a list narrowed to the user — is a function in `employee.functions.js`, never in the screen: `register({ name, event, description, execute })` with `@xeplr/actions`, then name it in `employee-edit.screen.json`'s `events` (`{ "beforeSave": "employeeBeforeSave" }`). Register before naming — a screen naming a function nobody registered is refused at publish. The app loads the file from its `functions/index.js`. The `screens` command never overwrites an existing functions file, even with `--force`. Details: the [`@xeplr/factory` README](https://www.npmjs.com/package/@xeplr/factory#events--the-apps-code-picked-by-name-in-the-screen).
|
|
93
93
|
9. **Hand over.** The person opens the screens in the designer to move, resize and restyle controls.
|
|
94
94
|
|
|
95
95
|
Do **not** add a button control for saving: every screen's footer already has **Save** (an AJAX call — never a form submit) and **Cancel** where it can go back. If the request mentions seeing or editing what was entered ("…and show the employees below"), add a `list`.
|
package/README.md
CHANGED
|
@@ -17,13 +17,13 @@ It is built to work with Claude: ask for *"a form for employees with name, email
|
|
|
17
17
|
npx xeplr-factory screens employee.entity.json -o src/screens/employee
|
|
18
18
|
# employee-list.screen.json EmployeeList.jsx the list page — uses EditEmployee.jsx's hooks
|
|
19
19
|
# employee-edit.screen.json EditEmployee.jsx the form page, holding the FRONT-END hooks (EmployeeHooks extends FactoryHooks)
|
|
20
|
-
# employee.
|
|
20
|
+
# employee.functions.js SERVER functions, registered by event, named in the screen's events
|
|
21
21
|
# employee.model.js SERVER model: EmployeeModel extends FactoryModel — getters / setters for values
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
`--no-pages` leaves out the two `.jsx` (for an app whose pages live elsewhere). `--force` replaces only the screen JSON: the pages, hooks and model are your code and are always kept.
|
|
25
25
|
|
|
26
|
-
**Rules:** records live in real tables, never JSON · a form's **key** (`employee`) names its screens and table and never changes once published; its **label** (the screens' name) is renamed any time · rules that must hold go in server
|
|
26
|
+
**Rules:** records live in real tables, never JSON · a form's **key** (`employee`) names its screens and table and never changes once published; its **label** (the screens' name) is renamed any time · rules that must hold go in server functions (named in the screen's `events`), never front-end hooks.
|
|
27
27
|
|
|
28
28
|
See [examples/](./examples) for exactly what it writes.
|
|
29
29
|
|
|
@@ -382,6 +382,13 @@ const flows = createFlowsApi({ fetch: authFetch, base: '/api/workflow' })
|
|
|
382
382
|
- Approvals (another person's turn, with an assignee and a timeout) are not
|
|
383
383
|
part of this designer.
|
|
384
384
|
|
|
385
|
+
An app page needs only its two API clients — the flow and the forms are
|
|
386
|
+
loaded for it:
|
|
387
|
+
|
|
388
|
+
```jsx
|
|
389
|
+
<FlowDesignerPage factory={factory} flows={flows} flowKey={key} urlKey="flow" />
|
|
390
|
+
```
|
|
391
|
+
|
|
385
392
|
Controllers, for a design of your own: `useFlowBuilder` (the flow, plus
|
|
386
393
|
`navigationOf` / `setNavigation`), `useScreenEvents` (one screen's events,
|
|
387
394
|
`adopt` to share its draft with a builder), `useFunctionCatalog` (the app's
|
|
@@ -724,7 +731,7 @@ const screen = screenFromSpec({
|
|
|
724
731
|
It lays the fields out as a tidy one- or two-column form and returns a checked document. The same is available on the command line:
|
|
725
732
|
|
|
726
733
|
```sh
|
|
727
|
-
npx xeplr-factory screens entity.json -o dir # an entity → list + edit screens, pages, server
|
|
734
|
+
npx xeplr-factory screens entity.json -o dir # an entity → list + edit screens, pages, server functions + model (--no-pages, --force)
|
|
728
735
|
npx xeplr-factory migration edit.screen.json # preview the SQL Publish would run
|
|
729
736
|
npx xeplr-factory generate spec.json -o screen.json # one screen from a spec
|
|
730
737
|
npx xeplr-factory validate screen.json # every problem, with its path
|
|
@@ -800,7 +807,7 @@ src/
|
|
|
800
807
|
validateDocument.js ─ the document checker
|
|
801
808
|
values.js ─ values, validation, whether Save can go out, records and list columns, formSchema
|
|
802
809
|
generate.js ─ screenFromSpec, screensFromSpec (list + edit), entity names
|
|
803
|
-
scaffold.js ─ an entity → its screen JSON, .jsx pages (with front-end hooks), server
|
|
810
|
+
scaffold.js ─ an entity → its screen JSON, .jsx pages (with front-end hooks), server functions + model stubs
|
|
804
811
|
hooks.js ─ FactoryHooks: get / save / delete / actions — extend and call super
|
|
805
812
|
presets.js ─ ready-made fields (Email, Age, Gender…), changing a field's kind, label suggestions
|
|
806
813
|
tableSchema.js ─ a form → its table; the publish plan (create / add / widen / confirmed drops and conversions)
|
package/bin/xeplr-factory.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// turn a short spec into a laid-out document, and check a document before
|
|
6
6
|
// handing it to an app, with errors precise enough to fix without guessing.
|
|
7
7
|
//
|
|
8
|
-
// xeplr-factory screens entity.json [-o dir] an entity → list + edit screens, their .jsx pages, server
|
|
8
|
+
// xeplr-factory screens entity.json [-o dir] an entity → list + edit screens, their .jsx pages, server functions + model stubs
|
|
9
9
|
// xeplr-factory migration edit.screen.json [--from previous.screen.json] [-o migrations/]
|
|
10
10
|
// the SQL that makes the table match the form
|
|
11
11
|
// xeplr-factory generate spec.json [-o screen.json] spec → laid-out document
|
|
@@ -81,11 +81,11 @@ switch (cmd) {
|
|
|
81
81
|
} catch (err) {
|
|
82
82
|
fail(err.message)
|
|
83
83
|
}
|
|
84
|
-
// The app's own code — the server
|
|
85
|
-
// hooks — is kept as it is, even with --force. Only the screen JSON is replaced.
|
|
84
|
+
// The app's own code — the server functions and model, and the pages with
|
|
85
|
+
// their front-end hooks — is kept as it is, even with --force. Only the screen JSON is replaced.
|
|
86
86
|
// --no-pages: only the screens and the server files — for an app whose pages live elsewhere (ui/src/pages).
|
|
87
87
|
if (args.includes('--no-pages')) Object.keys(result.files).filter((f) => f.endsWith('.jsx')).forEach((f) => delete result.files[f])
|
|
88
|
-
const kept = Object.keys(result.files).filter((f) => (f.endsWith('.
|
|
88
|
+
const kept = Object.keys(result.files).filter((f) => (f.endsWith('.functions.js') || f.endsWith('.model.js') || f.endsWith('.jsx')) && existsSync(path.join(dir, f)))
|
|
89
89
|
const writing = Object.keys(result.files).filter((f) => !kept.includes(f))
|
|
90
90
|
const targets = writing.map((f) => path.join(dir, f))
|
|
91
91
|
// Never overwrite a screen someone has since refined in the designer.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/ui-factory",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
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",
|
package/src/index.js
CHANGED
|
@@ -31,4 +31,4 @@ export { BuilderSample, ScreenSample, ListView, ScreenModal, FlowRunnerSample, F
|
|
|
31
31
|
export { FactoryHooks } from './hooks.js'
|
|
32
32
|
|
|
33
33
|
// Ready-made
|
|
34
|
-
export { FactoryBuilder, FactoryScreen, FlowRunner, FlowBuilder, FlowDesigner } from './pages.jsx'
|
|
34
|
+
export { FactoryBuilder, FactoryScreen, FlowRunner, FlowBuilder, FlowDesigner, FlowDesignerPage } from './pages.jsx'
|
package/src/pages.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef } from 'react'
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { useFactoryBuilder } from './useFactoryBuilder.js'
|
|
3
3
|
import { useFactoryScreen } from './useFactoryScreen.js'
|
|
4
4
|
import { useFlowRun } from './useFlowRun.js'
|
|
@@ -6,6 +6,7 @@ import { useFlowBuilder } from './useFlowBuilder.js'
|
|
|
6
6
|
import { useScreenEvents } from './useScreenEvents.js'
|
|
7
7
|
import { useFunctionCatalog } from './useFunctionCatalog.js'
|
|
8
8
|
import { useUrlTab } from './useUrlPlaces.js'
|
|
9
|
+
import { loadScreens } from './screenSource.js'
|
|
9
10
|
import { BuilderSample, ScreenSample, ScreenModal, FlowRunnerSample, FlowBuilderSample, FlowDesignerSample } from './designs/index.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -269,3 +270,52 @@ export function FlowDesigner(props) {
|
|
|
269
270
|
/>
|
|
270
271
|
)
|
|
271
272
|
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* <FlowDesigner>, wired to an app's two API clients — what an app's
|
|
276
|
+
* /configure/flows/:key page renders, so the wiring lives here and not, copied
|
|
277
|
+
* and drifting, in every generated app.
|
|
278
|
+
*
|
|
279
|
+
* Loads the flow and the app's FORMS (screens with fields of their own — a
|
|
280
|
+
* step shows one to be filled in) and hands everything else straight on.
|
|
281
|
+
*
|
|
282
|
+
* @prop factory createFactoryApi(…) — screens, drafts, publish, the app's functions
|
|
283
|
+
* @prop flows createFlowsApi(…) — load, save, publish the flow
|
|
284
|
+
* @prop flowKey the flow to open
|
|
285
|
+
* @prop urlKey keep the selected step and tab in the URL
|
|
286
|
+
*/
|
|
287
|
+
export function FlowDesignerPage({ factory, flows, flowKey, urlKey, className, style }) {
|
|
288
|
+
const [flow, setFlow] = useState(null)
|
|
289
|
+
const [screens, setScreens] = useState(null)
|
|
290
|
+
const [error, setError] = useState(null)
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
let gone = false
|
|
293
|
+
setFlow(null); setError(null)
|
|
294
|
+
Promise.all([flows.loadFlow(flowKey), loadScreens(factory)]).then(([f, list]) => {
|
|
295
|
+
if (gone) return
|
|
296
|
+
setFlow(f)
|
|
297
|
+
setScreens(list.filter((s) => s.fields && s.fields.length).map((s) => ({ id: s.key, name: s.name })))
|
|
298
|
+
}, (e) => { if (!gone) setError(e.message || `Could not open "${flowKey}"`) })
|
|
299
|
+
return () => { gone = true }
|
|
300
|
+
}, [factory, flows, flowKey])
|
|
301
|
+
|
|
302
|
+
if (error) return <div className="xeplr-factory-invalid" role="alert">{error}</div>
|
|
303
|
+
if (!flow || !screens) return <div className="xeplr-factory-hint">Opening the flow…</div>
|
|
304
|
+
return (
|
|
305
|
+
<FlowDesigner
|
|
306
|
+
flow={flow}
|
|
307
|
+
screens={screens}
|
|
308
|
+
loadScreen={factory.loadScreen}
|
|
309
|
+
onSave={flows.saveFlow}
|
|
310
|
+
onPublish={flows.publishFlow}
|
|
311
|
+
listFunctions={factory.listFunctions}
|
|
312
|
+
loadDraft={factory.loadDraft}
|
|
313
|
+
saveDraft={factory.saveDraft}
|
|
314
|
+
publishScreen={factory.publish}
|
|
315
|
+
builderProps={factory.builderProps}
|
|
316
|
+
urlKey={urlKey}
|
|
317
|
+
className={className}
|
|
318
|
+
style={style}
|
|
319
|
+
/>
|
|
320
|
+
)
|
|
321
|
+
}
|
package/src/scaffold.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// employee-list.screen.json EmployeeList.jsx — the list; Edit / New open a popup
|
|
7
7
|
// employee-edit.screen.json EditEmployee.jsx — the add / edit form, also a page on its own
|
|
8
|
-
// employee.
|
|
8
|
+
// employee.functions.js — the code its events can run, registered by event (none yet)
|
|
9
9
|
// employee.model.js — the server's model: getters / setters for its values (none yet)
|
|
10
10
|
//
|
|
11
11
|
// The pages are thin on purpose: a screen document and the app's data calls,
|
|
@@ -31,7 +31,7 @@ export function scaffoldEntity(spec, controls) {
|
|
|
31
31
|
[editJson]: JSON.stringify(edit, null, 2) + '\n',
|
|
32
32
|
[`${names.listComponent}.jsx`]: listPage(names, listJson, editJson, list, edit),
|
|
33
33
|
[`${names.editComponent}.jsx`]: editPage(names, editJson, edit),
|
|
34
|
-
[`${names.file}.
|
|
34
|
+
[`${names.file}.functions.js`]: functionsFile(names, edit),
|
|
35
35
|
[`${names.file}.model.js`]: modelFile(names, edit)
|
|
36
36
|
}
|
|
37
37
|
return { files, names, list, edit }
|
|
@@ -66,44 +66,40 @@ export default function ${n.listComponent}({ api }) {
|
|
|
66
66
|
`
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function
|
|
70
|
-
return `//
|
|
69
|
+
function functionsFile(n, edit) {
|
|
70
|
+
return `// Functions for ${n.singular} records — the code the "${edit.id}" screen's events run.
|
|
71
71
|
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
72
|
+
// Each is registered with @xeplr/actions under the EVENT it is for, and runs
|
|
73
|
+
// once the screen NAMES it — in ${n.file}-edit.screen.json's "events", or in the
|
|
74
|
+
// designer (Configure UI → Flows → a step → Events). A screen naming a function
|
|
75
|
+
// nobody registered is refused when it is published, so register first.
|
|
74
76
|
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// against the screen's rules); get: narrow ctx.query; any: ctx.reject(message, { field })
|
|
78
|
-
// after(ctx) once it succeeded — ctx.id, ctx.result; return a value to replace the result.
|
|
79
|
-
// If it throws, the operation still happened (and error is told).
|
|
80
|
-
// error(ctx, err) when the operation failed
|
|
81
|
-
// override(ctx) does the WHOLE operation instead — no rules, no before / after / error,
|
|
82
|
-
// no generic query. What it returns is the response.
|
|
77
|
+
// Load this file once at startup, before the screens: require it from the
|
|
78
|
+
// app's functions/index.js.
|
|
83
79
|
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
80
|
+
// event runs return
|
|
81
|
+
// beforeLoad before records are read nothing, or a narrower ctx.query
|
|
82
|
+
// beforeSave before a record is written the values to save
|
|
83
|
+
// afterSave once it is saved (ignored)
|
|
84
|
+
// beforeDelete before a delete nothing
|
|
85
|
+
// onError when load / save / delete failed (ignored)
|
|
86
|
+
//
|
|
87
|
+
// ctx: op, screenKey, screen, table, id, isNew (save), many (get), input (what the
|
|
88
|
+
// UI sent, read-only), values (save), previous (save / delete: the row
|
|
89
|
+
// before), result (afterSave), error (onError), query (beforeLoad), user,
|
|
90
|
+
// tenant, knex, db() (this form's table, scoped), reject(message, { field })
|
|
91
|
+
|
|
92
|
+
var { register } = require('@xeplr/actions');
|
|
93
|
+
|
|
94
|
+
// register({
|
|
95
|
+
// name: '${n.key}BeforeSave',
|
|
96
|
+
// event: 'beforeSave',
|
|
97
|
+
// description: 'What it checks or changes, in one sentence — the designer shows it.',
|
|
98
|
+
// execute: function(ctx) {
|
|
99
|
+
// // ctx.reject('Explain what is wrong', { field: 'someField' })
|
|
100
|
+
// return ctx.values;
|
|
101
|
+
// }
|
|
102
|
+
// });
|
|
107
103
|
`
|
|
108
104
|
}
|
|
109
105
|
|
|
@@ -111,10 +107,10 @@ function modelFile(n, edit) {
|
|
|
111
107
|
return `// The model for ${n.singular} records — how values are shaped between your code and
|
|
112
108
|
// the "${edit.source}" table, like Sequelize's getters and setters.
|
|
113
109
|
//
|
|
114
|
-
// Register with @xeplr/factory: factory.init({ knex,
|
|
110
|
+
// Register with @xeplr/factory: factory.init({ knex, models: [require('./${n.file}.model')] })
|
|
115
111
|
//
|
|
116
112
|
// Used everywhere: the screens' routes and factory.table('${edit.source}'). Models shape
|
|
117
|
-
// data; the
|
|
113
|
+
// data; the functions its events name decide behaviour.
|
|
118
114
|
|
|
119
115
|
var { FactoryModel } = require('@xeplr/factory');
|
|
120
116
|
|
|
@@ -159,7 +155,7 @@ import editScreen from './${editJson}'
|
|
|
159
155
|
//
|
|
160
156
|
// ctx.screen says which screen is calling ("${n.key}_list" or "${edit.id}").
|
|
161
157
|
// Keep them as methods (save(values, ctx) { … }) — super does not work in
|
|
162
|
-
// arrow functions. Rules that must hold belong in the server's
|
|
158
|
+
// arrow functions. Rules that must hold belong in the server's functions (${n.file}.functions.js).
|
|
163
159
|
export class ${n.hooksClass} extends FactoryHooks {
|
|
164
160
|
/** A list's rows (ctx.many), or the record Edit opens (ctx.id). */
|
|
165
161
|
get(ctx) {
|