@xeplr/cli 0.2.0 → 0.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 +1 -0
- package/package.json +1 -1
- package/templates/api/bin/workflow +3 -3
- package/templates/api/package.json +3 -3
- package/templates/api/routes/flows.js +11 -7
- package/templates/api/routes/index.js +2 -2
- package/templates/api/workflow.js +9 -2
- package/templates/ui/package.json +5 -3
- package/templates/ui/src/pages/FlowDesigner.jsx +44 -27
- package/templates/ui/src/pages/Flows.jsx +36 -14
package/README.md
CHANGED
|
@@ -77,6 +77,7 @@ These are architecture decisions, not defaults to tune — each one exists becau
|
|
|
77
77
|
| **`REDIS_PREFIX` is required and unique per app** (`api/development.env`, set to `<name>:`). | `@xeplr/auth`, `@xeplr/utils` | Two apps on one Redis under the shared default serve each other's sessions, menus and API permissions. Sign-in refuses to start without it, or with `xeplr:`. |
|
|
78
78
|
| **Designing, new forms and publishing are Super Admin only.** | `api/routes/access.js`, `@xeplr/auth` menu routes | Publishing changes database tables. Every other screen route checks the permission catalog. |
|
|
79
79
|
| **The browser is not a security boundary.** Front-end hooks (`ui/src/pages/Edit<Form>.jsx`) shape what people see and send; anything that must hold goes in server hooks (`api/screens/<form>/<form>.hooks.js`); data shape in the model (`<form>.model.js`). | `@xeplr/ui-factory`, `@xeplr/factory` | |
|
|
80
|
+
| **Anything every app would write identically lives in a package, not in the generated app.** A generated app is a starting point that then lives its own life — glue copied into it can never be fixed centrally, so an app made last month keeps that copy forever. When the flow designer needed this app's forms as its Screen steps, the wiring went into `@xeplr/ui-factory` (`useScreenSource`) and the generated page passes two props. | `@xeplr/ui-factory`, `@xeplr/ui-workflow` | A fix or a new control type has to reach apps that already exist, and it cannot reach code that was copied into them. `test/cli.test.js` fails if the glue reappears in the generated app. |
|
|
80
81
|
| **Every data address is under `/api`.** | `api/routes`, `ui/vite.config.js` | A page path and a data path must never collide. |
|
|
81
82
|
| **No guessed database names or connections.** | `api/development.env` | A wrong-but-present value starts cleanly and reads as empty data. |
|
|
82
83
|
| **Email is checked at start, never fatal.** The sign-in banner shows `email ✓ …` or `✗ NOT WORKING — reason`. | `@xeplr/auth` | Sign-in works without email; activation, invite and reset links cannot be sent until `EMAIL_PROVIDER` is set. |
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// The SAME workflow as the embedded one (../workflow.js): this app's database,
|
|
9
9
|
// this app's tenancy and membership gate, the same permission checks, and the
|
|
10
|
-
// same path — /api/workflow
|
|
10
|
+
// same path — /api/workflow — so the browser cannot tell the two apart.
|
|
11
11
|
// What routes /api/workflow here: ui/.env WORKFLOW_URL for Vite's dev proxy,
|
|
12
12
|
// nginx in production (README, "Production").
|
|
13
13
|
require('dotenv').config({ path: require('path').join(__dirname, '..', (process.env.NODE_ENV || 'development') + '.env') });
|
|
@@ -28,7 +28,7 @@ async function start() {
|
|
|
28
28
|
// This app's tenancy levels, registered exactly as the API registers them.
|
|
29
29
|
await require('../db/setup');
|
|
30
30
|
var app = require('../app');
|
|
31
|
-
var
|
|
31
|
+
var workflowRouter = await require('../workflow')(app.memberGate);
|
|
32
32
|
|
|
33
33
|
var health = express.Router();
|
|
34
34
|
health.get('/', function(req, res) { res.json({ status: 'ok', service: 'workflow' }); });
|
|
@@ -41,7 +41,7 @@ async function start() {
|
|
|
41
41
|
middleware: app.middleware(),
|
|
42
42
|
routes: {
|
|
43
43
|
'/health': health,
|
|
44
|
-
'/api/workflow
|
|
44
|
+
'/api/workflow': workflowRouter
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
}
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"@xeplr/auth": "^1.1.4",
|
|
22
22
|
"@xeplr/base-apis": "^2.0.0",
|
|
23
23
|
"@xeplr/db": "^1.0.6",
|
|
24
|
-
"@xeplr/factory": "^1.0
|
|
25
|
-
"@xeplr/ui-factory": "^1.0
|
|
24
|
+
"@xeplr/factory": "^1.1.0",
|
|
25
|
+
"@xeplr/ui-factory": "^1.1.0",
|
|
26
26
|
"@xeplr/utils": "^1.0.11",
|
|
27
|
-
"@xeplr/workflow": "^1.
|
|
27
|
+
"@xeplr/workflow": "^1.2.0",
|
|
28
28
|
"dotenv": "^17.4.2",
|
|
29
29
|
"express": "^5.2.0",
|
|
30
30
|
"multer": "^2.0.2"
|
|
@@ -20,24 +20,28 @@
|
|
|
20
20
|
// "Start flow run"…), granted by @xeplr/workflow's
|
|
21
21
|
// migrations-auth — in XEPLR_AUTH_MIGRATIONS.
|
|
22
22
|
//
|
|
23
|
-
// Mounted by routes/index.js at /api/workflow
|
|
23
|
+
// Mounted by routes/index.js at /api/workflow, behind this app's auth gate.
|
|
24
|
+
// Everything Workflow serves lives under it:
|
|
25
|
+
// /api/workflow/flows the flows people walk through (Journey)
|
|
26
|
+
// /api/workflow/workflows the designer's document API
|
|
27
|
+
// /api/workflow/actions what a step can DO, for the palette
|
|
24
28
|
var express = require('express');
|
|
25
29
|
|
|
26
30
|
var router = express.Router();
|
|
27
|
-
var
|
|
31
|
+
var workflow = null;
|
|
28
32
|
|
|
29
33
|
router.use(function(req, res, next) {
|
|
30
34
|
if (process.env.WORKFLOW_PORT) {
|
|
31
35
|
return res.status(404).json({ message: 'Workflow runs on its own port (WORKFLOW_PORT) — route /api/workflow there (ui/.env WORKFLOW_URL, or nginx)' });
|
|
32
36
|
}
|
|
33
37
|
// Only before startup has finished — bin/www mounts it before listening.
|
|
34
|
-
if (!
|
|
35
|
-
return
|
|
38
|
+
if (!workflow) return res.status(503).json({ message: 'Flows are still starting' });
|
|
39
|
+
return workflow(req, res, next);
|
|
36
40
|
});
|
|
37
41
|
|
|
38
|
-
/** bin/www hands over registerWorkflow's
|
|
39
|
-
router.mount = function(
|
|
40
|
-
|
|
42
|
+
/** bin/www hands over registerWorkflow's router, already gated. */
|
|
43
|
+
router.mount = function(workflowRouter) {
|
|
44
|
+
workflow = workflowRouter;
|
|
41
45
|
};
|
|
42
46
|
|
|
43
47
|
module.exports = router;
|
|
@@ -34,9 +34,9 @@ router.get('/whoami', function(req, res) {
|
|
|
34
34
|
// var { genericRoute } = require('@xeplr/base-apis');
|
|
35
35
|
// router.use('/api/things', genericRoute({ key: 'thing', model: require('../models').Thing }));
|
|
36
36
|
|
|
37
|
-
// Flows: Xeplr Workflow, at /api/workflow
|
|
37
|
+
// Flows: Xeplr Workflow, at /api/workflow — inside this API, or on its
|
|
38
38
|
// own port with WORKFLOW_PORT (see routes/flows.js). The path is the same
|
|
39
39
|
// either way: the browser never knows which.
|
|
40
|
-
router.use('/api/workflow
|
|
40
|
+
router.use('/api/workflow', require('./flows'));
|
|
41
41
|
|
|
42
42
|
module.exports = router;
|
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
// @xeplr/workflow's migrations-auth, which XEPLR_AUTH_MIGRATIONS loads.
|
|
10
10
|
var { registerWorkflow } = require('@xeplr/workflow');
|
|
11
11
|
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Runs workflow's migrations and returns its ROUTER — already gated.
|
|
14
|
+
*
|
|
15
|
+
* The whole router, not just the flows facade: it carries `/flows` (the
|
|
16
|
+
* screens a person walks through) AND `/workflows` (the designer's own
|
|
17
|
+
* document API). One designer serves both — @xeplr/ui-workflow's canvas, the
|
|
18
|
+
* same one every xeplr product uses — so the app has to serve what it reads.
|
|
19
|
+
*/
|
|
13
20
|
module.exports = async function startWorkflow(memberGate) {
|
|
14
21
|
var workflow = await registerWorkflow({
|
|
15
22
|
applicationId: '__NAME__',
|
|
@@ -23,5 +30,5 @@ module.exports = async function startWorkflow(memberGate) {
|
|
|
23
30
|
access: true,
|
|
24
31
|
mtMembershipGate: memberGate
|
|
25
32
|
});
|
|
26
|
-
return workflow.
|
|
33
|
+
return workflow.router;
|
|
27
34
|
};
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@tanstack/react-table": "^8.21.3",
|
|
13
13
|
"@xeplr/ui-account": "^1.0.9",
|
|
14
|
-
"@xeplr/ui-canvas": "^1.0
|
|
15
|
-
"@xeplr/ui-factory": "^1.0
|
|
14
|
+
"@xeplr/ui-canvas": "^1.1.0",
|
|
15
|
+
"@xeplr/ui-factory": "^1.1.0",
|
|
16
16
|
"@xeplr/ui-table": "^1.0.6",
|
|
17
17
|
"react": "^19.2.4",
|
|
18
18
|
"react-dom": "^19.2.4",
|
|
19
|
-
"react-router-dom": "^7.13.2"
|
|
19
|
+
"react-router-dom": "^7.13.2",
|
|
20
|
+
"@xeplr/ui-workflow": "^1.2.0",
|
|
21
|
+
"@xeplr/ui-utils": "^1.0.2"
|
|
20
22
|
},
|
|
21
23
|
"devDependencies": {
|
|
22
24
|
"@vitejs/plugin-react": "^5.2.0",
|
|
@@ -1,37 +1,54 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// ONE FLOW, ON WORKFLOW'S OWN CANVAS.
|
|
2
|
+
//
|
|
3
|
+
// The designer is a component from @xeplr/ui-workflow — the same one BI and
|
|
4
|
+
// every other xeplr product uses — dropped straight onto this page. It is not
|
|
5
|
+
// a section mounted at URLs of its own: this app already has a page for it,
|
|
6
|
+
// inside its own shell, and the designer renders where it is put.
|
|
7
|
+
//
|
|
8
|
+
// There used to be a second, screens-only builder here (@xeplr/ui-factory's
|
|
9
|
+
// FlowBuilder). One designer over one engine is the point: the trigger box,
|
|
10
|
+
// the condition boxes and the reference pickers are built once, and this app
|
|
11
|
+
// gets them by upgrading a package.
|
|
12
|
+
//
|
|
13
|
+
// THIS APP'S FORMS ARE THE FLOW'S SCREENS. A step can show one and wait for
|
|
14
|
+
// somebody to fill it in, and `useScreenSource` is @xeplr/ui-factory's answer
|
|
15
|
+
// to the two things the designer asks its host: which screens exist (with
|
|
16
|
+
// what each one hands back, so a later step can pick `title` by name) and how
|
|
17
|
+
// to design one without leaving the flow. Two props, because the alternative
|
|
18
|
+
// was the same twenty lines in every app — and a copy in your app could never
|
|
19
|
+
// be fixed once.
|
|
20
|
+
//
|
|
21
|
+
// WHERE THE API IS: /api/workflow, always — the dev proxy (vite.config.js) or
|
|
22
|
+
// nginx decides whether that is this API or Workflow on its own port.
|
|
23
|
+
import { useParams, useNavigate } from 'react-router-dom'
|
|
24
|
+
import { WorkflowDesigner } from '@xeplr/ui-workflow'
|
|
25
|
+
import { useScreenSource, FactoryBuilder } from '@xeplr/ui-factory'
|
|
8
26
|
import { factory } from '../api/factory.js'
|
|
9
27
|
|
|
10
28
|
export default function FlowDesigner() {
|
|
11
29
|
const { flow: key } = useParams()
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const [error, setError] = useState(null)
|
|
30
|
+
const navigate = useNavigate()
|
|
31
|
+
const { screens, screenEditor } = useScreenSource(factory, { Builder: FactoryBuilder })
|
|
15
32
|
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
flows.loadFlow(key).then(setFlow, (e) => setError(e.message))
|
|
18
|
-
// A step is a FORM (an edit screen) that is published: what people will
|
|
19
|
-
// actually see, and something they can fill in.
|
|
20
|
-
factory.listScreens().then((rows) => setScreens(rows
|
|
21
|
-
.filter((s) => /_edit$/.test(s.screenKey) && s.version !== null)
|
|
22
|
-
.map((s) => ({ id: s.screenKey, name: s.name }))), () => {})
|
|
23
|
-
}, [key])
|
|
24
|
-
|
|
25
|
-
if (error) return <div className="app-page"><div className="app-error">{error}</div></div>
|
|
26
|
-
if (!flow) return null
|
|
27
33
|
return (
|
|
28
34
|
<div className="app-designer">
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
<WorkflowDesigner
|
|
36
|
+
apiBase="/api/workflow"
|
|
37
|
+
screens={screens}
|
|
38
|
+
screenEditor={screenEditor}
|
|
39
|
+
// BY KEY, because that is what this app's URLs carry:
|
|
40
|
+
// /configure/flows/leave_request. A flow is created on the Flows page
|
|
41
|
+
// with a key made from its name, and that key is how it is addressed
|
|
42
|
+
// for the rest of its life — a readable link that survives a rebuild
|
|
43
|
+
// minting new ids.
|
|
44
|
+
workflowKey={key === 'new' ? undefined : key}
|
|
45
|
+
newKind={key === 'new' ? 'workflow' : undefined}
|
|
46
|
+
// Only reachable from /configure/flows/new, which this app does not
|
|
47
|
+
// link to — the Flows page creates the flow first, so the designer
|
|
48
|
+
// always opens on one that exists.
|
|
49
|
+
onOpened={(saved) => navigate('/configure/flows/' + saved, { replace: true })}
|
|
50
|
+
onBack={() => navigate('/configure?tab=flows')}
|
|
51
|
+
/>
|
|
35
52
|
</div>
|
|
36
53
|
)
|
|
37
54
|
}
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
// CONFIGURE UI → FLOWS — every journey across the app's forms.
|
|
2
2
|
//
|
|
3
|
-
// A flow is
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// at
|
|
3
|
+
// A flow is what someone is walked through: screens one after another, and
|
|
4
|
+
// whatever has to happen between them. What they fill in on one decides which
|
|
5
|
+
// comes next. Xeplr Workflow runs it, so a journey can be left and picked up
|
|
6
|
+
// later — by someone else, on another day. Design it here; open it at
|
|
7
|
+
// /journey/<key>.
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
9
|
+
// A FLOW IS AN ORDINARY WORKFLOW, addressed by a key this page makes from its
|
|
10
|
+
// name. It used to be created as kind 'screens', which is a workflow whose
|
|
11
|
+
// steps are GENERATED from a design held in another app — and that app was
|
|
12
|
+
// the screens-only builder this one replaced. The designer then had to refuse
|
|
13
|
+
// to edit its own flows ("designed in Configure UI"), which is how a flow
|
|
14
|
+
// became a page you could open and not change.
|
|
15
|
+
//
|
|
16
|
+
// So a flow is a workflow now, and every kind of step is available inside it:
|
|
17
|
+
// a Screen for the part a person fills in, an Action for the email that goes
|
|
18
|
+
// out after it, a Condition for where it goes next. Flows created the old way
|
|
19
|
+
// still appear here and still run; the designer shows them read-only.
|
|
10
20
|
import { useEffect, useState } from 'react'
|
|
11
21
|
import { Link, useNavigate } from 'react-router-dom'
|
|
12
|
-
import {
|
|
22
|
+
import { listWorkflows, saveWorkflow, configureWorkflowApi } from '@xeplr/ui-workflow'
|
|
23
|
+
|
|
24
|
+
// The same mount the designer uses. Said here as well because this page can
|
|
25
|
+
// be the first one to ask, and a base set after the first request is too late.
|
|
26
|
+
configureWorkflowApi('/api/workflow')
|
|
13
27
|
|
|
14
28
|
export default function Flows() {
|
|
15
29
|
const [list, setList] = useState(null)
|
|
@@ -18,7 +32,7 @@ export default function Flows() {
|
|
|
18
32
|
const [name, setName] = useState('')
|
|
19
33
|
const navigate = useNavigate()
|
|
20
34
|
|
|
21
|
-
const load = () =>
|
|
35
|
+
const load = () => listWorkflows().then(setList, (e) => {
|
|
22
36
|
const off = flowsOffline(e)
|
|
23
37
|
if (off) setOffline(off)
|
|
24
38
|
else setError(e.message)
|
|
@@ -31,7 +45,9 @@ export default function Flows() {
|
|
|
31
45
|
if (!label) return
|
|
32
46
|
const key = label.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
|
|
33
47
|
try {
|
|
34
|
-
|
|
48
|
+
// No steps: the designer is where those come from, and it opens on the
|
|
49
|
+
// flow this creates. `key` is what every link to it uses from here on.
|
|
50
|
+
await saveWorkflow({ name: label, key, kind: 'workflow', status: 'draft' })
|
|
35
51
|
navigate(`/configure/flows/${key}`)
|
|
36
52
|
} catch (err) {
|
|
37
53
|
setError(err.message)
|
|
@@ -62,14 +78,20 @@ export default function Flows() {
|
|
|
62
78
|
<thead><tr><th>Flow</th><th>Key</th><th>Status</th><th>Screens</th><th /></tr></thead>
|
|
63
79
|
<tbody>
|
|
64
80
|
{list.map((f) => (
|
|
65
|
-
<tr key={f.key}>
|
|
81
|
+
<tr key={f.id || f.key}>
|
|
66
82
|
<td><strong>{f.name}</strong></td>
|
|
67
|
-
<td className="app-sub">{f.key}</td>
|
|
68
|
-
<td>{f.status}</td>
|
|
83
|
+
<td className="app-sub">{f.key || '—'}</td>
|
|
84
|
+
<td>{f.kind === 'screens' ? f.status + ' · read-only' : f.status}</td>
|
|
69
85
|
<td>{typeof f.steps === 'number' ? f.steps : (f.steps || []).length}</td>
|
|
70
86
|
<td className="app-row-actions">
|
|
71
|
-
<Link to={`/configure/flows/${f.key}`}>Design</Link>
|
|
72
|
-
{
|
|
87
|
+
{f.key && <Link to={`/configure/flows/${f.key}`}>Design</Link>}
|
|
88
|
+
{/* PUBLISHING IS A SCREENS-FLOW IDEA: there, publish is what
|
|
89
|
+
checks the design before somebody is walked through it.
|
|
90
|
+
A flow drawn in the designer has no such step, so it can
|
|
91
|
+
be opened as soon as it has one. */}
|
|
92
|
+
{(f.kind === 'screens' ? f.status === 'published' : (f.steps || []).length > 0) && f.key && (
|
|
93
|
+
<Link to={`/journey/${f.key}`}>Open</Link>
|
|
94
|
+
)}
|
|
73
95
|
</td>
|
|
74
96
|
</tr>
|
|
75
97
|
))}
|