@hanzo/ui 8.0.24 → 8.0.25
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 +59 -0
- package/dist/chunk-3NG33DV2.cjs +607 -0
- package/dist/chunk-3NG33DV2.cjs.map +1 -0
- package/dist/chunk-5CZOYONP.js +343 -0
- package/dist/chunk-5CZOYONP.js.map +1 -0
- package/dist/{chunk-P2P6ST6J.js → chunk-7ZNK6JSS.js} +9 -394
- package/dist/chunk-7ZNK6JSS.js.map +1 -0
- package/dist/chunk-GVCOAJLO.js +397 -0
- package/dist/chunk-GVCOAJLO.js.map +1 -0
- package/dist/chunk-GWNNLYX4.cjs +353 -0
- package/dist/chunk-GWNNLYX4.cjs.map +1 -0
- package/dist/{chunk-NBZDIMP3.cjs → chunk-ONVERVFG.cjs} +33 -438
- package/dist/chunk-ONVERVFG.cjs.map +1 -0
- package/dist/chunk-P2QU5MN5.js +555 -0
- package/dist/chunk-P2QU5MN5.js.map +1 -0
- package/dist/chunk-WHJR5S4L.cjs +418 -0
- package/dist/chunk-WHJR5S4L.cjs.map +1 -0
- package/dist/framework/CollectionBuilder.d.ts +12 -0
- package/dist/framework/CollectionsBrowser.d.ts +26 -19
- package/dist/framework/DocTypeRecords.d.ts +31 -11
- package/dist/framework/MediaGrid.d.ts +42 -0
- package/dist/framework/RecordCards.d.ts +35 -0
- package/dist/framework/builder-logic.d.ts +76 -0
- package/dist/framework/client.d.ts +31 -44
- package/dist/framework/core.cjs +213 -0
- package/dist/framework/core.cjs.map +1 -0
- package/dist/framework/core.d.ts +6 -0
- package/dist/framework/core.js +4 -0
- package/dist/framework/core.js.map +1 -0
- package/dist/framework/data.d.ts +2 -2
- package/dist/framework/fields.d.ts +47 -4
- package/dist/framework/index.cjs +1340 -0
- package/dist/framework/index.cjs.map +1 -0
- package/dist/framework/index.d.ts +9 -23
- package/dist/framework/index.js +1119 -0
- package/dist/framework/index.js.map +1 -0
- package/dist/framework/media.d.ts +53 -0
- package/dist/framework/parts.d.ts +74 -0
- package/dist/framework/responsive.d.ts +41 -0
- package/dist/framework/types.d.ts +13 -7
- package/dist/product/SelectMenu.d.ts +13 -1
- package/dist/product/index.cjs +2061 -336
- package/dist/product/index.cjs.map +1 -1
- package/dist/product/index.js +1909 -3
- package/dist/product/index.js.map +1 -1
- package/dist/product/social/index.cjs +17 -16
- package/dist/product/social/index.js +3 -2
- package/package.json +11 -5
- package/dist/chunk-2BM42AZR.cjs +0 -2301
- package/dist/chunk-2BM42AZR.cjs.map +0 -1
- package/dist/chunk-7P7R7FC5.js +0 -2231
- package/dist/chunk-7P7R7FC5.js.map +0 -1
- package/dist/chunk-NBZDIMP3.cjs.map +0 -1
- package/dist/chunk-P2P6ST6J.js.map +0 -1
- package/dist/framework/Loader.d.ts +0 -4
- package/dist/framework.cjs +0 -760
- package/dist/framework.cjs.map +0 -1
- package/dist/framework.d.ts +0 -1
- package/dist/framework.js +0 -730
- package/dist/framework.js.map +0 -1
package/README.md
CHANGED
|
@@ -34,6 +34,65 @@ Both layers are **presentational + data-injected**: the host supplies rows and p
|
|
|
34
34
|
|
|
35
35
|
> Each layer owns a `DataTable` — the product one is a generic typed `<T>` list; the data one is the field-driven record grid. Keeping the record layer on the `./data` subpath keeps each name unambiguous.
|
|
36
36
|
|
|
37
|
+
## The DocType renderer — one UI for every business app
|
|
38
|
+
|
|
39
|
+
`@hanzo/ui/framework` renders the **Hanzo Framework** DocType engine (`hanzoai/cloud
|
|
40
|
+
clients/framework`, live at `/v1/framework/*`; also published as
|
|
41
|
+
[`hanzoai/framework`](https://github.com/hanzoai/framework) over
|
|
42
|
+
[`hanzoai/doctype`](https://github.com/hanzoai/doctype)). The engine is
|
|
43
|
+
metadata-driven: a CMS Page, an ERP Sales Order, a Helpdesk Ticket and a CRM
|
|
44
|
+
Company are the same kind of thing — a DocType with typed fields. So they get the
|
|
45
|
+
same renderer. An **app lane is a `module` filter** over the DocType registry plus
|
|
46
|
+
its own copy; there is no per-lane list, form, detail or builder to drift.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { createFrameworkClient } from '@hanzo/ui/framework/core' // transport only
|
|
50
|
+
import { CollectionsBrowser, DocTypeRecords, DocTypeDetail } from '@hanzo/ui/framework'
|
|
51
|
+
|
|
52
|
+
// The HOST owns transport: paths are relative to the framework root, so the app
|
|
53
|
+
// decides the origin + the credential. Nothing in the library picks a URL.
|
|
54
|
+
const client = createFrameworkClient({
|
|
55
|
+
get: (p) => api.get(`/v1/framework/${p}`),
|
|
56
|
+
post: (p, b) => api.post(`/v1/framework/${p}`, b),
|
|
57
|
+
put: (p, b) => api.put(`/v1/framework/${p}`, b),
|
|
58
|
+
del: (p) => api.del(`/v1/framework/${p}`),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
<CollectionsBrowser client={client} module="erp" label="ERP" … onOpen={openCollection} />
|
|
62
|
+
<DocTypeRecords client={client} doctype="erp-sales" onOpen={…} onCreate={…} />
|
|
63
|
+
<DocTypeDetail client={client} doctype="erp-sales" name={name} onBack={…} onView={…} />
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Mobile first — a rule, not a fallback.** The layout is decided from the
|
|
67
|
+
CONTAINER's measured width (`onLayout`, so a narrow pane on a wide screen is still
|
|
68
|
+
a phone), and the answer to "not measured yet" is PHONE. So the first paint —
|
|
69
|
+
SSR included — is a stacked **card** per record; the `@hanzo/data` table is the
|
|
70
|
+
enhancement applied once the box proves it can hold one. Every control meets the
|
|
71
|
+
44px tap floor (WCAG 2.5.5) at phone width, and the table honors the DocType's own
|
|
72
|
+
`inListView` projection so a twelve-field document does not become a horizontal
|
|
73
|
+
scroll wall.
|
|
74
|
+
|
|
75
|
+
**Two entries, because they are two different things.** `./framework/core` is the
|
|
76
|
+
contract — wire types, the client, the metadata↔render mapping, the builder
|
|
77
|
+
projection, the media model. It imports no React and no `@hanzo/gui`, so a data
|
|
78
|
+
layer, a server route or a plain node test can bind the engine without loading a
|
|
79
|
+
component tree. `./framework` re-exports all of it plus the views.
|
|
80
|
+
|
|
81
|
+
**Ports, not bindings.** The client takes a `FrameworkTransport`; the DAM takes a
|
|
82
|
+
`MediaStore` (ensure-bucket / presign / put / delete) and a **bucket parameter**,
|
|
83
|
+
so ERP attachments never land in a bucket named after the CMS. Routing is
|
|
84
|
+
`onOpen`/`onCreate`/`onBack`/`onView` callbacks — no router import anywhere.
|
|
85
|
+
|
|
86
|
+
**One entry, not two.** 8.0.24 shipped a first `./framework` lift with the same
|
|
87
|
+
`createFrameworkClient(Transport)` shape but no builder, no DAM and no mobile
|
|
88
|
+
layout, injecting `renderBuilder`/`renderMedia` instead. 8.0.25 is the convergence:
|
|
89
|
+
the same entry, a superset implementation, and every 8.0.24 name still resolves —
|
|
90
|
+
`Transport` aliases `FrameworkTransport`, `Loader` aliases the in-flow `Loading`,
|
|
91
|
+
`setupDescription`/`setupBullets` stay optional (they now fall back to copy DERIVED
|
|
92
|
+
FROM THE LANE, which is the actual fix — the old default was one lane's words), and
|
|
93
|
+
`renderBuilder`/`renderMedia` still win when supplied. A host that passes neither
|
|
94
|
+
now gets the real built-in `CollectionBuilder`/`MediaGrid` instead of nothing.
|
|
95
|
+
|
|
37
96
|
## Motion
|
|
38
97
|
|
|
39
98
|
The calm motion vocabulary (`FadeIn`, drawer/backdrop transitions, skeleton shimmer, live pulse) ships as one stylesheet. Import it once at the app root:
|
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// src/framework/types.ts
|
|
5
|
+
var DRAFT = 0;
|
|
6
|
+
var SUBMITTED = 1;
|
|
7
|
+
var CANCELLED = 2;
|
|
8
|
+
var REDACTED_MARKER = "__set__";
|
|
9
|
+
|
|
10
|
+
// src/framework/client.ts
|
|
11
|
+
var enc = encodeURIComponent;
|
|
12
|
+
var asRecord = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
13
|
+
function rows(payload) {
|
|
14
|
+
if (Array.isArray(payload)) {
|
|
15
|
+
return payload.filter((x) => x && typeof x === "object");
|
|
16
|
+
}
|
|
17
|
+
const o = asRecord(payload);
|
|
18
|
+
for (const k of ["data", "items", "rows"]) {
|
|
19
|
+
if (Array.isArray(o[k])) {
|
|
20
|
+
return o[k].filter((x) => x && typeof x === "object");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
var asDoc = (v) => asRecord(v);
|
|
26
|
+
var asDocType = (v) => {
|
|
27
|
+
const r = asRecord(v);
|
|
28
|
+
return {
|
|
29
|
+
...r,
|
|
30
|
+
name: String(r.name ?? ""),
|
|
31
|
+
fields: Array.isArray(r.fields) ? r.fields : []
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
function listQuery(q) {
|
|
35
|
+
if (!q) return "";
|
|
36
|
+
const p = new URLSearchParams();
|
|
37
|
+
if (q.filters && Object.keys(q.filters).length) p.set("filters", JSON.stringify(q.filters));
|
|
38
|
+
if (q.fields && q.fields.length) p.set("fields", q.fields.join(","));
|
|
39
|
+
if (q.orderBy) p.set("order_by", q.orderBy);
|
|
40
|
+
if (q.limit) p.set("limit", String(q.limit));
|
|
41
|
+
const s = p.toString();
|
|
42
|
+
return s ? `?${s}` : "";
|
|
43
|
+
}
|
|
44
|
+
function createFrameworkClient(t) {
|
|
45
|
+
return {
|
|
46
|
+
doctypes: {
|
|
47
|
+
list: () => t.get("doctypes").then((r) => rows(r).map(asDocType)),
|
|
48
|
+
get: (name) => t.get(`doctypes/${enc(name)}`).then(asDocType),
|
|
49
|
+
create: (dt) => t.post("doctypes", dt).then(asDocType),
|
|
50
|
+
update: (name, dt) => t.put(`doctypes/${enc(name)}`, dt).then(asDocType),
|
|
51
|
+
remove: (name) => t.del(`doctypes/${enc(name)}`)
|
|
52
|
+
},
|
|
53
|
+
records: {
|
|
54
|
+
list: (doctype, q) => t.get(`${enc(doctype)}${listQuery(q)}`).then((r) => rows(r).map(asDoc)),
|
|
55
|
+
get: (doctype, name) => t.get(`${enc(doctype)}/${enc(name)}`).then(asDoc),
|
|
56
|
+
create: (doctype, data) => t.post(enc(doctype), data).then(asDoc),
|
|
57
|
+
update: (doctype, name, data) => t.put(`${enc(doctype)}/${enc(name)}`, data).then(asDoc),
|
|
58
|
+
remove: (doctype, name) => t.del(`${enc(doctype)}/${enc(name)}`),
|
|
59
|
+
submit: (doctype, name) => t.post(`${enc(doctype)}/${enc(name)}/submit`).then(asDoc),
|
|
60
|
+
cancel: (doctype, name) => t.post(`${enc(doctype)}/${enc(name)}/cancel`).then(asDoc)
|
|
61
|
+
},
|
|
62
|
+
modules: {
|
|
63
|
+
list: () => t.get("modules").then(
|
|
64
|
+
(r) => rows(r).map((m) => ({
|
|
65
|
+
module: String(m.module ?? ""),
|
|
66
|
+
doctypes: Array.isArray(m.doctypes) ? m.doctypes : []
|
|
67
|
+
}))
|
|
68
|
+
),
|
|
69
|
+
get: (module) => t.get(`modules/${enc(module)}`).then((v) => {
|
|
70
|
+
const r = asRecord(v);
|
|
71
|
+
return {
|
|
72
|
+
module: String(r.module ?? module),
|
|
73
|
+
doctypes: Array.isArray(r.doctypes) ? r.doctypes : [],
|
|
74
|
+
installed: Array.isArray(r.installed) ? r.installed : []
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
install: (module) => t.post(`modules/${enc(module)}/install`).then((v) => {
|
|
78
|
+
const r = asRecord(v);
|
|
79
|
+
return {
|
|
80
|
+
module: String(r.module ?? module),
|
|
81
|
+
created: Array.isArray(r.created) ? r.created : [],
|
|
82
|
+
existing: Array.isArray(r.existing) ? r.existing : []
|
|
83
|
+
};
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
roles: {
|
|
87
|
+
list: () => t.get("roles").then(
|
|
88
|
+
(r) => rows(r).map((x) => ({ user: String(x.user ?? ""), role: String(x.role ?? "") }))
|
|
89
|
+
),
|
|
90
|
+
assign: (user, role) => t.post("roles", { user, role }).then(() => void 0),
|
|
91
|
+
revoke: (user, role) => t.del(`roles/${enc(user)}/${enc(role)}`)
|
|
92
|
+
},
|
|
93
|
+
summary: () => t.get("summary").then(asRecord)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/framework/fields.ts
|
|
98
|
+
var TYPE_MAP = {
|
|
99
|
+
Data: "text",
|
|
100
|
+
SmallText: "text",
|
|
101
|
+
Text: "longText",
|
|
102
|
+
LongText: "longText",
|
|
103
|
+
RichText: "richText",
|
|
104
|
+
Int: "number",
|
|
105
|
+
Float: "number",
|
|
106
|
+
Currency: "currency",
|
|
107
|
+
Check: "boolean",
|
|
108
|
+
Date: "date",
|
|
109
|
+
Datetime: "dateTime",
|
|
110
|
+
Select: "select",
|
|
111
|
+
Link: "relation",
|
|
112
|
+
Table: "json",
|
|
113
|
+
Attach: "url",
|
|
114
|
+
JSON: "json",
|
|
115
|
+
Password: "text"
|
|
116
|
+
};
|
|
117
|
+
var ACRONYMS = /* @__PURE__ */ new Set(["id", "url", "seo", "api", "ip"]);
|
|
118
|
+
function humanize(name) {
|
|
119
|
+
return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim().split(/\s+/).filter(Boolean).map((w) => ACRONYMS.has(w.toLowerCase()) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
120
|
+
}
|
|
121
|
+
function selectOptions(options) {
|
|
122
|
+
return (options ?? "").split("\n").map((s) => s.replace(/\r$/, "").trim()).filter(Boolean).map((v) => ({ value: v, label: v }));
|
|
123
|
+
}
|
|
124
|
+
function metadataFor(f) {
|
|
125
|
+
switch (f.fieldtype) {
|
|
126
|
+
case "Select":
|
|
127
|
+
return { options: selectOptions(f.options) };
|
|
128
|
+
case "Currency":
|
|
129
|
+
return { currencyCode: "USD" };
|
|
130
|
+
case "Int":
|
|
131
|
+
return { decimals: 0 };
|
|
132
|
+
case "Link":
|
|
133
|
+
return { objectName: f.options };
|
|
134
|
+
default:
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function autonameSource(dt) {
|
|
139
|
+
const a = (dt.autoname ?? "").trim();
|
|
140
|
+
return a.startsWith("field:") ? a.slice("field:".length).trim() : "";
|
|
141
|
+
}
|
|
142
|
+
function docTypeToFields(dt, opts = {}) {
|
|
143
|
+
const src = autonameSource(dt);
|
|
144
|
+
return (dt.fields ?? []).filter((f) => !f.hidden).map((f) => {
|
|
145
|
+
const readOnly = f.readOnly || opts.editing && f.fieldname === src || void 0;
|
|
146
|
+
const metadata = metadataFor(f);
|
|
147
|
+
return {
|
|
148
|
+
name: f.fieldname,
|
|
149
|
+
label: f.label || humanize(f.fieldname),
|
|
150
|
+
type: TYPE_MAP[f.fieldtype] ?? "text",
|
|
151
|
+
...metadata ? { metadata } : {},
|
|
152
|
+
...readOnly ? { readOnly: true } : {}
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function statusField(dt) {
|
|
157
|
+
const f = (dt.fields ?? []).find(
|
|
158
|
+
(x) => x.fieldtype === "Select" && x.fieldname === "status" && (x.options ?? "").includes("Published")
|
|
159
|
+
);
|
|
160
|
+
return f ? f.fieldname : "";
|
|
161
|
+
}
|
|
162
|
+
function listHiddenFields(dt) {
|
|
163
|
+
const anyListed = (dt.fields ?? []).some((f) => f.inListView);
|
|
164
|
+
if (!anyListed) return [];
|
|
165
|
+
return (dt.fields ?? []).filter((f) => !f.inListView && !f.hidden).map((f) => f.fieldname);
|
|
166
|
+
}
|
|
167
|
+
function cardFields(dt, fields, max = 4) {
|
|
168
|
+
const title = dt.titleField ?? "";
|
|
169
|
+
const listed = new Set(listedFieldNames(dt));
|
|
170
|
+
const pick = listed.size ? fields.filter((f) => listed.has(f.name)) : fields;
|
|
171
|
+
return pick.filter((f) => f.name !== title).slice(0, max);
|
|
172
|
+
}
|
|
173
|
+
function listedFieldNames(dt) {
|
|
174
|
+
return (dt.fields ?? []).filter((f) => f.inListView && !f.hidden).map((f) => f.fieldname);
|
|
175
|
+
}
|
|
176
|
+
function isMediaDoctype(dt) {
|
|
177
|
+
return (dt.fields ?? []).some((f) => f.fieldtype === "Attach" && f.reqd);
|
|
178
|
+
}
|
|
179
|
+
function mediaFileField(dt) {
|
|
180
|
+
const f = (dt.fields ?? []).find((x) => x.fieldtype === "Attach" && x.reqd) ?? (dt.fields ?? []).find((x) => x.fieldtype === "Attach");
|
|
181
|
+
return f ? f.fieldname : "";
|
|
182
|
+
}
|
|
183
|
+
function mediaDocPayload(dt, facts) {
|
|
184
|
+
const declared = new Set((dt.fields ?? []).map((f) => f.fieldname));
|
|
185
|
+
const out = {};
|
|
186
|
+
const fileField = mediaFileField(dt);
|
|
187
|
+
if (fileField) out[fileField] = facts.fileRef;
|
|
188
|
+
const titleF = dt.titleField && declared.has(dt.titleField) ? dt.titleField : declared.has("title") ? "title" : "";
|
|
189
|
+
if (titleF && titleF !== fileField) out[titleF] = facts.filename;
|
|
190
|
+
const optional = [
|
|
191
|
+
["mime", facts.mime],
|
|
192
|
+
["size", facts.size],
|
|
193
|
+
["width", facts.width],
|
|
194
|
+
["height", facts.height]
|
|
195
|
+
];
|
|
196
|
+
for (const [k, v] of optional) if (declared.has(k) && k !== fileField && k !== titleF) out[k] = v;
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
function moduleDoctypes(dts, module) {
|
|
200
|
+
return dts.filter((dt) => (dt.module ?? "") === module);
|
|
201
|
+
}
|
|
202
|
+
var PROJECT_FIELD = "project";
|
|
203
|
+
function hasProjectField(dt) {
|
|
204
|
+
return (dt.fields ?? []).some((f) => f.fieldname === PROJECT_FIELD);
|
|
205
|
+
}
|
|
206
|
+
function isDraft(record) {
|
|
207
|
+
return Number(record?.docstatus ?? DRAFT) === DRAFT;
|
|
208
|
+
}
|
|
209
|
+
function titleOf(doc, dt) {
|
|
210
|
+
if (dt.titleField && typeof doc[dt.titleField] === "string" && doc[dt.titleField]) {
|
|
211
|
+
return String(doc[dt.titleField]);
|
|
212
|
+
}
|
|
213
|
+
return typeof doc.name === "string" && doc.name ? doc.name : "(untitled)";
|
|
214
|
+
}
|
|
215
|
+
function slugify(s) {
|
|
216
|
+
return String(s ?? "").normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
217
|
+
}
|
|
218
|
+
function isValidDoctypeName(name) {
|
|
219
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name.trim());
|
|
220
|
+
}
|
|
221
|
+
var secToMs = (v) => {
|
|
222
|
+
const n = Number(v);
|
|
223
|
+
return Number.isFinite(n) && n > 0 ? n * 1e3 : void 0;
|
|
224
|
+
};
|
|
225
|
+
var numOrNull = (v) => {
|
|
226
|
+
const n = Number(v);
|
|
227
|
+
return Number.isFinite(n) ? n : null;
|
|
228
|
+
};
|
|
229
|
+
function coerceForDisplay(f, v) {
|
|
230
|
+
switch (f.fieldtype) {
|
|
231
|
+
case "Currency":
|
|
232
|
+
return { amount: numOrNull(v), currencyCode: "USD" };
|
|
233
|
+
case "Check":
|
|
234
|
+
return v === 1 || v === true || v === "1";
|
|
235
|
+
case "Datetime":
|
|
236
|
+
return typeof v === "string" ? v.replace(" ", "T") : v;
|
|
237
|
+
case "Password":
|
|
238
|
+
return v === REDACTED_MARKER ? "" : v;
|
|
239
|
+
// never surface the marker
|
|
240
|
+
default:
|
|
241
|
+
return v;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function toRecord(doc, dt) {
|
|
245
|
+
const rec = {
|
|
246
|
+
id: doc.name,
|
|
247
|
+
name: doc.name,
|
|
248
|
+
doctype: doc.doctype,
|
|
249
|
+
docstatus: doc.docstatus,
|
|
250
|
+
createdAt: secToMs(doc.createdAt),
|
|
251
|
+
updatedAt: secToMs(doc.updatedAt)
|
|
252
|
+
};
|
|
253
|
+
for (const f of dt.fields ?? []) {
|
|
254
|
+
if (!(f.fieldname in doc)) continue;
|
|
255
|
+
rec[f.fieldname] = coerceForDisplay(f, doc[f.fieldname]);
|
|
256
|
+
}
|
|
257
|
+
return rec;
|
|
258
|
+
}
|
|
259
|
+
function coerceForWrite(f, v) {
|
|
260
|
+
switch (f.fieldtype) {
|
|
261
|
+
case "Currency":
|
|
262
|
+
return v && typeof v === "object" && "amount" in v ? Number(v.amount) : Number(v);
|
|
263
|
+
case "Check":
|
|
264
|
+
return Boolean(v);
|
|
265
|
+
case "Int":
|
|
266
|
+
case "Float":
|
|
267
|
+
return Number(v);
|
|
268
|
+
case "Datetime":
|
|
269
|
+
if (v instanceof Date) return v.toISOString();
|
|
270
|
+
if (typeof v === "number" && Number.isFinite(v)) return new Date(v).toISOString();
|
|
271
|
+
return v;
|
|
272
|
+
case "Link":
|
|
273
|
+
if (v == null || v === "") return "";
|
|
274
|
+
if (typeof v === "object") {
|
|
275
|
+
return String(v.id ?? v.value ?? "");
|
|
276
|
+
}
|
|
277
|
+
return v;
|
|
278
|
+
default:
|
|
279
|
+
return v;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function enrichLinks(record, dt, optionsByField) {
|
|
283
|
+
const out = { ...record };
|
|
284
|
+
for (const f of dt.fields ?? []) {
|
|
285
|
+
if (f.fieldtype !== "Link") continue;
|
|
286
|
+
const raw = out[f.fieldname];
|
|
287
|
+
if (typeof raw === "string" && raw) {
|
|
288
|
+
const opt = (optionsByField[f.fieldname] ?? []).find((o) => o.value === raw);
|
|
289
|
+
out[f.fieldname] = { id: raw, label: opt?.label ?? raw };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
function linkFields(dt) {
|
|
295
|
+
return (dt.fields ?? []).filter((f) => f.fieldtype === "Link");
|
|
296
|
+
}
|
|
297
|
+
function newDraft(dt) {
|
|
298
|
+
const out = {};
|
|
299
|
+
for (const f of dt.fields ?? []) {
|
|
300
|
+
if (f.default === void 0 || f.default === "") continue;
|
|
301
|
+
out[f.fieldname] = f.fieldtype === "Check" ? f.default === "1" || f.default === "true" : f.default;
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
function savePayload(draft, dt) {
|
|
306
|
+
const src = autonameSource(dt);
|
|
307
|
+
const out = {};
|
|
308
|
+
for (const f of dt.fields ?? []) {
|
|
309
|
+
if (f.readOnly) continue;
|
|
310
|
+
let v = draft[f.fieldname];
|
|
311
|
+
if (v === void 0) continue;
|
|
312
|
+
v = coerceForWrite(f, v);
|
|
313
|
+
if (f.fieldname === src && typeof v === "string") v = slugify(v);
|
|
314
|
+
out[f.fieldname] = v;
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/framework/builder-logic.ts
|
|
320
|
+
var BUILDER_FIELD_TYPES = [
|
|
321
|
+
{ type: "Data", label: "Text", hint: "Single line of text" },
|
|
322
|
+
{ type: "RichText", label: "Rich text", hint: "Formatted body (WYSIWYG)" },
|
|
323
|
+
{ type: "LongText", label: "Long text", hint: "Multi-line plain text" },
|
|
324
|
+
{ type: "Int", label: "Number", hint: "Whole number" },
|
|
325
|
+
{ type: "Float", label: "Decimal", hint: "Decimal number" },
|
|
326
|
+
{ type: "Currency", label: "Currency", hint: "Money amount" },
|
|
327
|
+
{ type: "Check", label: "Checkbox", hint: "True / false" },
|
|
328
|
+
{ type: "Date", label: "Date", hint: "Calendar date" },
|
|
329
|
+
{ type: "Datetime", label: "Date & time", hint: "Date with time" },
|
|
330
|
+
{ type: "Select", label: "Select", hint: "One of a list of options" },
|
|
331
|
+
{ type: "Link", label: "Relation", hint: "Reference another collection" },
|
|
332
|
+
{ type: "Attach", label: "Attachment", hint: "A file / image URL" },
|
|
333
|
+
{ type: "Table", label: "Table", hint: "Repeatable child rows (JSON)" },
|
|
334
|
+
{ type: "JSON", label: "JSON", hint: "Arbitrary JSON" }
|
|
335
|
+
];
|
|
336
|
+
var LABELS = Object.fromEntries(
|
|
337
|
+
BUILDER_FIELD_TYPES.map((t) => [t.type, t.label])
|
|
338
|
+
);
|
|
339
|
+
function fieldTypeLabel(t) {
|
|
340
|
+
return LABELS[t] ?? t;
|
|
341
|
+
}
|
|
342
|
+
function fieldNeedsOptions(t) {
|
|
343
|
+
return t === "Select" || t === "Link" || t === "Table";
|
|
344
|
+
}
|
|
345
|
+
var seq = 0;
|
|
346
|
+
function newKey() {
|
|
347
|
+
seq += 1;
|
|
348
|
+
return `f${seq}_${Math.random().toString(36).slice(2, 8)}`;
|
|
349
|
+
}
|
|
350
|
+
function blankField(type = "Data") {
|
|
351
|
+
return { key: newKey(), label: "", type, required: false, inListView: false, options: "" };
|
|
352
|
+
}
|
|
353
|
+
function starterFields() {
|
|
354
|
+
return [
|
|
355
|
+
{ key: newKey(), label: "Title", type: "Data", required: true, inListView: true, options: "" },
|
|
356
|
+
{ key: newKey(), label: "Slug", type: "Data", required: true, inListView: true, options: "" },
|
|
357
|
+
{ key: newKey(), label: "Body", type: "RichText", required: false, inListView: false, options: "" },
|
|
358
|
+
{ key: newKey(), label: "Status", type: "Select", required: false, inListView: true, options: "Draft\nPublished" },
|
|
359
|
+
// Optional project scope — lets a host's org→project switcher filter this collection.
|
|
360
|
+
{ key: newKey(), label: "Project", type: "Data", required: false, inListView: false, options: "" }
|
|
361
|
+
];
|
|
362
|
+
}
|
|
363
|
+
function fieldsFromDocType(dt) {
|
|
364
|
+
return (dt.fields ?? []).map((f) => ({
|
|
365
|
+
key: newKey(),
|
|
366
|
+
label: f.label || f.fieldname,
|
|
367
|
+
type: f.fieldtype,
|
|
368
|
+
required: Boolean(f.reqd),
|
|
369
|
+
inListView: Boolean(f.inListView),
|
|
370
|
+
options: f.options ?? ""
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
function fieldNameFromLabel(label) {
|
|
374
|
+
const slug = slugify(label).replace(/-/g, "_");
|
|
375
|
+
return /^[0-9]/.test(slug) ? `f_${slug}` : slug;
|
|
376
|
+
}
|
|
377
|
+
function validateBuilder(name, fields) {
|
|
378
|
+
const fieldErrors = {};
|
|
379
|
+
let formError = null;
|
|
380
|
+
if (!isValidDoctypeName(name.trim())) {
|
|
381
|
+
formError = "Enter a collection name with letters, digits, dashes or underscores (no spaces).";
|
|
382
|
+
} else if (fields.length === 0) {
|
|
383
|
+
formError = "Add at least one field.";
|
|
384
|
+
}
|
|
385
|
+
const seen = /* @__PURE__ */ new Set();
|
|
386
|
+
for (const f of fields) {
|
|
387
|
+
const label = f.label.trim();
|
|
388
|
+
if (!label) {
|
|
389
|
+
fieldErrors[f.key] = "Name this field.";
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const fname = fieldNameFromLabel(label);
|
|
393
|
+
if (!fname) {
|
|
394
|
+
fieldErrors[f.key] = "Use letters or digits in the field name.";
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (seen.has(fname)) {
|
|
398
|
+
fieldErrors[f.key] = `Duplicate field "${fname}".`;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
seen.add(fname);
|
|
402
|
+
if ((f.type === "Link" || f.type === "Table") && !f.options.trim()) {
|
|
403
|
+
fieldErrors[f.key] = f.type === "Link" ? "Choose the collection to relate to." : "Name the child collection.";
|
|
404
|
+
} else if (f.type === "Select" && !f.options.trim()) {
|
|
405
|
+
fieldErrors[f.key] = "Add at least one option.";
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return { ok: !formError && Object.keys(fieldErrors).length === 0, fieldErrors, formError };
|
|
409
|
+
}
|
|
410
|
+
function toDocField(f) {
|
|
411
|
+
const fieldname = fieldNameFromLabel(f.label);
|
|
412
|
+
const field = {
|
|
413
|
+
fieldname,
|
|
414
|
+
fieldtype: f.type,
|
|
415
|
+
label: f.label.trim()
|
|
416
|
+
};
|
|
417
|
+
if (f.required) field.reqd = true;
|
|
418
|
+
if (f.inListView) field.inListView = true;
|
|
419
|
+
if (fieldNeedsOptions(f.type) && f.options.trim()) field.options = f.options.trim();
|
|
420
|
+
return field;
|
|
421
|
+
}
|
|
422
|
+
function moveField(fields, key, dir) {
|
|
423
|
+
const i = fields.findIndex((f) => f.key === key);
|
|
424
|
+
const j = i + dir;
|
|
425
|
+
if (i < 0 || j < 0 || j >= fields.length) return fields;
|
|
426
|
+
const next = [...fields];
|
|
427
|
+
[next[i], next[j]] = [next[j], next[i]];
|
|
428
|
+
return next;
|
|
429
|
+
}
|
|
430
|
+
function toDocType(name, module, fields) {
|
|
431
|
+
const docFields = fields.map(toDocField);
|
|
432
|
+
const byName = new Set(docFields.map((f) => f.fieldname));
|
|
433
|
+
const dt = { name: name.trim(), module, fields: docFields };
|
|
434
|
+
if (byName.has("slug")) dt.autoname = "field:slug";
|
|
435
|
+
if (byName.has("title")) dt.titleField = "title";
|
|
436
|
+
return dt;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/framework/media.ts
|
|
440
|
+
var REF_PREFIX = "s3://";
|
|
441
|
+
function mediaRef(bucket, key) {
|
|
442
|
+
return `${REF_PREFIX}${bucket}/${key}`;
|
|
443
|
+
}
|
|
444
|
+
function isRef(value) {
|
|
445
|
+
return typeof value === "string" && value.startsWith(REF_PREFIX);
|
|
446
|
+
}
|
|
447
|
+
function parseRef(ref) {
|
|
448
|
+
if (!ref.startsWith(REF_PREFIX)) return null;
|
|
449
|
+
const rest = ref.slice(REF_PREFIX.length);
|
|
450
|
+
const slash = rest.indexOf("/");
|
|
451
|
+
if (slash <= 0) return null;
|
|
452
|
+
return { bucket: rest.slice(0, slash), key: rest.slice(slash + 1) };
|
|
453
|
+
}
|
|
454
|
+
function joinKey(...parts) {
|
|
455
|
+
return parts.filter(Boolean).join("/");
|
|
456
|
+
}
|
|
457
|
+
function mediaKey(filename, folder = "") {
|
|
458
|
+
const dot = filename.lastIndexOf(".");
|
|
459
|
+
const ext = dot > 0 ? filename.slice(dot + 1).toLowerCase().replace(/[^a-z0-9]/g, "") : "";
|
|
460
|
+
const base = slugify(dot > 0 ? filename.slice(0, dot) : filename) || "file";
|
|
461
|
+
const stamp = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
462
|
+
const name = ext ? `${base}-${stamp}.${ext}` : `${base}-${stamp}`;
|
|
463
|
+
return folder ? joinKey(slugify(folder), name) : name;
|
|
464
|
+
}
|
|
465
|
+
function looksLikeImage(value) {
|
|
466
|
+
return /\.(png|jpe?g|gif|webp|svg|avif)(\?|#|$)/i.test(value) || value.startsWith("data:image/");
|
|
467
|
+
}
|
|
468
|
+
function createMediaUploader(store, bucket) {
|
|
469
|
+
return {
|
|
470
|
+
bucket,
|
|
471
|
+
async upload(file, folder = "") {
|
|
472
|
+
await store.ensureBucket(bucket).catch(() => void 0);
|
|
473
|
+
const key = mediaKey(file.name, folder);
|
|
474
|
+
const presigned = await store.presignUpload(bucket, key);
|
|
475
|
+
if (!presigned || (presigned.method ?? "PUT") !== "PUT" || !presigned.url) {
|
|
476
|
+
throw new Error("Could not start the upload (object storage not configured).");
|
|
477
|
+
}
|
|
478
|
+
await store.put(presigned.url, file);
|
|
479
|
+
const { width, height } = await imageDimensions(file);
|
|
480
|
+
return {
|
|
481
|
+
fileRef: mediaRef(bucket, key),
|
|
482
|
+
filename: file.name,
|
|
483
|
+
mime: file.type || "application/octet-stream",
|
|
484
|
+
size: file.size,
|
|
485
|
+
width,
|
|
486
|
+
height
|
|
487
|
+
};
|
|
488
|
+
},
|
|
489
|
+
async resolveUrl(value) {
|
|
490
|
+
if (typeof value !== "string" || value === "") return "";
|
|
491
|
+
if (!isRef(value)) return value;
|
|
492
|
+
const parsed = parseRef(value);
|
|
493
|
+
if (!parsed) return "";
|
|
494
|
+
const presigned = await store.presignDownload(parsed.bucket, parsed.key).catch(() => null);
|
|
495
|
+
return presigned?.url ?? "";
|
|
496
|
+
},
|
|
497
|
+
async remove(value) {
|
|
498
|
+
if (!isRef(value)) return;
|
|
499
|
+
const parsed = parseRef(value);
|
|
500
|
+
if (!parsed) return;
|
|
501
|
+
await store.deleteObject(parsed.bucket, parsed.key).catch(() => void 0);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function imageDimensions(file) {
|
|
506
|
+
return new Promise((resolve) => {
|
|
507
|
+
if (typeof window === "undefined" || typeof Image === "undefined" || !file.type.startsWith("image/")) {
|
|
508
|
+
resolve({ width: 0, height: 0 });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const url = URL.createObjectURL(file);
|
|
512
|
+
const img = new Image();
|
|
513
|
+
img.onload = () => {
|
|
514
|
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
515
|
+
URL.revokeObjectURL(url);
|
|
516
|
+
};
|
|
517
|
+
img.onerror = () => {
|
|
518
|
+
resolve({ width: 0, height: 0 });
|
|
519
|
+
URL.revokeObjectURL(url);
|
|
520
|
+
};
|
|
521
|
+
img.src = url;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/framework/data.ts
|
|
526
|
+
async function loadLinkOptions(client, dt) {
|
|
527
|
+
const links = linkFields(dt);
|
|
528
|
+
const out = {};
|
|
529
|
+
await Promise.all(
|
|
530
|
+
links.map(async (f) => {
|
|
531
|
+
const target = f.options;
|
|
532
|
+
if (!target) return;
|
|
533
|
+
const [tdt, docs] = await Promise.all([
|
|
534
|
+
client.doctypes.get(target).catch(() => null),
|
|
535
|
+
client.records.list(target, { limit: 200 }).catch(() => [])
|
|
536
|
+
]);
|
|
537
|
+
out[f.fieldname] = docs.map((d) => ({
|
|
538
|
+
value: String(d.name),
|
|
539
|
+
label: tdt ? titleOf(d, tdt) : String(d.name)
|
|
540
|
+
}));
|
|
541
|
+
})
|
|
542
|
+
);
|
|
543
|
+
return out;
|
|
544
|
+
}
|
|
545
|
+
function makeFieldOptions(linkOptions) {
|
|
546
|
+
return (field) => {
|
|
547
|
+
if (field.type === "relation") return linkOptions[field.name] ?? [];
|
|
548
|
+
if (field.type === "select" || field.type === "multiSelect") {
|
|
549
|
+
return field.metadata?.options;
|
|
550
|
+
}
|
|
551
|
+
return void 0;
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
exports.BUILDER_FIELD_TYPES = BUILDER_FIELD_TYPES;
|
|
556
|
+
exports.CANCELLED = CANCELLED;
|
|
557
|
+
exports.DRAFT = DRAFT;
|
|
558
|
+
exports.PROJECT_FIELD = PROJECT_FIELD;
|
|
559
|
+
exports.REDACTED_MARKER = REDACTED_MARKER;
|
|
560
|
+
exports.SUBMITTED = SUBMITTED;
|
|
561
|
+
exports.autonameSource = autonameSource;
|
|
562
|
+
exports.blankField = blankField;
|
|
563
|
+
exports.cardFields = cardFields;
|
|
564
|
+
exports.createFrameworkClient = createFrameworkClient;
|
|
565
|
+
exports.createMediaUploader = createMediaUploader;
|
|
566
|
+
exports.docTypeToFields = docTypeToFields;
|
|
567
|
+
exports.enrichLinks = enrichLinks;
|
|
568
|
+
exports.fieldNameFromLabel = fieldNameFromLabel;
|
|
569
|
+
exports.fieldNeedsOptions = fieldNeedsOptions;
|
|
570
|
+
exports.fieldTypeLabel = fieldTypeLabel;
|
|
571
|
+
exports.fieldsFromDocType = fieldsFromDocType;
|
|
572
|
+
exports.hasProjectField = hasProjectField;
|
|
573
|
+
exports.humanize = humanize;
|
|
574
|
+
exports.imageDimensions = imageDimensions;
|
|
575
|
+
exports.isDraft = isDraft;
|
|
576
|
+
exports.isMediaDoctype = isMediaDoctype;
|
|
577
|
+
exports.isRef = isRef;
|
|
578
|
+
exports.isValidDoctypeName = isValidDoctypeName;
|
|
579
|
+
exports.joinKey = joinKey;
|
|
580
|
+
exports.linkFields = linkFields;
|
|
581
|
+
exports.listHiddenFields = listHiddenFields;
|
|
582
|
+
exports.listQuery = listQuery;
|
|
583
|
+
exports.listedFieldNames = listedFieldNames;
|
|
584
|
+
exports.loadLinkOptions = loadLinkOptions;
|
|
585
|
+
exports.looksLikeImage = looksLikeImage;
|
|
586
|
+
exports.makeFieldOptions = makeFieldOptions;
|
|
587
|
+
exports.mediaDocPayload = mediaDocPayload;
|
|
588
|
+
exports.mediaFileField = mediaFileField;
|
|
589
|
+
exports.mediaKey = mediaKey;
|
|
590
|
+
exports.mediaRef = mediaRef;
|
|
591
|
+
exports.moduleDoctypes = moduleDoctypes;
|
|
592
|
+
exports.moveField = moveField;
|
|
593
|
+
exports.newDraft = newDraft;
|
|
594
|
+
exports.newKey = newKey;
|
|
595
|
+
exports.parseRef = parseRef;
|
|
596
|
+
exports.rows = rows;
|
|
597
|
+
exports.savePayload = savePayload;
|
|
598
|
+
exports.slugify = slugify;
|
|
599
|
+
exports.starterFields = starterFields;
|
|
600
|
+
exports.statusField = statusField;
|
|
601
|
+
exports.titleOf = titleOf;
|
|
602
|
+
exports.toDocField = toDocField;
|
|
603
|
+
exports.toDocType = toDocType;
|
|
604
|
+
exports.toRecord = toRecord;
|
|
605
|
+
exports.validateBuilder = validateBuilder;
|
|
606
|
+
//# sourceMappingURL=chunk-3NG33DV2.cjs.map
|
|
607
|
+
//# sourceMappingURL=chunk-3NG33DV2.cjs.map
|