@xeplr/ui-factory 1.0.3 → 1.0.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/package.json +1 -1
- package/src/designs/factory.css +8 -5
- package/src/remote.js +35 -18
- package/src/useFactoryScreen.js +24 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/ui-factory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Design data-entry screens on @xeplr/ui-canvas, save them as JSON, run them as auto-saving forms with lists of saved records — built for Claude to draft and people to refine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/designs/factory.css
CHANGED
|
@@ -50,8 +50,11 @@
|
|
|
50
50
|
.xeplr-factory-required { color: var(--xf-danger); }
|
|
51
51
|
.xeplr-factory-input {
|
|
52
52
|
flex: 1; min-height: 0; width: 100%; box-sizing: border-box;
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
/* The screen's own colours, never the host theme's input tokens: a dark theme
|
|
54
|
+
would give the box a dark background while the text stays the screen's
|
|
55
|
+
dark ink — unreadable. A screen's look is its style properties. */
|
|
56
|
+
font: inherit; color: var(--xf-ink); background: var(--xf-surface);
|
|
57
|
+
border: 1px solid var(--xf-border); border-radius: calc(6 * var(--xf-u));
|
|
55
58
|
padding: 0 calc(10 * var(--xf-u));
|
|
56
59
|
}
|
|
57
60
|
.xeplr-factory-textarea { padding-block: calc(8 * var(--xf-u)); resize: none; }
|
|
@@ -89,7 +92,7 @@
|
|
|
89
92
|
.xeplr-factory-list-mock { width: 100%; border-collapse: collapse; }
|
|
90
93
|
.xeplr-factory-list-mock th { text-align: left; font-weight: 600; font-size: .9em; padding: calc(6 * var(--xf-u)) calc(10 * var(--xf-u)); border-bottom: 1px solid var(--xf-border); color: var(--xf-muted); }
|
|
91
94
|
.xeplr-factory-list-mock td { padding: calc(8 * var(--xf-u)) calc(10 * var(--xf-u)); border-bottom: 1px solid var(--xf-border); }
|
|
92
|
-
.xeplr-factory-list-bar { display: block; height: calc(8 * var(--xf-u)); width: 70%; border-radius: 4px; background:
|
|
95
|
+
.xeplr-factory-list-bar { display: block; height: calc(8 * var(--xf-u)); width: 70%; border-radius: 4px; background: #ecebe4; }
|
|
93
96
|
.xeplr-factory-list .xeplr-table-row.xeplr-factory-row-open td,
|
|
94
97
|
.xeplr-factory-list tr.xeplr-factory-row-open td { background: color-mix(in srgb, var(--xf-accent) 10%, transparent); }
|
|
95
98
|
|
|
@@ -177,8 +180,8 @@
|
|
|
177
180
|
.xeplr-factory-prop { margin-bottom: 10px; }
|
|
178
181
|
.xeplr-factory-prop-label { display: block; margin-bottom: 3px; font-weight: 600; font-size: 12px; }
|
|
179
182
|
.xeplr-factory-prop-input {
|
|
180
|
-
width: 100%; box-sizing: border-box; font: inherit; padding: 5px 7px;
|
|
181
|
-
border: 1px solid var(--xf-border); border-radius: 5px; background: var(--
|
|
183
|
+
width: 100%; box-sizing: border-box; font: inherit; padding: 5px 7px;
|
|
184
|
+
border: 1px solid var(--xf-border); border-radius: 5px; background: #fff; color: var(--xf-ink, #1a1a19);
|
|
182
185
|
}
|
|
183
186
|
.xeplr-factory-prop.has-error .xeplr-factory-prop-input { border-color: var(--xf-danger); }
|
|
184
187
|
.xeplr-factory-prop-help { margin-top: 3px; color: var(--xf-muted); font-size: 11px; line-height: 1.4; }
|
package/src/remote.js
CHANGED
|
@@ -16,27 +16,44 @@
|
|
|
16
16
|
export function createFactoryApi({ fetch: doFetch, base = '' } = {}) {
|
|
17
17
|
if (typeof doFetch !== 'function') throw new Error('createFactoryApi: pass { fetch } — usually authFetch from @xeplr/ui-account')
|
|
18
18
|
|
|
19
|
+
// Two kinds of fetch are accepted, because both are what apps have:
|
|
20
|
+
// window.fetch-like → a Response: read it, and throw on !res.ok
|
|
21
|
+
// authFetch-like → the PARSED body, and on failure it throws an Error
|
|
22
|
+
// carrying status and the body (err.status, err.body)
|
|
23
|
+
// Either way a failure becomes the same Error, with the server's fields,
|
|
24
|
+
// confirm and refused lists on it.
|
|
19
25
|
async function call(method, path, body) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
err.status = res.status
|
|
30
|
-
err.fields = json && json.error && json.error.fields
|
|
31
|
-
const extra = (json && json.dataArray && json.dataArray[0]) || {}
|
|
32
|
-
// Publish: columns it would drop, waiting for a yes — and what it keeps.
|
|
33
|
-
err.confirm = extra.confirm
|
|
34
|
-
err.keep = extra.keep
|
|
35
|
-
err.refused = extra.refused
|
|
36
|
-
if (extra.statements) err.detail = extra.statements.join('\n')
|
|
26
|
+
let res
|
|
27
|
+
try {
|
|
28
|
+
res = await doFetch(`${base}/factory${path}`, {
|
|
29
|
+
method,
|
|
30
|
+
headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
|
|
31
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
32
|
+
})
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err && err.body !== undefined && err.status) throw failure(method, path, err.status, err.body, err.message)
|
|
37
35
|
throw err
|
|
38
36
|
}
|
|
39
|
-
|
|
37
|
+
if (res && typeof res.json === 'function' && typeof res.ok === 'boolean') {
|
|
38
|
+
let json = null
|
|
39
|
+
try { json = await res.json() } catch (_) { /* not JSON — reported below */ }
|
|
40
|
+
if (!res.ok) throw failure(method, path, res.status, json)
|
|
41
|
+
return json ? json.dataArray : []
|
|
42
|
+
}
|
|
43
|
+
return res && Array.isArray(res.dataArray) ? res.dataArray : []
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function failure(method, path, status, json, fallback) {
|
|
47
|
+
const err = new Error((json && json.message) || fallback || `${method} ${path} failed (${status})`)
|
|
48
|
+
err.status = status
|
|
49
|
+
err.fields = json && json.error && json.error.fields
|
|
50
|
+
const extra = (json && json.dataArray && json.dataArray[0]) || {}
|
|
51
|
+
// Publish: columns it would drop, waiting for a yes — and what it keeps.
|
|
52
|
+
err.confirm = extra.confirm
|
|
53
|
+
err.keep = extra.keep
|
|
54
|
+
err.refused = extra.refused
|
|
55
|
+
if (extra.statements) err.detail = extra.statements.join('\n')
|
|
56
|
+
return err
|
|
40
57
|
}
|
|
41
58
|
const one = (rows) => (Array.isArray(rows) ? rows[0] : rows)
|
|
42
59
|
const enc = encodeURIComponent
|
package/src/useFactoryScreen.js
CHANGED
|
@@ -53,11 +53,32 @@ export function useFactoryScreen({
|
|
|
53
53
|
// `department` is a dropdown whose 2 reads "Finance" — lives on the screen it
|
|
54
54
|
// edits in. Those fields are borrowed for display, and their table options
|
|
55
55
|
// loaded alongside this screen's own.
|
|
56
|
+
//
|
|
57
|
+
// An edit screen not passed in `screens` is fetched with loadScreen as soon
|
|
58
|
+
// as the list shows — not when Edit is first pressed — or the list's columns
|
|
59
|
+
// would show stored values ("in_progress") until then.
|
|
60
|
+
const [loadedScreens, setLoadedScreens] = useState({})
|
|
61
|
+
const knownScreens = useMemo(() => ({ ...loadedScreens, ...(screens || {}) }), [loadedScreens, screens])
|
|
62
|
+
const editScreenIds = check.ok && !inputs.length
|
|
63
|
+
? [...new Set(doc.nodes.filter((n) => n.type === 'list' && n.props.editScreen).map((n) => n.props.editScreen))].join('|')
|
|
64
|
+
: ''
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!editScreenIds || !loadScreen) return undefined
|
|
67
|
+
let cancelled = false
|
|
68
|
+
editScreenIds.split('|').filter((id) => !(screens && screens[id])).forEach((id) => {
|
|
69
|
+
loadScreen(id).then(
|
|
70
|
+
(found) => { if (!cancelled && found) setLoadedScreens((m) => (m[id] ? m : { ...m, [id]: found })) },
|
|
71
|
+
() => { /* shown when Edit is pressed, where the person is looking */ }
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
return () => { cancelled = true }
|
|
75
|
+
}, [editScreenIds]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
76
|
+
|
|
56
77
|
const fieldsFor = useCallback((node) => {
|
|
57
78
|
if (inputs.length || !node || !node.props.editScreen) return inputs
|
|
58
|
-
const editDoc =
|
|
79
|
+
const editDoc = knownScreens[node.props.editScreen]
|
|
59
80
|
return editDoc && validateDocument(editDoc, controls).ok ? inputNodes(editDoc, controls) : []
|
|
60
|
-
}, [inputs,
|
|
81
|
+
}, [inputs, knownScreens, controls])
|
|
61
82
|
const optionNodes = useMemo(() => {
|
|
62
83
|
if (!check.ok) return []
|
|
63
84
|
const borrowed = doc.nodes.filter((n) => n.type === 'list').flatMap((n) => (inputs.length ? [] : fieldsFor(n)))
|
|
@@ -290,7 +311,7 @@ export function useFactoryScreen({
|
|
|
290
311
|
|
|
291
312
|
// ── popup: a list's Edit / New in its edit screen ─────────────────────
|
|
292
313
|
const [popup, setPopup] = useState(null) // { node, screenId, record, document, loading, error }
|
|
293
|
-
const screensRef = useRef(
|
|
314
|
+
const screensRef = useRef(knownScreens); screensRef.current = knownScreens
|
|
294
315
|
const fetchRecordRef = useRef(fetchRecord); fetchRecordRef.current = fetchRecord
|
|
295
316
|
const loadScreenRef = useRef(loadScreen); loadScreenRef.current = loadScreen
|
|
296
317
|
|