@xeplr/cli 0.1.2 → 0.1.4
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 +37 -13
- package/bin/xeplr.js +10 -4
- package/lib/generate/index.js +121 -3
- package/lib/nextSteps.js +33 -18
- package/lib/plan.js +8 -0
- package/lib/questions.js +60 -1
- package/lib/setup.js +59 -0
- package/package.json +2 -1
- package/templates/CLAUDE.md +57 -0
- package/templates/api/app.js +31 -1
- package/templates/api/bin/www +19 -4
- package/templates/api/db/setup.js +1 -0
- package/templates/api/development.env.tpl +8 -2
- package/templates/api/env.required.js +1 -1
- package/templates/api/migrations/README.md +9 -0
- package/templates/api/migrations-auth/0003_forms_menu.sql +19 -0
- package/templates/api/models/index.js +7 -6
- package/templates/api/package.json +5 -2
- package/templates/api/routes/access.js +25 -0
- package/templates/api/routes/index.js +6 -15
- package/templates/api/screens/index.js +39 -0
- package/templates/api/screens/task/task-edit.screen.json +103 -0
- package/templates/api/screens/task/task-list.screen.json +45 -0
- package/templates/api/screens/task/task.entity.json +10 -0
- package/templates/api/screens/task/task.hooks.js +37 -0
- package/templates/api/screens/task/task.model.js +29 -0
- package/templates/ui/package.json +4 -0
- package/templates/ui/src/App.jsx +22 -2
- package/templates/ui/src/api/factory.js +10 -0
- package/templates/ui/src/index.css +60 -0
- package/templates/ui/src/main.jsx +2 -0
- package/templates/ui/src/pages/EditTask.jsx +57 -0
- package/templates/ui/src/pages/FormDesigner.jsx +74 -0
- package/templates/ui/src/pages/FormRecords.jsx +24 -0
- package/templates/ui/src/pages/Forms.jsx +144 -0
- package/templates/ui/src/pages/Home.jsx +4 -0
- package/templates/ui/src/pages/Tasks.jsx +20 -94
- package/templates/ui/src/pages/TasksHelp.jsx +66 -50
- package/templates/ui/vite.config.js +1 -1
- package/templates-tenancy/api/migrations/0001_tenants.sql +8 -0
- package/templates-tenancy/api/migrations-auth/0004_switch_menu.sql +10 -0
- package/templates-tenancy/api/routes/tenants.js +126 -0
- package/templates-tenancy/api/tenancy.js +12 -0
- package/templates-tenancy/ui/src/pages/SelectScope.css +45 -0
- package/templates-tenancy/ui/src/pages/SelectScope.jsx +154 -0
- package/templates-tenancy/ui/src/tenancy.js +3 -0
- package/templates/api/migrations/0001_tasks.sql +0 -23
- package/templates/api/models/Task.js +0 -48
- package/templates/ui/src/api/tasks.js +0 -34
- package/templates/ui/src/useTasksController.js +0 -83
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// THE TASK FORM — and what the task screens do in the browser.
|
|
2
|
+
//
|
|
3
|
+
// The form itself is the "task_edit" screen, designed on the Forms page. This
|
|
4
|
+
// module holds its HOOKS, used by this page, by the Tasks list and by the
|
|
5
|
+
// popup the list opens.
|
|
6
|
+
import { useEffect, useState } from 'react'
|
|
7
|
+
import { FactoryScreen, FactoryHooks } from '@xeplr/ui-factory'
|
|
8
|
+
import { factory } from '../api/factory.js'
|
|
9
|
+
|
|
10
|
+
// ── WHAT THE TASK SCREENS DO IN THE BROWSER ──────────────────────────────
|
|
11
|
+
// Every method runs the default (super). Change the ones you need; leave the
|
|
12
|
+
// rest as they are.
|
|
13
|
+
//
|
|
14
|
+
// before change the input, then call super
|
|
15
|
+
// after call super, then use or change what it returns
|
|
16
|
+
// override do not call super
|
|
17
|
+
//
|
|
18
|
+
// ctx.screen says which screen is calling ("task_list" or "task_edit").
|
|
19
|
+
// Keep them as methods (save(values, ctx) { … }) — super does not work in
|
|
20
|
+
// arrow functions. Rules that must hold belong in the server's hooks
|
|
21
|
+
// (api/screens/task/task.hooks.js).
|
|
22
|
+
export class TaskHooks extends FactoryHooks {
|
|
23
|
+
/** A list's rows (ctx.many), or the record Edit opens (ctx.id). */
|
|
24
|
+
get(ctx) {
|
|
25
|
+
return super.get(ctx)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Every autosave — keep it quick. Returns the saved record. */
|
|
29
|
+
save(values, ctx) {
|
|
30
|
+
return super.save(values, ctx)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A list row, after the person confirmed. */
|
|
34
|
+
delete(record, ctx) {
|
|
35
|
+
return super.delete(record, ctx)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Extra row buttons beside Edit / Delete: [{ label, onClick: (record, ctx) => … }]. ctx.refresh() reloads. */
|
|
39
|
+
actions(ctx) {
|
|
40
|
+
return super.actions(ctx)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const taskHooks = new TaskHooks()
|
|
45
|
+
|
|
46
|
+
/** The task form on a page of its own: <EditTask /> for a new task, <EditTask record={task} /> for one. */
|
|
47
|
+
export default function EditTask({ record }) {
|
|
48
|
+
const [screen, setScreen] = useState(null)
|
|
49
|
+
const [error, setError] = useState(null)
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
factory.loadScreen('task_edit').then(setScreen, (e) => setError(e.message))
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
if (error) return <div className="app-error">Could not load the task form — {error}</div>
|
|
56
|
+
return screen ? <FactoryScreen document={screen} record={record} {...factory.screenProps} hooks={taskHooks} /> : null
|
|
57
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// DESIGN ONE FORM — its add / edit form, or its list. Move and resize fields,
|
|
2
|
+
// add new ones, change labels, rules, fonts and colours.
|
|
3
|
+
//
|
|
4
|
+
// Edits are kept as a DRAFT automatically; nobody sees them until Publish,
|
|
5
|
+
// which also changes the table to match: a new field adds a column; removing
|
|
6
|
+
// one asks first, because its column and every value in it would be deleted.
|
|
7
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
8
|
+
import { useNavigate, useParams } from 'react-router-dom'
|
|
9
|
+
import { FactoryBuilder } from '@xeplr/ui-factory'
|
|
10
|
+
import { factory } from '../api/factory.js'
|
|
11
|
+
|
|
12
|
+
const PARTS = [
|
|
13
|
+
{ part: 'form', suffix: '_edit', label: 'Form' },
|
|
14
|
+
{ part: 'list', suffix: '_list', label: 'List' }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export default function FormDesigner() {
|
|
18
|
+
const { form, part } = useParams()
|
|
19
|
+
const navigate = useNavigate()
|
|
20
|
+
const screenKey = form + (part === 'list' ? '_list' : '_edit')
|
|
21
|
+
const [current, setCurrent] = useState(null) // { key, document, lockedNames }
|
|
22
|
+
const [error, setError] = useState(null)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
let cancelled = false
|
|
26
|
+
setCurrent(null)
|
|
27
|
+
// The draft if there is one, else the published version — with the field
|
|
28
|
+
// names that are already columns, which cannot be renamed.
|
|
29
|
+
factory.loadDraft(screenKey).then(
|
|
30
|
+
(row) => { if (!cancelled) { setCurrent({ key: screenKey, document: row.document, lockedNames: row.lockedNames || [] }); setError(null) } },
|
|
31
|
+
(e) => { if (!cancelled) setError(e.message) }
|
|
32
|
+
)
|
|
33
|
+
return () => { cancelled = true }
|
|
34
|
+
}, [screenKey])
|
|
35
|
+
|
|
36
|
+
const publish = useCallback(async (doc, options) => {
|
|
37
|
+
const result = await factory.publish(doc, options)
|
|
38
|
+
const row = await factory.loadDraft(doc.id)
|
|
39
|
+
setCurrent((c) => (c && c.key === doc.id ? { ...c, lockedNames: row.lockedNames || [] } : c))
|
|
40
|
+
return result
|
|
41
|
+
}, [])
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="app-designer">
|
|
45
|
+
<div className="app-designer-bar">
|
|
46
|
+
<button type="button" className="app-btn app-btn-quiet" onClick={() => navigate('/forms')}>← Forms</button>
|
|
47
|
+
<span className="app-designer-title">{current ? current.document.name : form}</span>
|
|
48
|
+
{PARTS.map((p) => (
|
|
49
|
+
<button
|
|
50
|
+
key={p.part}
|
|
51
|
+
type="button"
|
|
52
|
+
className={'app-btn app-btn-quiet' + ((part === 'list') === (p.part === 'list') ? ' is-active' : '')}
|
|
53
|
+
onClick={() => navigate('/forms/' + form + '/design/' + p.part)}
|
|
54
|
+
>
|
|
55
|
+
{p.label}
|
|
56
|
+
</button>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
{error && <div className="app-error">{error}</div>}
|
|
60
|
+
{current && (
|
|
61
|
+
<div className="app-designer-canvas">
|
|
62
|
+
<FactoryBuilder
|
|
63
|
+
key={current.key}
|
|
64
|
+
{...factory.builderProps}
|
|
65
|
+
document={current.document}
|
|
66
|
+
lockedNames={current.lockedNames}
|
|
67
|
+
onPublish={publish}
|
|
68
|
+
screens={PARTS.map((p) => ({ id: form + p.suffix, name: p.label }))}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// USE ANY FORM — its published list, with New / Edit opening the form in a
|
|
2
|
+
// popup. Tasks.jsx is the same thing for one form, with a help panel beside it.
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useParams } from 'react-router-dom'
|
|
5
|
+
import { FactoryScreen } from '@xeplr/ui-factory'
|
|
6
|
+
import { factory } from '../api/factory.js'
|
|
7
|
+
|
|
8
|
+
export default function FormRecords() {
|
|
9
|
+
const { form } = useParams()
|
|
10
|
+
const [screen, setScreen] = useState(null)
|
|
11
|
+
const [error, setError] = useState(null)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setScreen(null)
|
|
15
|
+
factory.loadScreen(form + '_list').then((doc) => { setScreen(doc); setError(null) }, (e) => setError(e.message))
|
|
16
|
+
}, [form])
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="app-page">
|
|
20
|
+
{error && <div className="app-error">Could not open this form — {error}</div>}
|
|
21
|
+
{screen && <FactoryScreen key={form} document={screen} {...factory.screenProps} />}
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// FORMS — every form in the app, for Super Admin: see them, make a new one,
|
|
2
|
+
// design it, publish it, open it.
|
|
3
|
+
//
|
|
4
|
+
// A form is two screens: a LIST of its records, and the add / edit FORM the
|
|
5
|
+
// list opens in a popup. Records live in an ordinary table named for the form
|
|
6
|
+
// ("farming_departments"), created when the form is first published.
|
|
7
|
+
//
|
|
8
|
+
// Claude can make forms too (api/screens/, `npx xeplr-factory screens`); this
|
|
9
|
+
// page is the same thing by hand.
|
|
10
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
11
|
+
import { useNavigate } from 'react-router-dom'
|
|
12
|
+
import { factory } from '../api/factory.js'
|
|
13
|
+
|
|
14
|
+
/** listScreens rows → one entry per form: { key, name, source, list, edit }. */
|
|
15
|
+
function groupForms(rows) {
|
|
16
|
+
const byKey = new Map()
|
|
17
|
+
rows.forEach((row) => {
|
|
18
|
+
const m = /^(.*)_(list|edit)$/.exec(row.screenKey)
|
|
19
|
+
const key = m ? m[1] : row.screenKey
|
|
20
|
+
const entry = byKey.get(key) || { key, name: null, source: null, list: null, edit: null }
|
|
21
|
+
if (m) entry[m[2]] = row
|
|
22
|
+
else entry.edit = row
|
|
23
|
+
if (m && m[2] === 'list') entry.name = row.name
|
|
24
|
+
entry.name = entry.name || row.name
|
|
25
|
+
entry.source = entry.source || row.source
|
|
26
|
+
byKey.set(key, entry)
|
|
27
|
+
})
|
|
28
|
+
return [...byKey.values()].sort((a, b) => String(a.name).localeCompare(String(b.name)))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function state(screen) {
|
|
32
|
+
if (!screen) return { label: '—', unpublished: false }
|
|
33
|
+
if (screen.version === null) return { label: 'Not published yet', unpublished: true }
|
|
34
|
+
return { label: 'v' + screen.version + (screen.hasDraft ? ' · unpublished changes' : ''), unpublished: screen.hasDraft }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function Forms() {
|
|
38
|
+
const navigate = useNavigate()
|
|
39
|
+
const [forms, setForms] = useState(null)
|
|
40
|
+
const [error, setError] = useState(null)
|
|
41
|
+
const [notice, setNotice] = useState(null)
|
|
42
|
+
const [name, setName] = useState('')
|
|
43
|
+
const [busy, setBusy] = useState(null)
|
|
44
|
+
|
|
45
|
+
const load = useCallback(() => {
|
|
46
|
+
factory.listScreens().then((rows) => { setForms(groupForms(rows)); setError(null) }, (e) => setError(e.message))
|
|
47
|
+
}, [])
|
|
48
|
+
useEffect(load, [load])
|
|
49
|
+
|
|
50
|
+
const create = async (e) => {
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
setBusy('new')
|
|
53
|
+
try {
|
|
54
|
+
const made = await factory.createEntity({ entity: name })
|
|
55
|
+
// Straight into its form, to add the fields.
|
|
56
|
+
navigate('/forms/' + made.edit.replace(/_edit$/, '') + '/design/form')
|
|
57
|
+
} catch (err) {
|
|
58
|
+
setError(err.message)
|
|
59
|
+
setBusy(null)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The form first — publishing it creates or changes the table — then the list.
|
|
64
|
+
const publish = async (form) => {
|
|
65
|
+
setBusy(form.key)
|
|
66
|
+
setNotice(null)
|
|
67
|
+
try {
|
|
68
|
+
for (const screen of [form.edit, form.list]) {
|
|
69
|
+
if (!screen || (screen.version !== null && !screen.hasDraft)) continue
|
|
70
|
+
const row = await factory.loadDraft(screen.screenKey)
|
|
71
|
+
await factory.publish(row.document)
|
|
72
|
+
}
|
|
73
|
+
setNotice('Published "' + form.name + '"')
|
|
74
|
+
setError(null)
|
|
75
|
+
load()
|
|
76
|
+
} catch (err) {
|
|
77
|
+
// Removing a field drops its column — that is confirmed in the designer,
|
|
78
|
+
// where the columns and their values are shown.
|
|
79
|
+
setError(err.confirm
|
|
80
|
+
? 'Publishing "' + form.name + '" would remove columns and their data — open Design form and publish there to see what, and confirm.'
|
|
81
|
+
: err.message)
|
|
82
|
+
} finally {
|
|
83
|
+
setBusy(null)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="app-page app-forms">
|
|
89
|
+
<h1>Forms</h1>
|
|
90
|
+
<p className="app-page-lead">
|
|
91
|
+
Every form in the app. A new one starts with a Name field — add the rest in its design, then publish to create its table.
|
|
92
|
+
</p>
|
|
93
|
+
|
|
94
|
+
<form className="app-forms-new" onSubmit={create}>
|
|
95
|
+
<input
|
|
96
|
+
value={name}
|
|
97
|
+
onChange={(e) => setName(e.target.value)}
|
|
98
|
+
placeholder='New form — one record is a… e.g. "farming department"'
|
|
99
|
+
aria-label="New form name"
|
|
100
|
+
/>
|
|
101
|
+
<button type="submit" className="app-btn" disabled={!name.trim() || busy === 'new'}>
|
|
102
|
+
{busy === 'new' ? 'Creating…' : 'New form'}
|
|
103
|
+
</button>
|
|
104
|
+
</form>
|
|
105
|
+
|
|
106
|
+
{error && <div className="app-error">{error}</div>}
|
|
107
|
+
{notice && <div className="app-notice">{notice}</div>}
|
|
108
|
+
|
|
109
|
+
{forms && forms.length === 0 && <p className="app-sub">No forms yet.</p>}
|
|
110
|
+
{forms && forms.length > 0 && (
|
|
111
|
+
<table className="app-table">
|
|
112
|
+
<thead>
|
|
113
|
+
<tr><th>Name</th><th>Table</th><th>Form</th><th>List</th><th /></tr>
|
|
114
|
+
</thead>
|
|
115
|
+
<tbody>
|
|
116
|
+
{forms.map((form) => {
|
|
117
|
+
const edit = state(form.edit)
|
|
118
|
+
const list = state(form.list)
|
|
119
|
+
const canOpen = form.list && form.list.version !== null
|
|
120
|
+
return (
|
|
121
|
+
<tr key={form.key}>
|
|
122
|
+
<td><strong>{form.name || form.key}</strong><div className="app-sub">{form.key}</div></td>
|
|
123
|
+
<td><code>{form.source || '—'}</code></td>
|
|
124
|
+
<td className={edit.unpublished ? 'app-forms-pending' : undefined}>{edit.label}</td>
|
|
125
|
+
<td className={list.unpublished ? 'app-forms-pending' : undefined}>{list.label}</td>
|
|
126
|
+
<td className="app-row-actions">
|
|
127
|
+
<button className="app-btn app-btn-quiet" disabled={!canOpen} title={canOpen ? '' : 'Publish it first'} onClick={() => navigate('/forms/' + form.key)}>Open</button>
|
|
128
|
+
{form.edit && <button className="app-btn app-btn-quiet" onClick={() => navigate('/forms/' + form.key + '/design/form')}>Design form</button>}
|
|
129
|
+
{form.list && <button className="app-btn app-btn-quiet" onClick={() => navigate('/forms/' + form.key + '/design/list')}>Design list</button>}
|
|
130
|
+
{(edit.unpublished || list.unpublished) && (
|
|
131
|
+
<button className="app-btn" disabled={busy === form.key} onClick={() => publish(form)}>
|
|
132
|
+
{busy === form.key ? 'Publishing…' : 'Publish'}
|
|
133
|
+
</button>
|
|
134
|
+
)}
|
|
135
|
+
</td>
|
|
136
|
+
</tr>
|
|
137
|
+
)
|
|
138
|
+
})}
|
|
139
|
+
</tbody>
|
|
140
|
+
</table>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -58,6 +58,10 @@ export default function Home() {
|
|
|
58
58
|
<code>public/logo.svg</code> and <code>public/logo-wide.svg</code> — the
|
|
59
59
|
sample logo, in the top bar and the side rail.
|
|
60
60
|
</li>
|
|
61
|
+
<li>
|
|
62
|
+
<strong>Forms</strong>, in the side rail — make a new form, or change the
|
|
63
|
+
sample task screens. Publish creates or changes the table too.
|
|
64
|
+
</li>
|
|
61
65
|
<li>
|
|
62
66
|
<code>src/App.jsx</code> — add your own pages, and the side-rail items
|
|
63
67
|
that open them.
|
|
@@ -1,105 +1,31 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
1
|
+
// THE SAMPLE — a list of tasks. New and Edit open the task form in a popup,
|
|
2
|
+
// where it saves itself as you type; there is no Save button.
|
|
3
|
+
//
|
|
4
|
+
// Nothing about tasks is written in this file. The list and the form are
|
|
5
|
+
// SCREENS, designed on the Forms page and stored by the API; this page only
|
|
6
|
+
// loads the published "task_list" and hands it the calls it needs — and the
|
|
7
|
+
// task hooks (EditTask.jsx), which the popup uses too.
|
|
8
|
+
import { useEffect, useState } from 'react'
|
|
9
|
+
import { FactoryScreen } from '@xeplr/ui-factory'
|
|
10
|
+
import { factory } from '../api/factory.js'
|
|
4
11
|
import TasksHelp from './TasksHelp.jsx'
|
|
5
|
-
|
|
6
|
-
const STATUSES = [
|
|
7
|
-
{ value: 'todo', label: 'To do' },
|
|
8
|
-
{ value: 'in_progress', label: 'In progress' },
|
|
9
|
-
{ value: 'done', label: 'Done' }
|
|
10
|
-
]
|
|
12
|
+
import { taskHooks } from './EditTask.jsx'
|
|
11
13
|
|
|
12
14
|
export default function Tasks() {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
+
const [screen, setScreen] = useState(null)
|
|
16
|
+
const [error, setError] = useState(null)
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
factory.loadScreen('task_list').then(setScreen, (e) => setError(e.message))
|
|
20
|
+
}, [])
|
|
15
21
|
|
|
16
22
|
return (
|
|
17
23
|
<div className="app-page app-page-split">
|
|
18
24
|
<div className="app-page-main">
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
</p>
|
|
23
|
-
|
|
24
|
-
{error && <div className="app-error">{error}</div>}
|
|
25
|
-
|
|
26
|
-
<form className="app-form" onSubmit={submit}>
|
|
27
|
-
<label className="app-field">
|
|
28
|
-
<span>Title</span>
|
|
29
|
-
<input
|
|
30
|
-
value={form.title}
|
|
31
|
-
onChange={(e) => setField('title', e.target.value)}
|
|
32
|
-
placeholder="What needs doing?"
|
|
33
|
-
required
|
|
34
|
-
/>
|
|
35
|
-
</label>
|
|
36
|
-
|
|
37
|
-
<label className="app-field">
|
|
38
|
-
<span>Description</span>
|
|
39
|
-
<textarea
|
|
40
|
-
rows={2}
|
|
41
|
-
value={form.description}
|
|
42
|
-
onChange={(e) => setField('description', e.target.value)}
|
|
43
|
-
placeholder="Optional"
|
|
44
|
-
/>
|
|
45
|
-
</label>
|
|
46
|
-
|
|
47
|
-
<div className="app-field-row">
|
|
48
|
-
<label className="app-field">
|
|
49
|
-
<span>Status</span>
|
|
50
|
-
<select value={form.status} onChange={(e) => setField('status', e.target.value)}>
|
|
51
|
-
{STATUSES.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}
|
|
52
|
-
</select>
|
|
53
|
-
</label>
|
|
54
|
-
|
|
55
|
-
<label className="app-field">
|
|
56
|
-
<span>Due date</span>
|
|
57
|
-
<input
|
|
58
|
-
type="date"
|
|
59
|
-
value={form.dueDate}
|
|
60
|
-
onChange={(e) => setField('dueDate', e.target.value)}
|
|
61
|
-
/>
|
|
62
|
-
</label>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div className="app-form-actions">
|
|
66
|
-
<button type="submit" className="app-btn" disabled={busy}>
|
|
67
|
-
{editingId ? 'Save changes' : 'Add task'}
|
|
68
|
-
</button>
|
|
69
|
-
{editingId && (
|
|
70
|
-
<button type="button" className="app-btn app-btn-quiet" onClick={cancel}>
|
|
71
|
-
Cancel
|
|
72
|
-
</button>
|
|
73
|
-
)}
|
|
74
|
-
</div>
|
|
75
|
-
</form>
|
|
76
|
-
|
|
77
|
-
<table className="app-table">
|
|
78
|
-
<thead>
|
|
79
|
-
<tr><th>Title</th><th>Status</th><th>Due</th><th /></tr>
|
|
80
|
-
</thead>
|
|
81
|
-
<tbody>
|
|
82
|
-
{tasks.length === 0 && (
|
|
83
|
-
<tr><td colSpan={4} className="app-empty">No tasks yet. Add one above.</td></tr>
|
|
84
|
-
)}
|
|
85
|
-
{tasks.map((t) => (
|
|
86
|
-
<tr key={t.id} className={editingId === t.id ? 'is-editing' : undefined}>
|
|
87
|
-
<td>
|
|
88
|
-
<strong>{t.title}</strong>
|
|
89
|
-
{t.description && <div className="app-sub">{t.description}</div>}
|
|
90
|
-
</td>
|
|
91
|
-
<td><span className={'app-tag is-' + t.status}>{t.status.replace('_', ' ')}</span></td>
|
|
92
|
-
<td>{t.dueDate ? String(t.dueDate).slice(0, 10) : '—'}</td>
|
|
93
|
-
<td className="app-row-actions">
|
|
94
|
-
<button className="app-btn app-btn-quiet" onClick={() => edit(t)}>Edit</button>
|
|
95
|
-
<button className="app-btn app-btn-quiet" onClick={() => remove(t.id)}>Delete</button>
|
|
96
|
-
</td>
|
|
97
|
-
</tr>
|
|
98
|
-
))}
|
|
99
|
-
</tbody>
|
|
100
|
-
</table>
|
|
25
|
+
{error && <div className="app-error">Could not load the task list — {error}</div>}
|
|
26
|
+
{/* loadScreen lets the list's Edit / New fetch "task_edit" when first opened. */}
|
|
27
|
+
{screen && <FactoryScreen document={screen} {...factory.screenProps} hooks={taskHooks} />}
|
|
101
28
|
</div>
|
|
102
|
-
|
|
103
29
|
<TasksHelp />
|
|
104
30
|
</div>
|
|
105
31
|
)
|
|
@@ -1,64 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
// The panel beside the sample. It is here so the first thing anybody builds is
|
|
4
|
+
// explained where they are looking, rather than in a document they have to go
|
|
5
|
+
// and find.
|
|
6
|
+
|
|
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.'
|
|
9
|
+
|
|
4
10
|
export default function TasksHelp() {
|
|
11
|
+
const [copied, setCopied] = useState(false)
|
|
12
|
+
const copy = () => {
|
|
13
|
+
navigator.clipboard.writeText(PROMPT).then(() => {
|
|
14
|
+
setCopied(true)
|
|
15
|
+
setTimeout(() => setCopied(false), 2000)
|
|
16
|
+
}, () => {})
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
return (
|
|
6
20
|
<aside className="app-help">
|
|
7
21
|
<div className="app-help-badge">Sample</div>
|
|
8
|
-
<h2>
|
|
22
|
+
<h2>Create your own UI with Claude</h2>
|
|
9
23
|
<p>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
24
|
+
Open Claude in this project's folder (the one with <code>api</code> and{' '}
|
|
25
|
+
<code>ui</code>) and give it this — change the name and the fields:
|
|
26
|
+
</p>
|
|
27
|
+
<pre className="app-help-prompt">{PROMPT}</pre>
|
|
28
|
+
<button type="button" className="app-btn app-btn-quiet" onClick={copy}>
|
|
29
|
+
{copied ? 'Copied' : 'Copy prompt'}
|
|
30
|
+
</button>
|
|
31
|
+
<p>
|
|
32
|
+
Add behaviour in the same message if you want it — "a Mark active button
|
|
33
|
+
on each row", "Region is required when Area is over 100". When Claude is
|
|
34
|
+
done, restart the sign-in service and the API: the menu item appears and
|
|
35
|
+
the table is created.
|
|
13
36
|
</p>
|
|
14
37
|
|
|
15
|
-
<h3>
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
<code>api/migrations-auth/000N_things_menu.sql</code>
|
|
49
|
-
<span>
|
|
50
|
-
The side-rail item appears only if its name exists in the database,
|
|
51
|
-
spelled exactly the same.
|
|
52
|
-
</span>
|
|
53
|
-
</li>
|
|
54
|
-
</ol>
|
|
38
|
+
<h3>This page is an example</h3>
|
|
39
|
+
<p>
|
|
40
|
+
A task has a title, a status, a due date and a description. Neither the
|
|
41
|
+
list nor the form is written by hand — both are <strong>screens</strong>,
|
|
42
|
+
and the tasks are saved in an ordinary <code>tasks</code> table with one
|
|
43
|
+
column per field.
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<h3>Change it</h3>
|
|
47
|
+
<p>
|
|
48
|
+
Open <strong>Forms</strong> in the side rail and{' '}
|
|
49
|
+
<strong>Design form</strong> on Tasks. Add a field, move one, change a
|
|
50
|
+
label or a colour — then <strong>Publish</strong>. The table gains its
|
|
51
|
+
new column as you publish.
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
<h3>Or by hand</h3>
|
|
55
|
+
<p>
|
|
56
|
+
<strong>Forms → New form</strong>: name it, design it, publish it, open
|
|
57
|
+
it — no code. The full steps for a form with its own page, hooks and
|
|
58
|
+
model are in <code>CLAUDE.md</code>, under "Create a new UI".
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
<h3>More than saving</h3>
|
|
62
|
+
<p>
|
|
63
|
+
In the browser — a value added before saving, rows filtered for display,
|
|
64
|
+
an extra button on each row: <code>ui/src/pages/EditTask.jsx</code>.
|
|
65
|
+
Every method calls <code>super</code>; change the one you need.
|
|
66
|
+
</p>
|
|
67
|
+
<p>
|
|
68
|
+
On the server — a check against other records, an email afterwards, a
|
|
69
|
+
list only managers see: <code>api/screens/task/task.hooks.js</code>.
|
|
70
|
+
</p>
|
|
55
71
|
|
|
56
72
|
<h3>Two things that catch people</h3>
|
|
57
73
|
<ul className="app-help-notes">
|
|
58
74
|
<li>
|
|
59
|
-
|
|
60
|
-
<code
|
|
61
|
-
|
|
75
|
+
Once a screen is published, the <em>database</em> holds its design.
|
|
76
|
+
Editing its <code>.screen.json</code> afterwards changes nothing — use
|
|
77
|
+
Forms.
|
|
62
78
|
</li>
|
|
63
79
|
<li>
|
|
64
80
|
A new menu entry is remembered for ten minutes. Restart the sign-in
|
|
@@ -43,7 +43,7 @@ export default defineConfig(({ mode }) => {
|
|
|
43
43
|
plugins: [react()],
|
|
44
44
|
// Force every import to THIS app's copy, so hook-using libraries don't
|
|
45
45
|
// load twice and break the rules of hooks in ways that read as impossible.
|
|
46
|
-
resolve: { dedupe: ['react', 'react-dom', 'react-router-dom'] },
|
|
46
|
+
resolve: { dedupe: ['react', 'react-dom', 'react-router-dom', '@xeplr/ui-canvas', '@xeplr/ui-table'] },
|
|
47
47
|
server: { port, strictPort: true, proxy },
|
|
48
48
|
preview: { port, strictPort: true }
|
|
49
49
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- The tables the picker lists and adds to: __MT_LEVEL_NAMES__.
|
|
2
|
+
--
|
|
3
|
+
-- Deliberately plain — an id and a name. Add what your app needs (an address,
|
|
4
|
+
-- a plan) with a later migration. Who may use one is NOT here: that is a
|
|
5
|
+
-- membership row in the sign-in database (userTenantsMapping), made for
|
|
6
|
+
-- whoever adds it.
|
|
7
|
+
|
|
8
|
+
__MT_TABLES_SQL__
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- "__MT_SWITCH_NAME__" — the drawer item that goes back to the picker.
|
|
2
|
+
--
|
|
3
|
+
-- Public: everybody who signs in works inside one, so everybody can change it.
|
|
4
|
+
-- The name must match ui/src/App.jsx's drawerItems entry EXACTLY.
|
|
5
|
+
|
|
6
|
+
INSERT INTO "menus" (id, name, "menuGroup", "isPublic", "isActive", "mtId1", "recordCreatedDate", "recordModifiedDate")
|
|
7
|
+
SELECT encode(gen_random_bytes(12), 'hex'), '__MT_SWITCH_NAME__', '', true, true, '*', now(), now()
|
|
8
|
+
WHERE NOT EXISTS (SELECT 1 FROM "menus" WHERE name = '__MT_SWITCH_NAME__');
|
|
9
|
+
|
|
10
|
+
UPDATE "menus" SET "isPublic" = true WHERE name = '__MT_SWITCH_NAME__';
|