@xeplr/cli 0.1.10 → 0.1.11
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 +23 -7
- package/lib/generate/index.js +92 -1
- package/lib/prompt.js +4 -1
- package/lib/questions.js +44 -1
- package/package.json +1 -1
- package/templates/CLAUDE.md +1 -0
- package/templates/README.md +1 -0
- package/templates/api/development.env.tpl +2 -1
- package/templates/api/env.required.js +2 -1
- package/templates/api/package.json +1 -1
- package/templates/api/routes/index.js +2 -0
- package/templates/ui/package.json +1 -1
- package/templates/ui/src/App.jsx +2 -0
- package/templates/ui/src/pages/ConfigureUI.jsx +3 -0
package/README.md
CHANGED
|
@@ -26,9 +26,11 @@ Already working, with nothing to wire up:
|
|
|
26
26
|
open is named explicitly; there is no way to leave the whole thing public by
|
|
27
27
|
forgetting.
|
|
28
28
|
- **A sample built with the screen designer** — a task list whose New and Edit
|
|
29
|
-
open the task form
|
|
30
|
-
table.
|
|
31
|
-
|
|
29
|
+
open the task form on a page of its own, filled in over three steps and saving
|
|
30
|
+
as you type into an ordinary `tasks` table. It uses every control there is:
|
|
31
|
+
text, dropdown, radio buttons, tick box, date, date and time, a multi-select
|
|
32
|
+
and a file upload. Neither the list nor the form is written by hand: both are
|
|
33
|
+
screens ([`@xeplr/ui-factory`](https://www.npmjs.com/package/@xeplr/ui-factory)),
|
|
32
34
|
published when the API first starts.
|
|
33
35
|
- **Configure UI** for Super Admin, in the settings menu (top right) — not in
|
|
34
36
|
the side rail, which is for the pages people use:
|
|
@@ -39,6 +41,9 @@ Already working, with nothing to wire up:
|
|
|
39
41
|
- **Menu**: rename, reorder and hide the side rail's items.
|
|
40
42
|
|
|
41
43
|
Nobody else sees it, and the API refuses everyone else.
|
|
44
|
+
- **Flows, if you run Xeplr Workflow** — your forms one after another, where
|
|
45
|
+
what someone fills in decides the next screen. Designed in Configure UI →
|
|
46
|
+
Flows, walked at `/journey/<key>`, and resumable, since Workflow keeps the run.
|
|
42
47
|
- **Multi-tenancy, if you ask for it** — companies (or companies and
|
|
43
48
|
workspaces, up to four levels). Every row is stamped with the one it was made
|
|
44
49
|
in, every read sees only its rows, membership is checked on every request,
|
|
@@ -53,8 +58,10 @@ Already working, with nothing to wire up:
|
|
|
53
58
|
|
|
54
59
|
> Create a new UI for farming departments. One record is a farming department
|
|
55
60
|
> with: Name (required), Region (dropdown: North, South, East, West), Area in
|
|
56
|
-
> acres (number, at least 0), Started on (date),
|
|
57
|
-
>
|
|
61
|
+
> acres (number, at least 0), Started on (date), Certifications (choose several:
|
|
62
|
+
> Organic, Fair trade, Rainforest), Inspection at (date and time), Licence
|
|
63
|
+
> (file: .pdf or .jpg), Notes (long text). Show Name, Region and Area in the
|
|
64
|
+
> list. Follow "Create a new UI" in CLAUDE.md.
|
|
58
65
|
|
|
59
66
|
## Rules the generated app follows
|
|
60
67
|
|
|
@@ -129,8 +136,10 @@ Two ways, both in the running app's project:
|
|
|
129
136
|
|
|
130
137
|
> Create a new UI for farming departments. One record is a farming department
|
|
131
138
|
> with: Name (required), Region (dropdown: North, South, East, West), Area in
|
|
132
|
-
> acres (number, at least 0), Started on (date),
|
|
133
|
-
>
|
|
139
|
+
> acres (number, at least 0), Started on (date), Certifications (choose several:
|
|
140
|
+
> Organic, Fair trade, Rainforest), Inspection at (date and time), Licence
|
|
141
|
+
> (file: .pdf or .jpg), Notes (long text). Show Name, Region and Area in the
|
|
142
|
+
> list. Follow "Create a new UI" in CLAUDE.md.
|
|
134
143
|
|
|
135
144
|
Claude follows `CLAUDE.md`: the screens and server hooks and model
|
|
136
145
|
(`npx xeplr-factory screens … --no-pages`), the page with its front-end hooks,
|
|
@@ -189,10 +198,17 @@ no defaults, ever.
|
|
|
189
198
|
| Super admin email | the account you sign in with |
|
|
190
199
|
| Super admin password | its password |
|
|
191
200
|
| Multi-tenant? | no — or the levels, outermost first: `company`, `company, workspace` |
|
|
201
|
+
| Flows — is Xeplr Workflow running? | no — or its address (`http://localhost:19122`) and its backend folder. Yes adds **Configure UI → Flows**, a `/journey/<key>` page, and `/api/flows` forwarded to Workflow — and sets multi-tenancy to Workflow's own levels, company then workspace, since it keeps every journey inside both |
|
|
192
202
|
| Database connection | host, port, user, password — or skip, and fill it in later |
|
|
193
203
|
|
|
194
204
|
Encryption keys and signing secrets are **generated, never asked**.
|
|
195
205
|
|
|
206
|
+
**Flows need Workflow to share the app's sign-in.** Workflow is a separate
|
|
207
|
+
service, not a package the project installs: set its `AUTH_URL` and
|
|
208
|
+
`AUTH_DB_NAME` to the new app's, and the app's sign-in service loads Workflow's
|
|
209
|
+
access rules from the folder you gave. The generated README says exactly which
|
|
210
|
+
values.
|
|
211
|
+
|
|
196
212
|
## Licence
|
|
197
213
|
|
|
198
214
|
MIT
|
package/lib/generate/index.js
CHANGED
|
@@ -8,6 +8,10 @@ var TEMPLATES = path.join(__dirname, '..', '..', 'templates');
|
|
|
8
8
|
// with the same path as one there replaces it.
|
|
9
9
|
var TENANCY_TEMPLATES = path.join(__dirname, '..', '..', 'templates-tenancy');
|
|
10
10
|
|
|
11
|
+
// Files that exist ONLY in an app connected to Xeplr Workflow — the route that
|
|
12
|
+
// forwards flows to it, the designer, and the page a journey runs on.
|
|
13
|
+
var FLOWS_TEMPLATES = path.join(__dirname, '..', '..', 'templates-flows');
|
|
14
|
+
|
|
11
15
|
// Files whose names cannot ship as-is. `.env` and `.gitignore` are both
|
|
12
16
|
// swallowed on the way here — npm refuses to pack a .gitignore, and an .env in
|
|
13
17
|
// a published package is a mistake waiting to happen — so they travel under a
|
|
@@ -34,7 +38,51 @@ function substitutions(answers) {
|
|
|
34
38
|
__ADMIN_PASSWORD__: answers.admin.password,
|
|
35
39
|
__DB_CONNECTION__: answers.connection || '',
|
|
36
40
|
__CONNECTION_NOTE__: connectionNote(answers)
|
|
37
|
-
}, tenancySubstitutions(answers.tenancy || []));
|
|
41
|
+
}, tenancySubstitutions(answers.tenancy || []), flowsSubstitutions(answers.workflow || null, answers));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The lines an app connected to Xeplr Workflow has, and one without it does
|
|
46
|
+
* not. Like tenancy, every token exists either way and expands to nothing when
|
|
47
|
+
* flows are off, so no template needs a condition of its own.
|
|
48
|
+
*/
|
|
49
|
+
function flowsSubstitutions(workflow, answers) {
|
|
50
|
+
var on = Boolean(workflow);
|
|
51
|
+
return {
|
|
52
|
+
__WF_ENV__: on ? [
|
|
53
|
+
'# ── Flows (Xeplr Workflow) ──',
|
|
54
|
+
'# Your forms, one after another — Workflow runs the journey. It is a separate',
|
|
55
|
+
'# service: start it, and point it at THIS app\'s sign-in (see README, "Flows").',
|
|
56
|
+
'WORKFLOW_URL=' + workflow.url,
|
|
57
|
+
''
|
|
58
|
+
].join('\n') : '',
|
|
59
|
+
// Workflow's access rules — who may design a flow, who may run one — are
|
|
60
|
+
// loaded by this app's sign-in service, after the app's own.
|
|
61
|
+
__WF_AUTH_MIGRATIONS__: on && workflow.dir ? ',' + workflow.dir + '/migrations-auth' : '',
|
|
62
|
+
__WF_REQUIRED_ENV__: on ? " // Where Xeplr Workflow is — flows are forwarded to it.\n 'WORKFLOW_URL'," : '',
|
|
63
|
+
__WF_API_ROUTE__: on ? [
|
|
64
|
+
'// Flows go to Xeplr Workflow (WORKFLOW_URL), behind this app\'s own gate.',
|
|
65
|
+
"router.use('/api/flows', require('./flows'));"
|
|
66
|
+
].join('\n') : '',
|
|
67
|
+
__WF_UI_IMPORTS__: on ? [
|
|
68
|
+
"import FlowDesigner from './pages/FlowDesigner.jsx'",
|
|
69
|
+
"import Journey from './pages/Journey.jsx'"
|
|
70
|
+
].join('\n') : '',
|
|
71
|
+
__WF_UI_ROUTES__: on ? [
|
|
72
|
+
' <Route path="/configure/flows/:flow" element={<FlowDesigner />} />',
|
|
73
|
+
' {/* A journey: its run id joins the address once it starts, so it can be resumed. */}',
|
|
74
|
+
' <Route path="/journey/:flow" element={<Journey />} />',
|
|
75
|
+
' <Route path="/journey/:flow/:run" element={<Journey />} />'
|
|
76
|
+
].join('\n') : '',
|
|
77
|
+
__WF_TAB_IMPORT__: on ? "import Flows from './Flows.jsx'" : '',
|
|
78
|
+
__WF_TAB__: on ? " { id: 'flows', label: 'Flows', Page: Flows }," : '',
|
|
79
|
+
__WF_TAB_NOTE__: on ? '// Flows forms one after another — what is filled in decides the next; run by Xeplr Workflow' : '',
|
|
80
|
+
__WF_README__: on ? flowsReadme(workflow, answers) : '',
|
|
81
|
+
__WF_CLAUDE__: on ? [
|
|
82
|
+
'**Flows** — forms one after another, run by Xeplr Workflow (`WORKFLOW_URL`). Design them in Configure UI → Flows, never in code: a flow is data in Workflow, reached through `/api/flows`, and walked at `/journey/<key>`. An arrow tests one field of the screen it leaves; a form that is a step must be published first.',
|
|
83
|
+
''
|
|
84
|
+
].join('\n') : ''
|
|
85
|
+
};
|
|
38
86
|
}
|
|
39
87
|
|
|
40
88
|
/**
|
|
@@ -118,6 +166,37 @@ function tenancySubstitutions(levels) {
|
|
|
118
166
|
};
|
|
119
167
|
}
|
|
120
168
|
|
|
169
|
+
/**
|
|
170
|
+
* The README's Flows section — what a flow is, and the one thing that has to
|
|
171
|
+
* be true for it to work: Workflow and this app share a sign-in.
|
|
172
|
+
*/
|
|
173
|
+
function flowsReadme(workflow, answers) {
|
|
174
|
+
return [
|
|
175
|
+
'## Flows',
|
|
176
|
+
'',
|
|
177
|
+
'Your forms, one after another: what someone fills in on one decides which comes next. Design one in **Configure UI → Flows**, publish it, and open it at `/journey/<key>`. The run id joins the address once it starts, so a journey can be reloaded, left, or handed to someone else and opened on the step it is on.',
|
|
178
|
+
'',
|
|
179
|
+
'Flows are run by **Xeplr Workflow**, a separate service at `WORKFLOW_URL` (' + workflow.url + '). The API forwards `/api/flows/…` to it unchanged; each screen still saves into its own table.',
|
|
180
|
+
'',
|
|
181
|
+
'Workflow keeps every journey inside a **company and a workspace** — the two levels it registers — which is why this app has exactly those: a flow is made, and a run started, in the company and workspace picked after signing in.',
|
|
182
|
+
'',
|
|
183
|
+
'**Workflow must use this app\'s sign-in.** In Workflow\'s own `development.env`:',
|
|
184
|
+
'',
|
|
185
|
+
'```sh',
|
|
186
|
+
'AUTH_URL=http://localhost:' + answers.ports.auth + ' # this app\'s sign-in service',
|
|
187
|
+
'AUTH_DB_NAME=' + answers.name + '_auth # and its database',
|
|
188
|
+
'REDIS_PREFIX=' + answers.name + ':',
|
|
189
|
+
'```',
|
|
190
|
+
'',
|
|
191
|
+
'If Workflow\'s database server is not this app\'s, also set `AUTH_DB_CONNECTION_INFO_ENCRYPTED` in Workflow to this app\'s server, encrypted with **Workflow\'s** `ENCRYPTION_KEY` (`npm run db:encrypt` in Workflow\'s backend). Otherwise every flow call fails with *database "' + answers.name + '_auth" does not exist*.',
|
|
192
|
+
'',
|
|
193
|
+
workflow.dir
|
|
194
|
+
? 'Its access rules — who may design a flow, who may run one — are loaded by this app\'s sign-in service from `' + workflow.dir + '/migrations-auth` (in `XEPLR_AUTH_MIGRATIONS`). Restart the sign-in service after changing it.'
|
|
195
|
+
: 'Its access rules — who may design a flow, who may run one — live in Workflow\'s `backend/migrations-auth`. Add that folder to `XEPLR_AUTH_MIGRATIONS` in `api/development.env` (comma separated, after `./migrations-auth`) and restart the sign-in service; until then, flows answer 403.',
|
|
196
|
+
''
|
|
197
|
+
].join('\n');
|
|
198
|
+
}
|
|
199
|
+
|
|
121
200
|
/** One level's own table: what the picker lists. Its parents' ids in mtIdN. */
|
|
122
201
|
function tenantTableSql(level, i) {
|
|
123
202
|
var parents = i === 0 ? 'none — the outermost level' : i === 1 ? 'mtId1 holds the one above' : 'mtId1–mtId' + i + ' hold the ones above';
|
|
@@ -230,6 +309,15 @@ function generate(answers, target) {
|
|
|
230
309
|
throw new Error(target + ' already exists and is not empty.');
|
|
231
310
|
}
|
|
232
311
|
|
|
312
|
+
// Workflow files every flow and run under a company; an app without
|
|
313
|
+
// companies would build, start, and then refuse every flow it tried to make.
|
|
314
|
+
if (answers.workflow) {
|
|
315
|
+
var q = require('../questions');
|
|
316
|
+
if (!q.sameLevels(answers.tenancy || [], q.tenancyLevels(q.WORKFLOW_LEVELS))) {
|
|
317
|
+
throw new Error('Flows need the same multi-tenancy as Xeplr Workflow — levels "' + q.WORKFLOW_LEVELS + '" — because Workflow keeps every journey inside a company and a workspace.');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
233
321
|
var subs = substitutions(answers);
|
|
234
322
|
var written = [];
|
|
235
323
|
|
|
@@ -238,6 +326,9 @@ function generate(answers, target) {
|
|
|
238
326
|
if (answers.tenancy && answers.tenancy.length) {
|
|
239
327
|
walk(TENANCY_TEMPLATES).forEach(function (src) { sources[path.relative(TENANCY_TEMPLATES, src)] = src; });
|
|
240
328
|
}
|
|
329
|
+
if (answers.workflow) {
|
|
330
|
+
walk(FLOWS_TEMPLATES).forEach(function (src) { sources[path.relative(FLOWS_TEMPLATES, src)] = src; });
|
|
331
|
+
}
|
|
241
332
|
|
|
242
333
|
Object.keys(sources).forEach(function (rel) {
|
|
243
334
|
var src = sources[rel];
|
package/lib/prompt.js
CHANGED
|
@@ -124,7 +124,10 @@ function createSession(options) {
|
|
|
124
124
|
return /^y/i.test(v);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
/** A line of explanation between questions — no answer expected. */
|
|
128
|
+
function say(text) { output.write(text + '\n'); }
|
|
129
|
+
|
|
130
|
+
return { ask: ask, askSecret: askSecret, confirm: confirm, say: say, close: function () { rl.close(); } };
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
module.exports = { createSession: createSession };
|
package/lib/questions.js
CHANGED
|
@@ -134,6 +134,40 @@ async function collect(session, argvName) {
|
|
|
134
134
|
tenancy = tenancyLevels(levelsText);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// ── flows ─────────────────────────────────────────────────────────────
|
|
138
|
+
//
|
|
139
|
+
// Screens one after another, where what someone filled in decides the next
|
|
140
|
+
// screen. The journey is run by Xeplr Workflow — a separate service, not a
|
|
141
|
+
// package — so this only asks where it is. Off, the app has no flows at all.
|
|
142
|
+
var workflow = null;
|
|
143
|
+
var wantsFlows = await session.confirm('Flows — is Xeplr Workflow running for this app?', false);
|
|
144
|
+
if (wantsFlows) {
|
|
145
|
+
var workflowUrl = await session.ask(' Workflow API address?', {
|
|
146
|
+
default: 'http://localhost:19122',
|
|
147
|
+
validate: function (v) { return /^https?:\/\/[^\s]+$/.test(v) ? true : 'An http:// or https:// address.'; }
|
|
148
|
+
});
|
|
149
|
+
// Workflow keeps its access rules (who may design a flow, who may run one)
|
|
150
|
+
// in migrations-auth/. This app's sign-in service has to load them, so it
|
|
151
|
+
// needs to know where they are. Blank is allowed: the README says how.
|
|
152
|
+
var workflowDir = await session.ask(' Workflow backend folder (for its access rules — blank to add later)?', {
|
|
153
|
+
default: '',
|
|
154
|
+
validate: function (v) { return !v || !/\s/.test(v) ? true : 'A path without spaces.'; }
|
|
155
|
+
});
|
|
156
|
+
workflow = { url: workflowUrl.replace(/\/+$/, ''), dir: workflowDir.replace(/\/+$/, '') };
|
|
157
|
+
|
|
158
|
+
// Workflow files every flow and every run under a company and a workspace
|
|
159
|
+
// — the two levels it registers, with those headers. An app that sends
|
|
160
|
+
// anything else builds, starts, and then has every flow refused, so the
|
|
161
|
+
// levels that make flows work are set here, and said out loud.
|
|
162
|
+
if (!sameLevels(tenancy, tenancyLevels(WORKFLOW_LEVELS))) {
|
|
163
|
+
tenancy = tenancyLevels(WORKFLOW_LEVELS);
|
|
164
|
+
if (session.say) {
|
|
165
|
+
session.say(' Flows need the same tenancy as Xeplr Workflow — company, then workspace —');
|
|
166
|
+
session.say(' so this app uses those two levels.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
137
171
|
// ── the database ──────────────────────────────────────────────────────
|
|
138
172
|
//
|
|
139
173
|
// ASKED, not defaulted, and not left for the reader to do by hand either.
|
|
@@ -174,6 +208,7 @@ async function collect(session, argvName) {
|
|
|
174
208
|
databases: { auth: name + '_auth', api: name + '_api' },
|
|
175
209
|
admin: { email: adminEmail, password: adminPassword },
|
|
176
210
|
tenancy: tenancy,
|
|
211
|
+
workflow: workflow,
|
|
177
212
|
|
|
178
213
|
// GENERATED, never asked. Nobody should be inventing these by hand, and a
|
|
179
214
|
// prompt invites a weak answer or a reused one.
|
|
@@ -181,4 +216,12 @@ async function collect(session, argvName) {
|
|
|
181
216
|
};
|
|
182
217
|
}
|
|
183
218
|
|
|
184
|
-
|
|
219
|
+
/** The tenancy Xeplr Workflow registers: a flows app must send the same headers. */
|
|
220
|
+
var WORKFLOW_LEVELS = 'company, workspace';
|
|
221
|
+
|
|
222
|
+
/** Same levels, in the same order, with the same headers. */
|
|
223
|
+
function sameLevels(a, b) {
|
|
224
|
+
return a.length === b.length && a.every(function (l, i) { return l.header === b[i].header; });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = { collect: collect, envPrefix: envPrefix, tenancyLevels: tenancyLevels, sameLevels: sameLevels, WORKFLOW_LEVELS: WORKFLOW_LEVELS, NAME_RE: NAME_RE };
|
package/package.json
CHANGED
package/templates/CLAUDE.md
CHANGED
|
@@ -49,6 +49,7 @@ Then change only what the request asks for:
|
|
|
49
49
|
|
|
50
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
51
|
|
|
52
|
+
__WF_CLAUDE__
|
|
52
53
|
**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.
|
|
53
54
|
|
|
54
55
|
### The prompt
|
package/templates/README.md
CHANGED
|
@@ -34,6 +34,7 @@ Open http://localhost:__UI_PORT__ and sign in as `__ADMIN_EMAIL__`.
|
|
|
34
34
|
|
|
35
35
|
> 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.
|
|
36
36
|
|
|
37
|
+
__WF_README__
|
|
37
38
|
## Rules this app follows
|
|
38
39
|
|
|
39
40
|
| Rule | Why |
|
|
@@ -42,7 +42,7 @@ AUTH_URL=http://localhost:__AUTH_PORT__
|
|
|
42
42
|
|
|
43
43
|
# Sign-in setup files: the screens' permissions (@xeplr/factory's), then this
|
|
44
44
|
# app's own — menu entries and anything else it adds. Comma-separated, in order.
|
|
45
|
-
XEPLR_AUTH_MIGRATIONS=./node_modules/@xeplr/factory/migrations-auth,./migrations-
|
|
45
|
+
XEPLR_AUTH_MIGRATIONS=./node_modules/@xeplr/factory/migrations-auth,./migrations-auth__WF_AUTH_MIGRATIONS__
|
|
46
46
|
|
|
47
47
|
# ── The first account you sign in with ──
|
|
48
48
|
# Created for you when you set up the database. Change the password here and
|
|
@@ -57,6 +57,7 @@ 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
|
+
__WF_ENV__
|
|
60
61
|
# ── Uploaded files ──
|
|
61
62
|
# Where a form's FILE field puts what people upload. The path is what the
|
|
62
63
|
# record stores; the file itself is served only through the API's own route,
|
package/templates/ui/src/App.jsx
CHANGED
|
@@ -4,6 +4,7 @@ import { authRoutes, authPath, NavPage, ProtectedRoute, useAccess } from '@xeplr
|
|
|
4
4
|
import Home from './pages/Home.jsx'
|
|
5
5
|
import Tasks from './pages/Tasks.jsx'
|
|
6
6
|
import EditTask from './pages/EditTask.jsx'
|
|
7
|
+
__WF_UI_IMPORTS__
|
|
7
8
|
import ConfigureUI from './pages/ConfigureUI.jsx'
|
|
8
9
|
import { FORM_MENU_PREFIX } from './menu.js'
|
|
9
10
|
import FormDesigner from './pages/FormDesigner.jsx'
|
|
@@ -112,6 +113,7 @@ __MT_SELECT_ROUTE__
|
|
|
112
113
|
<Route path="/forms/:form" element={<FormRecords />} />
|
|
113
114
|
<Route path="/configure" element={<ConfigureUI />} />
|
|
114
115
|
<Route path="/configure/forms/:form/design/:part" element={<FormDesigner />} />
|
|
116
|
+
__WF_UI_ROUTES__
|
|
115
117
|
</Route>
|
|
116
118
|
|
|
117
119
|
<Route path="*" element={<Navigate to="/auth/login" replace />} />
|
|
@@ -3,15 +3,18 @@
|
|
|
3
3
|
// Forms every UI in the app: make one, design it, publish it, open it, add it
|
|
4
4
|
// to the side rail
|
|
5
5
|
// Menu the side rail's items: rename, reorder, hide
|
|
6
|
+
__WF_TAB_NOTE__
|
|
6
7
|
//
|
|
7
8
|
// Configuration lives here, not in the side rail — the rail is for the pages
|
|
8
9
|
// people use.
|
|
9
10
|
import { useSearchParams } from 'react-router-dom'
|
|
10
11
|
import Forms from './Forms.jsx'
|
|
11
12
|
import MenuSettings from './MenuSettings.jsx'
|
|
13
|
+
__WF_TAB_IMPORT__
|
|
12
14
|
|
|
13
15
|
const TABS = [
|
|
14
16
|
{ id: 'forms', label: 'Forms', Page: Forms },
|
|
17
|
+
__WF_TAB__
|
|
15
18
|
{ id: 'menu', label: 'Menu', Page: MenuSettings }
|
|
16
19
|
]
|
|
17
20
|
|