@xeplr/cli 0.1.8 → 0.1.10
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/package.json +1 -1
- package/templates/CLAUDE.md +13 -3
- package/templates/README.md +1 -1
- package/templates/api/README.md +1 -0
- package/templates/api/bin/www +9 -1
- package/templates/api/development.env.tpl +6 -0
- package/templates/api/gitignore.tpl +2 -0
- package/templates/api/package.json +5 -4
- package/templates/api/screens/task/task-edit.screen.json +186 -16
- package/templates/api/screens/task/task-list.screen.json +9 -0
- package/templates/api/screens/task/task.entity.json +86 -5
- package/templates/ui/README.md +1 -1
- package/templates/ui/package.json +2 -2
- package/templates/ui/src/App.jsx +4 -0
- package/templates/ui/src/pages/EditTask.jsx +48 -2
- package/templates/ui/src/pages/Tasks.jsx +15 -3
- package/templates/ui/src/pages/TasksHelp.jsx +13 -5
package/package.json
CHANGED
package/templates/CLAUDE.md
CHANGED
|
@@ -7,7 +7,7 @@ Read `api/node_modules/@xeplr/ui-factory/AUTHORING.md` for what screens, hooks a
|
|
|
7
7
|
|
|
8
8
|
## Create a new UI
|
|
9
9
|
|
|
10
|
-
A new UI is a **form**: a list, and an add / edit form it opens in a popup
|
|
10
|
+
A new UI is a **form**: a list, and an add / edit form it opens — on a page of its own (like Tasks) or in a popup — saved in its own table. Use the singular for `<form>` (`farming_department`), the plural for the page (`FarmingDepartments`).
|
|
11
11
|
|
|
12
12
|
Create every file below — the hooks and the model run the defaults (`super`) until the request needs more.
|
|
13
13
|
|
|
@@ -32,18 +32,28 @@ Then change only what the request asks for:
|
|
|
32
32
|
| the request says… | change |
|
|
33
33
|
|---|---|
|
|
34
34
|
| a field, label, rule, dropdown, layout, colour | the screen JSON → validate → publish in Configure UI → Forms (or, never published yet, restart the API) |
|
|
35
|
-
| hide / filter rows on screen, an extra button on each row, fill in a value before saving | front-end hooks — `ui/src/pages/Edit<Form>.jsx` |
|
|
35
|
+
| hide / filter rows on screen, an extra button on each row, fill in a value before saving, check something before Next moves on | front-end hooks — `ui/src/pages/Edit<Form>.jsx` (`get`, `save`, `delete`, `actions`, `step`) |
|
|
36
36
|
| must / only if / check against / email when / only managers see | server hooks — `api/screens/<form>/<form>.hooks.js` |
|
|
37
37
|
| store as / convert / comma-separated ↔ array / work out X from Y | server model — `api/screens/<form>/<form>.model.js` |
|
|
38
38
|
| a query in your own server code | `factory.table('<forms table>')` — this company, active rows, the model applied |
|
|
39
39
|
|
|
40
40
|
**Without code:** Super Admin can also make a form in **Configure UI → Forms** (settings menu, top right): New form (label + key) → design → Publish → Add to menu. It gets no page, hooks or model files; add them with the steps above when it needs them.
|
|
41
41
|
|
|
42
|
+
### The controls a field can be
|
|
43
|
+
|
|
44
|
+
`text` (default), `textarea`, `number`, `date`, `datetime`, `checkbox`, `dropdown`, `radio`, `multiselect`, `file`, plus `stepper` and `label`. The sample uses every one — read `api/screens/task/task.entity.json` for how each is written. Three are worth knowing about:
|
|
45
|
+
|
|
46
|
+
- **`multiselect`** — several ids in ONE text column, comma separated, and an array in your code. An option's id may not contain a comma.
|
|
47
|
+
- **`file`** — the column holds the **path**, never the file. Say which extensions (`"accept": ".pdf,.docx"`) and how large (`"maxSize": 10`, in MB); the API enforces both, by extension, and keeps the file under `FACTORY_FILES_DIR`. Uploading needs `multer` in `api/` (already in package.json).
|
|
48
|
+
- **`stepper`** — a journey across the top. Every other field says which step it is on (`"step": "Planning"`), and one without a `step` shows on every step. `step(ctx)` in the front-end hooks runs before each move: return `false` to stay, a step's key to go there, `ctx.disable(['planning'])` to rule steps out (struck through on the bar, passed over by Next and Back) and `ctx.enable()` to bring them back.
|
|
49
|
+
|
|
50
|
+
**A list opens its form on a page or in a popup** — `"openIn": "page"` in the entity spec, or Configure UI → Forms → the list. A page needs `onOpenRecord` on the list's page (where to navigate) and `onDone` on the form's page (where Done goes back to): `Tasks.jsx` and `EditTask.jsx` show both, with routes `/tasks/new` and `/tasks/:id` in `App.jsx`.
|
|
51
|
+
|
|
42
52
|
**Keys and labels.** A form's key names its screens and table and never changes once published; its label is the screens' name. A menu item's key (`menus.name`) is what `drawerItems` matches; its label is what the rail shows, renamed in Configure UI → Menu. Code only ever uses keys.
|
|
43
53
|
|
|
44
54
|
### The prompt
|
|
45
55
|
|
|
46
|
-
> Create a new UI for **farming departments**. One record is a farming department with: **Name** (required), **Region** (dropdown: North, South, East, West), **Area in acres** (number, at least 0), **Started on** (date), **Notes** (long text). Show Name, Region and Area in the list. Follow "Create a new UI" in CLAUDE.md.
|
|
56
|
+
> Create a new UI for **farming departments**. One record is a farming department with: **Name** (required), **Region** (dropdown: North, South, East, West), **Area in acres** (number, at least 0), **Started on** (date), **Certifications** (choose several: Organic, Fair trade, Rainforest), **Inspection at** (date and time), **Licence** (file: .pdf or .jpg), **Notes** (long text). Show Name, Region and Area in the list. Follow "Create a new UI" in CLAUDE.md.
|
|
47
57
|
|
|
48
58
|
Replace the bold parts. Add behaviour in the same message if you want it — "a Mark active button on each row", "Region is required when Area is over 100", "store tags as an array".
|
|
49
59
|
|
package/templates/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Open http://localhost:__UI_PORT__ and sign in as `__ADMIN_EMAIL__`.
|
|
|
24
24
|
## What is in it
|
|
25
25
|
|
|
26
26
|
- **Sign-in** — login, register, password reset, profile, and the admin screens for users, roles and permissions (`@xeplr/auth`, `@xeplr/ui-account`).
|
|
27
|
-
- **Tasks** — a sample built from screens: a list, and a form it opens
|
|
27
|
+
- **Tasks** — a sample built from screens: a list, and a form it opens on a page of its own, saved into an ordinary `tasks` table. The form is filled in over three steps and uses every control there is — text, a dropdown, radio buttons, a tick box, a date, a date and time, several tags at once, and a file.
|
|
28
28
|
- **Configure UI** — in the settings menu, for Super Admin: every form (make one, design it, publish it, add it to the side rail) and the side rail itself (rename, reorder, hide).
|
|
29
29
|
|
|
30
30
|
## Create a new UI
|
package/templates/api/README.md
CHANGED
|
@@ -25,6 +25,7 @@ The API (`@xeplr/base-apis`) and the sign-in service (`@xeplr/auth`), with this
|
|
|
25
25
|
| `routes/index.js` | this app's own routes — every data address under `/api` |
|
|
26
26
|
| `routes/access.js` | who may design and publish forms (Super Admin) |
|
|
27
27
|
| `screens/` | the forms this app ships with: per form, its spec, list and edit screens, server hooks and model — registered in `screens/index.js` |
|
|
28
|
+
| `uploads/` | what people attach through a form's **file** field (`FACTORY_FILES_DIR`). Never committed, and never served: a file comes back only through the API's own route, which checks the caller the way reading a record does |
|
|
28
29
|
| `migrations/` | hand-written SQL for tables no form owns |
|
|
29
30
|
| `migrations-auth/` | this app's rows in the sign-in database — menus (by key), permissions |
|
|
30
31
|
| `models/` | models for hand-written tables |
|
package/templates/api/bin/www
CHANGED
|
@@ -61,7 +61,15 @@ async function start() {
|
|
|
61
61
|
// The screens, their hooks and their models. A hooks file with a typo
|
|
62
62
|
// ("beforeSave") or a malformed model stops startup here, rather than
|
|
63
63
|
// silently never running.
|
|
64
|
-
|
|
64
|
+
// filesDir: where a form's file field puts what people upload. Nothing is
|
|
65
|
+
// served from it — a file comes back only through the factory's own route,
|
|
66
|
+
// which checks the caller the way reading a record does.
|
|
67
|
+
await factory.init({
|
|
68
|
+
knex: knex,
|
|
69
|
+
hooks: screens.hooks,
|
|
70
|
+
models: screens.models,
|
|
71
|
+
filesDir: process.env.FACTORY_FILES_DIR
|
|
72
|
+
});
|
|
65
73
|
|
|
66
74
|
// Screens never published yet are published now, creating their tables.
|
|
67
75
|
// Ones already published are left alone — Configure UI → Forms owns them.
|
|
@@ -57,6 +57,12 @@ AUTH_SUPER_ADMIN_PASSWORD=__ADMIN_PASSWORD__
|
|
|
57
57
|
AUTH_ACTIVATION_URL=http://localhost:__UI_PORT__/auth/activate
|
|
58
58
|
AUTH_INVITE_URL=http://localhost:__UI_PORT__/auth/accept-invite
|
|
59
59
|
|
|
60
|
+
# ── Uploaded files ──
|
|
61
|
+
# Where a form's FILE field puts what people upload. The path is what the
|
|
62
|
+
# record stores; the file itself is served only through the API's own route,
|
|
63
|
+
# which checks the caller the way reading a record does. Relative to api/.
|
|
64
|
+
FACTORY_FILES_DIR=./uploads/factory
|
|
65
|
+
|
|
60
66
|
# ── Email (optional) ──
|
|
61
67
|
# Without it sign-in works, but activation, invite and password-reset links
|
|
62
68
|
# cannot be sent. The sign-in service checks it at start and says so in its
|
|
@@ -20,14 +20,15 @@
|
|
|
20
20
|
"@xeplr/auth": "^1.1.4",
|
|
21
21
|
"@xeplr/base-apis": "^2.0.0",
|
|
22
22
|
"@xeplr/db": "^1.0.6",
|
|
23
|
-
"@xeplr/factory": "^1.0.
|
|
24
|
-
"@xeplr/ui-factory": "^1.0.
|
|
23
|
+
"@xeplr/factory": "^1.0.6",
|
|
24
|
+
"@xeplr/ui-factory": "^1.0.12",
|
|
25
25
|
"@xeplr/utils": "^1.0.11",
|
|
26
26
|
"dotenv": "^17.4.2",
|
|
27
|
-
"express": "^5.2.0"
|
|
27
|
+
"express": "^5.2.0",
|
|
28
|
+
"multer": "^2.0.2"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"dotenv-cli": "^8.0.0",
|
|
31
|
-
"@xeplr/cli": "^0.1.
|
|
32
|
+
"@xeplr/cli": "^0.1.8"
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -20,14 +20,40 @@
|
|
|
20
20
|
"variant": "heading"
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
|
+
{
|
|
24
|
+
"id": "stepper",
|
|
25
|
+
"type": "stepper",
|
|
26
|
+
"x": 0.04,
|
|
27
|
+
"y": 0.15,
|
|
28
|
+
"w": 0.92,
|
|
29
|
+
"h": 0.09,
|
|
30
|
+
"z": 2,
|
|
31
|
+
"props": {
|
|
32
|
+
"steps": [
|
|
33
|
+
{
|
|
34
|
+
"key": "the_task",
|
|
35
|
+
"label": "The task"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"key": "planning",
|
|
39
|
+
"label": "Planning"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"key": "attachment",
|
|
43
|
+
"label": "Attachment"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"showNumbers": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
23
49
|
{
|
|
24
50
|
"id": "title",
|
|
25
51
|
"type": "text",
|
|
26
52
|
"x": 0.04,
|
|
27
|
-
"y": 0.
|
|
28
|
-
"w": 0.
|
|
53
|
+
"y": 0.28,
|
|
54
|
+
"w": 0.92,
|
|
29
55
|
"h": 0.08,
|
|
30
|
-
"z":
|
|
56
|
+
"z": 3,
|
|
31
57
|
"props": {
|
|
32
58
|
"label": "Title",
|
|
33
59
|
"required": true,
|
|
@@ -35,16 +61,20 @@
|
|
|
35
61
|
"maxLength": 200
|
|
36
62
|
},
|
|
37
63
|
"name": "title"
|
|
64
|
+
},
|
|
65
|
+
"step": {
|
|
66
|
+
"of": "stepper",
|
|
67
|
+
"index": 0
|
|
38
68
|
}
|
|
39
69
|
},
|
|
40
70
|
{
|
|
41
71
|
"id": "status",
|
|
42
72
|
"type": "dropdown",
|
|
43
|
-
"x": 0.
|
|
44
|
-
"y": 0.
|
|
73
|
+
"x": 0.04,
|
|
74
|
+
"y": 0.4,
|
|
45
75
|
"w": 0.4475,
|
|
46
76
|
"h": 0.08,
|
|
47
|
-
"z":
|
|
77
|
+
"z": 4,
|
|
48
78
|
"props": {
|
|
49
79
|
"label": "Status",
|
|
50
80
|
"placeholder": "Select…",
|
|
@@ -67,35 +97,175 @@
|
|
|
67
97
|
},
|
|
68
98
|
"required": true,
|
|
69
99
|
"name": "status"
|
|
100
|
+
},
|
|
101
|
+
"step": {
|
|
102
|
+
"of": "stepper",
|
|
103
|
+
"index": 0
|
|
70
104
|
}
|
|
71
105
|
},
|
|
72
106
|
{
|
|
73
|
-
"id": "
|
|
74
|
-
"type": "
|
|
75
|
-
"x": 0.
|
|
76
|
-
"y": 0.
|
|
107
|
+
"id": "priority",
|
|
108
|
+
"type": "radio",
|
|
109
|
+
"x": 0.5125,
|
|
110
|
+
"y": 0.4,
|
|
77
111
|
"w": 0.4475,
|
|
78
|
-
"h": 0.
|
|
79
|
-
"z":
|
|
112
|
+
"h": 0.065,
|
|
113
|
+
"z": 5,
|
|
80
114
|
"props": {
|
|
81
|
-
"label": "
|
|
82
|
-
"
|
|
115
|
+
"label": "Priority",
|
|
116
|
+
"data": {
|
|
117
|
+
"source": "static",
|
|
118
|
+
"options": [
|
|
119
|
+
{
|
|
120
|
+
"id": "low",
|
|
121
|
+
"name": "Low"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "normal",
|
|
125
|
+
"name": "Normal"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"id": "high",
|
|
129
|
+
"name": "High"
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
"layout": "horizontal",
|
|
134
|
+
"name": "priority"
|
|
135
|
+
},
|
|
136
|
+
"step": {
|
|
137
|
+
"of": "stepper",
|
|
138
|
+
"index": 0
|
|
83
139
|
}
|
|
84
140
|
},
|
|
85
141
|
{
|
|
86
142
|
"id": "description",
|
|
87
143
|
"type": "textarea",
|
|
88
144
|
"x": 0.04,
|
|
89
|
-
"y": 0.
|
|
145
|
+
"y": 0.52,
|
|
90
146
|
"w": 0.92,
|
|
91
147
|
"h": 0.18,
|
|
92
|
-
"z":
|
|
148
|
+
"z": 6,
|
|
93
149
|
"props": {
|
|
94
150
|
"label": "Description",
|
|
95
151
|
"validation": {
|
|
96
152
|
"maxLength": 1000
|
|
97
153
|
},
|
|
98
154
|
"name": "description"
|
|
155
|
+
},
|
|
156
|
+
"step": {
|
|
157
|
+
"of": "stepper",
|
|
158
|
+
"index": 0
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"id": "dueDate",
|
|
163
|
+
"type": "date",
|
|
164
|
+
"x": 0.04,
|
|
165
|
+
"y": 0.28,
|
|
166
|
+
"w": 0.4475,
|
|
167
|
+
"h": 0.08,
|
|
168
|
+
"z": 7,
|
|
169
|
+
"props": {
|
|
170
|
+
"label": "Due date",
|
|
171
|
+
"name": "dueDate"
|
|
172
|
+
},
|
|
173
|
+
"step": {
|
|
174
|
+
"of": "stepper",
|
|
175
|
+
"index": 1
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"id": "remindAt",
|
|
180
|
+
"type": "datetime",
|
|
181
|
+
"x": 0.5125,
|
|
182
|
+
"y": 0.28,
|
|
183
|
+
"w": 0.4475,
|
|
184
|
+
"h": 0.08,
|
|
185
|
+
"z": 8,
|
|
186
|
+
"props": {
|
|
187
|
+
"label": "Remind at",
|
|
188
|
+
"name": "remindAt"
|
|
189
|
+
},
|
|
190
|
+
"step": {
|
|
191
|
+
"of": "stepper",
|
|
192
|
+
"index": 1
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"id": "tags",
|
|
197
|
+
"type": "multiselect",
|
|
198
|
+
"x": 0.04,
|
|
199
|
+
"y": 0.4,
|
|
200
|
+
"w": 0.92,
|
|
201
|
+
"h": 0.065,
|
|
202
|
+
"z": 9,
|
|
203
|
+
"props": {
|
|
204
|
+
"label": "Tags",
|
|
205
|
+
"data": {
|
|
206
|
+
"source": "static",
|
|
207
|
+
"options": [
|
|
208
|
+
{
|
|
209
|
+
"id": "bug",
|
|
210
|
+
"name": "Bug"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"id": "feature",
|
|
214
|
+
"name": "Feature"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"id": "chore",
|
|
218
|
+
"name": "Chore"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"id": "customer",
|
|
222
|
+
"name": "Customer"
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
},
|
|
226
|
+
"layout": "horizontal",
|
|
227
|
+
"name": "tags"
|
|
228
|
+
},
|
|
229
|
+
"step": {
|
|
230
|
+
"of": "stepper",
|
|
231
|
+
"index": 1
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"id": "blocked",
|
|
236
|
+
"type": "checkbox",
|
|
237
|
+
"x": 0.04,
|
|
238
|
+
"y": 0.505,
|
|
239
|
+
"w": 0.4475,
|
|
240
|
+
"h": 0.05,
|
|
241
|
+
"z": 10,
|
|
242
|
+
"props": {
|
|
243
|
+
"label": "Blocked",
|
|
244
|
+
"default": false,
|
|
245
|
+
"name": "blocked"
|
|
246
|
+
},
|
|
247
|
+
"step": {
|
|
248
|
+
"of": "stepper",
|
|
249
|
+
"index": 1
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"id": "attachment",
|
|
254
|
+
"type": "file",
|
|
255
|
+
"x": 0.04,
|
|
256
|
+
"y": 0.28,
|
|
257
|
+
"w": 0.92,
|
|
258
|
+
"h": 0.1,
|
|
259
|
+
"z": 11,
|
|
260
|
+
"props": {
|
|
261
|
+
"label": "Attachment",
|
|
262
|
+
"accept": ".pdf,.png,.jpg,.jpeg,.docx,.xlsx",
|
|
263
|
+
"maxSize": 10,
|
|
264
|
+
"name": "attachment"
|
|
265
|
+
},
|
|
266
|
+
"step": {
|
|
267
|
+
"of": "stepper",
|
|
268
|
+
"index": 2
|
|
99
269
|
}
|
|
100
270
|
}
|
|
101
271
|
],
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"edit",
|
|
24
24
|
"delete"
|
|
25
25
|
],
|
|
26
|
+
"openIn": "page",
|
|
26
27
|
"editScreen": "task_edit",
|
|
27
28
|
"columns": [
|
|
28
29
|
{
|
|
@@ -33,9 +34,17 @@
|
|
|
33
34
|
"field": "status",
|
|
34
35
|
"label": "Status"
|
|
35
36
|
},
|
|
37
|
+
{
|
|
38
|
+
"field": "priority",
|
|
39
|
+
"label": "Priority"
|
|
40
|
+
},
|
|
36
41
|
{
|
|
37
42
|
"field": "dueDate",
|
|
38
43
|
"label": "Due date"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"field": "tags",
|
|
47
|
+
"label": "Tags"
|
|
39
48
|
}
|
|
40
49
|
]
|
|
41
50
|
}
|
|
@@ -1,10 +1,91 @@
|
|
|
1
1
|
{
|
|
2
2
|
"entity": "task",
|
|
3
|
+
"columns": 2,
|
|
4
|
+
"openIn": "page",
|
|
3
5
|
"fields": [
|
|
4
|
-
{
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
{
|
|
7
|
+
"type": "stepper",
|
|
8
|
+
"steps": [
|
|
9
|
+
"The task",
|
|
10
|
+
"Planning",
|
|
11
|
+
"Attachment"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"label": "Title",
|
|
16
|
+
"step": "The task",
|
|
17
|
+
"required": true,
|
|
18
|
+
"validation": {
|
|
19
|
+
"maxLength": 200
|
|
20
|
+
},
|
|
21
|
+
"width": "full"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"label": "Status",
|
|
25
|
+
"type": "dropdown",
|
|
26
|
+
"options": [
|
|
27
|
+
"To do",
|
|
28
|
+
"In progress",
|
|
29
|
+
"Done"
|
|
30
|
+
],
|
|
31
|
+
"required": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"label": "Priority",
|
|
35
|
+
"type": "radio",
|
|
36
|
+
"options": [
|
|
37
|
+
"Low",
|
|
38
|
+
"Normal",
|
|
39
|
+
"High"
|
|
40
|
+
],
|
|
41
|
+
"layout": "horizontal"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"label": "Description",
|
|
45
|
+
"type": "textarea",
|
|
46
|
+
"validation": {
|
|
47
|
+
"maxLength": 1000
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"label": "Due date",
|
|
52
|
+
"type": "date",
|
|
53
|
+
"step": "Planning"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"label": "Remind at",
|
|
57
|
+
"type": "datetime"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"label": "Tags",
|
|
61
|
+
"type": "multiselect",
|
|
62
|
+
"options": [
|
|
63
|
+
"Bug",
|
|
64
|
+
"Feature",
|
|
65
|
+
"Chore",
|
|
66
|
+
"Customer"
|
|
67
|
+
],
|
|
68
|
+
"layout": "horizontal",
|
|
69
|
+
"width": "full"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"label": "Blocked",
|
|
73
|
+
"type": "checkbox"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"label": "Attachment",
|
|
77
|
+
"type": "file",
|
|
78
|
+
"step": "Attachment",
|
|
79
|
+
"accept": ".pdf,.png,.jpg,.jpeg,.docx,.xlsx",
|
|
80
|
+
"maxSize": 10,
|
|
81
|
+
"width": "full"
|
|
82
|
+
}
|
|
8
83
|
],
|
|
9
|
-
"listColumns": [
|
|
84
|
+
"listColumns": [
|
|
85
|
+
"title",
|
|
86
|
+
"status",
|
|
87
|
+
"priority",
|
|
88
|
+
"dueDate",
|
|
89
|
+
"tags"
|
|
90
|
+
]
|
|
10
91
|
}
|
package/templates/ui/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm run build # → dist/
|
|
|
17
17
|
| `src/App.jsx` | routes, the side rail (`drawerItems`) and the settings menu (`settingsOverrides`) — **by key**, never by label |
|
|
18
18
|
| `src/menu.js` | every menu key this app uses; forms added to the menu are `form:<key>` |
|
|
19
19
|
| `src/api/factory.js` | the calls to the API's forms routes |
|
|
20
|
-
| `src/pages/Tasks.jsx` | the sample list
|
|
20
|
+
| `src/pages/Tasks.jsx` | the sample list. It opens records on a **page** (`onOpenRecord` → `/tasks/new`, `/tasks/:id`), and `EditTask.jsx` is that page — it also holds the task form's **front-end hooks** (`TaskHooks extends FactoryHooks` — every method calls `super`, including `step`, run before a stepper moves) and sends **Done** back to the list |
|
|
21
21
|
| `src/pages/ConfigureUI.jsx` | Configure UI (Super Admin): `Forms.jsx` and `MenuSettings.jsx` tabs; `FormDesigner.jsx` designs one form |
|
|
22
22
|
| `src/pages/FormRecords.jsx` | opens any published form — what a form added to the menu links to |
|
|
23
23
|
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@tanstack/react-table": "^8.21.3",
|
|
13
|
-
"@xeplr/ui-account": "^1.0.
|
|
13
|
+
"@xeplr/ui-account": "^1.0.9",
|
|
14
14
|
"@xeplr/ui-canvas": "^1.0.2",
|
|
15
|
-
"@xeplr/ui-factory": "^1.0.
|
|
15
|
+
"@xeplr/ui-factory": "^1.0.12",
|
|
16
16
|
"@xeplr/ui-table": "^1.0.6",
|
|
17
17
|
"react": "^19.2.4",
|
|
18
18
|
"react-dom": "^19.2.4",
|
package/templates/ui/src/App.jsx
CHANGED
|
@@ -3,6 +3,7 @@ import { Routes, Route, Outlet, Navigate, useNavigate } from 'react-router-dom'
|
|
|
3
3
|
import { authRoutes, authPath, NavPage, ProtectedRoute, useAccess } from '@xeplr/ui-account'
|
|
4
4
|
import Home from './pages/Home.jsx'
|
|
5
5
|
import Tasks from './pages/Tasks.jsx'
|
|
6
|
+
import EditTask from './pages/EditTask.jsx'
|
|
6
7
|
import ConfigureUI from './pages/ConfigureUI.jsx'
|
|
7
8
|
import { FORM_MENU_PREFIX } from './menu.js'
|
|
8
9
|
import FormDesigner from './pages/FormDesigner.jsx'
|
|
@@ -105,6 +106,9 @@ __MT_SELECT_ROUTE__
|
|
|
105
106
|
<Route path="/" element={<Navigate to="/home" replace />} />
|
|
106
107
|
<Route path="/home" element={<Home />} />
|
|
107
108
|
<Route path="/tasks" element={<Tasks />} />
|
|
109
|
+
{/* The task list opens its form on a page — see Tasks.jsx's onOpenRecord. */}
|
|
110
|
+
<Route path="/tasks/new" element={<EditTask />} />
|
|
111
|
+
<Route path="/tasks/:id" element={<EditTask />} />
|
|
108
112
|
<Route path="/forms/:form" element={<FormRecords />} />
|
|
109
113
|
<Route path="/configure" element={<ConfigureUI />} />
|
|
110
114
|
<Route path="/configure/forms/:form/design/:part" element={<FormDesigner />} />
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// module holds its HOOKS, used by this page, by the Tasks list and by the
|
|
5
5
|
// popup the list opens.
|
|
6
6
|
import { useEffect, useState } from 'react'
|
|
7
|
+
import { useNavigate, useParams } from 'react-router-dom'
|
|
7
8
|
import { FactoryScreen, FactoryHooks } from '@xeplr/ui-factory'
|
|
8
9
|
import { factory } from '../api/factory.js'
|
|
9
10
|
|
|
@@ -35,6 +36,25 @@ export class TaskHooks extends FactoryHooks {
|
|
|
35
36
|
return super.delete(record, ctx)
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Before a stepper moves — Next, Back, or a click on the bar.
|
|
41
|
+
*
|
|
42
|
+
* Return false to stay where you are, or a step (its key or its number) to
|
|
43
|
+
* go there instead. ctx.disable(['planning']) rules steps out — they stay on
|
|
44
|
+
* the bar, struck through, and Next and Back pass over them; ctx.enable()
|
|
45
|
+
* brings them all back. The step's own required fields are checked before
|
|
46
|
+
* this runs, so this is for your rules, not theirs. For example:
|
|
47
|
+
*
|
|
48
|
+
* if (ctx.from === 0 && ctx.values.priority === 'low') {
|
|
49
|
+
* ctx.disable(['planning'])
|
|
50
|
+
* return 'attachment'
|
|
51
|
+
* }
|
|
52
|
+
* ctx.enable()
|
|
53
|
+
*/
|
|
54
|
+
step(ctx) {
|
|
55
|
+
return super.step(ctx)
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
/** Extra row buttons beside Edit / Delete: [{ label, onClick: (record, ctx) => … }]. ctx.refresh() reloads. */
|
|
39
59
|
actions(ctx) {
|
|
40
60
|
return super.actions(ctx)
|
|
@@ -43,15 +63,41 @@ export class TaskHooks extends FactoryHooks {
|
|
|
43
63
|
|
|
44
64
|
export const taskHooks = new TaskHooks()
|
|
45
65
|
|
|
46
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* The task form on a page of its own — /tasks/new, or /tasks/:id for a task
|
|
68
|
+
* that exists. The list sends people here (its "Opens in" is a page), and
|
|
69
|
+
* Done brings them back.
|
|
70
|
+
*
|
|
71
|
+
* There is no Save button: the form saves itself a moment after each change.
|
|
72
|
+
* Done only waits for anything still in flight, then leaves.
|
|
73
|
+
*/
|
|
47
74
|
export default function EditTask({ record }) {
|
|
75
|
+
const { id } = useParams()
|
|
76
|
+
const navigate = useNavigate()
|
|
48
77
|
const [screen, setScreen] = useState(null)
|
|
78
|
+
const [task, setTask] = useState(record || null)
|
|
49
79
|
const [error, setError] = useState(null)
|
|
50
80
|
|
|
51
81
|
useEffect(() => {
|
|
52
82
|
factory.loadScreen('task_edit').then(setScreen, (e) => setError(e.message))
|
|
53
83
|
}, [])
|
|
54
84
|
|
|
85
|
+
// The record as it is NOW, through the server's get hooks — not a copy the
|
|
86
|
+
// list happened to have.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (record || !id || id === 'new') return
|
|
89
|
+
factory.fetchRecord({ screen: 'task_edit', id }).then(setTask, (e) => setError(e.message))
|
|
90
|
+
}, [id, record])
|
|
91
|
+
|
|
55
92
|
if (error) return <div className="app-error">Could not load the task form — {error}</div>
|
|
56
|
-
|
|
93
|
+
if (!screen || (id && id !== 'new' && !task)) return null
|
|
94
|
+
return (
|
|
95
|
+
<FactoryScreen
|
|
96
|
+
document={screen}
|
|
97
|
+
record={task}
|
|
98
|
+
{...factory.screenProps}
|
|
99
|
+
hooks={taskHooks}
|
|
100
|
+
onDone={() => navigate('/tasks')}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
57
103
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
// THE SAMPLE — a list of tasks. New and Edit open the task form
|
|
2
|
-
// where it saves itself as you type; there is no Save button
|
|
1
|
+
// THE SAMPLE — a list of tasks. New and Edit open the task form on a PAGE of
|
|
2
|
+
// its own, where it saves itself as you type; there is no Save button, and
|
|
3
|
+
// Done goes back here. (A list can open its form in a popup instead: Configure
|
|
4
|
+
// UI → Forms → the list → Opens in.)
|
|
3
5
|
//
|
|
4
6
|
// Nothing about tasks is written in this file. The list and the form are
|
|
5
7
|
// SCREENS, designed in Configure UI → Forms and stored by the API; this page only
|
|
6
8
|
// loads the published "task_list" and hands it the calls it needs — and the
|
|
7
9
|
// task hooks (EditTask.jsx), which the popup uses too.
|
|
8
10
|
import { useEffect, useState } from 'react'
|
|
11
|
+
import { useNavigate } from 'react-router-dom'
|
|
9
12
|
import { FactoryScreen } from '@xeplr/ui-factory'
|
|
10
13
|
import { factory } from '../api/factory.js'
|
|
11
14
|
import TasksHelp from './TasksHelp.jsx'
|
|
@@ -14,6 +17,7 @@ import { taskHooks } from './EditTask.jsx'
|
|
|
14
17
|
export default function Tasks() {
|
|
15
18
|
const [screen, setScreen] = useState(null)
|
|
16
19
|
const [error, setError] = useState(null)
|
|
20
|
+
const navigate = useNavigate()
|
|
17
21
|
|
|
18
22
|
useEffect(() => {
|
|
19
23
|
factory.loadScreen('task_list').then(setScreen, (e) => setError(e.message))
|
|
@@ -24,7 +28,15 @@ export default function Tasks() {
|
|
|
24
28
|
<div className="app-page-main">
|
|
25
29
|
{error && <div className="app-error">Could not load the task list — {error}</div>}
|
|
26
30
|
{/* loadScreen lets the list's Edit / New fetch "task_edit" when first opened. */}
|
|
27
|
-
{screen &&
|
|
31
|
+
{screen && (
|
|
32
|
+
<FactoryScreen
|
|
33
|
+
document={screen}
|
|
34
|
+
{...factory.screenProps}
|
|
35
|
+
hooks={taskHooks}
|
|
36
|
+
// The list opens records on a page, so it asks this app where to go.
|
|
37
|
+
onOpenRecord={({ id }) => navigate(id ? `/tasks/${id}` : '/tasks/new')}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
28
40
|
</div>
|
|
29
41
|
<TasksHelp />
|
|
30
42
|
</div>
|
|
@@ -5,7 +5,7 @@ import { useState } from 'react'
|
|
|
5
5
|
// and find.
|
|
6
6
|
|
|
7
7
|
// The same prompt as in CLAUDE.md. Replace the form and its fields.
|
|
8
|
-
const PROMPT = 'Create a new UI for farming departments. One record is a farming department with: Name (required), Region (dropdown: North, South, East, West), Area in acres (number, at least 0), Started on (date), Notes (long text). Show Name, Region and Area in the list. Follow "Create a new UI" in CLAUDE.md.'
|
|
8
|
+
const PROMPT = 'Create a new UI for farming departments. One record is a farming department with: Name (required), Region (dropdown: North, South, East, West), Area in acres (number, at least 0), Started on (date), Certifications (choose several: Organic, Fair trade, Rainforest), Inspection at (date and time), Licence (file: .pdf or .jpg), Notes (long text). Show Name, Region and Area in the list. Follow "Create a new UI" in CLAUDE.md.'
|
|
9
9
|
|
|
10
10
|
export default function TasksHelp() {
|
|
11
11
|
const [copied, setCopied] = useState(false)
|
|
@@ -37,10 +37,18 @@ export default function TasksHelp() {
|
|
|
37
37
|
|
|
38
38
|
<h3>This page is an example</h3>
|
|
39
39
|
<p>
|
|
40
|
-
A task
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
A task is filled in over three steps — the task, planning, an
|
|
41
|
+
attachment — and shows every kind of control there is: text, a
|
|
42
|
+
dropdown, radio buttons, a tick box, dates, a date and time, several
|
|
43
|
+
tags at once, and a file. Neither the list nor the form is written by
|
|
44
|
+
hand: both are <strong>screens</strong>, and a task is saved in an
|
|
45
|
+
ordinary <code>tasks</code> table with one column per field. The file
|
|
46
|
+
itself is kept outside the database, and its column holds the path.
|
|
47
|
+
</p>
|
|
48
|
+
<p>
|
|
49
|
+
<strong>New</strong> and <strong>Edit</strong> open the form on a page
|
|
50
|
+
of its own, and <strong>Done</strong> comes back here. A list can open
|
|
51
|
+
it in a popup instead — that is its <strong>Opens in</strong> setting.
|
|
44
52
|
</p>
|
|
45
53
|
|
|
46
54
|
<h3>Change it</h3>
|