@nuasite/collections-admin 0.43.0-beta.4 → 0.43.0-beta.8
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/dist/types/app.d.ts +11 -6
- package/dist/types/app.d.ts.map +1 -1
- package/dist/types/entry-create.d.ts +1 -1
- package/dist/types/entry-create.d.ts.map +1 -1
- package/dist/types/entry-editor.d.ts +1 -1
- package/dist/types/entry-editor.d.ts.map +1 -1
- package/dist/types/field-editor.d.ts +1 -1
- package/dist/types/field-editor.d.ts.map +1 -1
- package/dist/types/media-picker.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/app.tsx +181 -109
- package/src/entry-editor.tsx +224 -36
- package/src/field-editor.tsx +54 -5
- package/src/media-picker.tsx +16 -2
- package/src/styles.css +220 -1
- package/src/tsconfig.json +1 -0
- package/dist/types/app.js +0 -240
- package/dist/types/client.d.ts +0 -149
- package/dist/types/client.d.ts.map +0 -1
- package/dist/types/client.js +0 -134
- package/dist/types/field-view.d.ts +0 -17
- package/dist/types/field-view.d.ts.map +0 -1
- package/dist/types/field-view.js +0 -77
- package/dist/types/form-model.d.ts +0 -61
- package/dist/types/form-model.d.ts.map +0 -1
- package/dist/types/index.js +0 -8
package/dist/types/client.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Typed read-only client over the cms-sidecar `/cms/v1` HTTP contract.
|
|
3
|
-
*
|
|
4
|
-
* The host (webmaster BFF, or a local dev proxy in F7) mounts the sidecar under
|
|
5
|
-
* an `apiBase` and adds the `/cms/v1` prefix itself — so this client requests
|
|
6
|
-
* `${apiBase}/project`, `${apiBase}/collections`, etc. (never `/cms/v1/...`).
|
|
7
|
-
*
|
|
8
|
-
* The structural model (collections/entries/fields) is reused 1:1 from
|
|
9
|
-
* `@nuasite/cms-types`. The thin HTTP envelope (project model, sparse entries
|
|
10
|
-
* list, error codes) mirrors the sidecar's wire types; it is declared here
|
|
11
|
-
* because those types are not part of the `@nuasite/cms-types` contract surface.
|
|
12
|
-
*/
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// Client error
|
|
15
|
-
// ============================================================================
|
|
16
|
-
/**
|
|
17
|
-
* Thrown for any non-2xx response. Carries the parsed sidecar error code so the
|
|
18
|
-
* UI can distinguish auth failures (`unauthorized`/`forbidden`) from a missing
|
|
19
|
-
* collection/entry (`not_found`) or a generic failure.
|
|
20
|
-
*/
|
|
21
|
-
export class CmsClientError extends Error {
|
|
22
|
-
status;
|
|
23
|
-
code;
|
|
24
|
-
constructor(status, code, message) {
|
|
25
|
-
super(message);
|
|
26
|
-
this.status = status;
|
|
27
|
-
this.code = code;
|
|
28
|
-
this.name = 'CmsClientError';
|
|
29
|
-
}
|
|
30
|
-
/** Session cookie missing/expired upstream — the user must re-authenticate. */
|
|
31
|
-
get isUnauthorized() {
|
|
32
|
-
return this.code === 'unauthorized' || this.status === 401;
|
|
33
|
-
}
|
|
34
|
-
/** Authenticated but lacks access to this project. */
|
|
35
|
-
get isForbidden() {
|
|
36
|
-
return this.code === 'forbidden' || this.status === 403;
|
|
37
|
-
}
|
|
38
|
-
get isNotFound() {
|
|
39
|
-
return this.code === 'not_found' || this.status === 404;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
// ============================================================================
|
|
43
|
-
// Client
|
|
44
|
-
// ============================================================================
|
|
45
|
-
function isApiError(value) {
|
|
46
|
-
return typeof value === 'object'
|
|
47
|
-
&& value !== null
|
|
48
|
-
&& 'error' in value
|
|
49
|
-
&& typeof value.error === 'string'
|
|
50
|
-
&& 'code' in value
|
|
51
|
-
&& typeof value.code === 'string';
|
|
52
|
-
}
|
|
53
|
-
const KNOWN_ERROR_CODES = [
|
|
54
|
-
'not_found',
|
|
55
|
-
'conflict',
|
|
56
|
-
'validation',
|
|
57
|
-
'parse_error',
|
|
58
|
-
'io_error',
|
|
59
|
-
'unsupported',
|
|
60
|
-
'unauthorized',
|
|
61
|
-
];
|
|
62
|
-
function isErrorCode(value) {
|
|
63
|
-
return KNOWN_ERROR_CODES.includes(value);
|
|
64
|
-
}
|
|
65
|
-
export function createClient(apiBase) {
|
|
66
|
-
// Normalise: drop a trailing slash so `${base}${path}` joins cleanly.
|
|
67
|
-
const base = apiBase.endsWith('/') ? apiBase.slice(0, -1) : apiBase;
|
|
68
|
-
async function request(path) {
|
|
69
|
-
const response = await fetch(`${base}${path}`, {
|
|
70
|
-
method: 'GET',
|
|
71
|
-
credentials: 'include',
|
|
72
|
-
headers: { accept: 'application/json' },
|
|
73
|
-
});
|
|
74
|
-
if (!response.ok) {
|
|
75
|
-
throw await toError(response);
|
|
76
|
-
}
|
|
77
|
-
// Successful responses are always JSON in the read-only surface.
|
|
78
|
-
const value = await response.json();
|
|
79
|
-
return value;
|
|
80
|
-
}
|
|
81
|
-
async function toError(response) {
|
|
82
|
-
// 403 is produced by the BFF (project scope), not the sidecar, so it has no
|
|
83
|
-
// sidecar `code`; surface it as a distinct `forbidden`.
|
|
84
|
-
if (response.status === 403) {
|
|
85
|
-
const message = await readErrorMessage(response, 'You do not have access to this project.');
|
|
86
|
-
return new CmsClientError(403, 'forbidden', message);
|
|
87
|
-
}
|
|
88
|
-
const body = await response.json().catch(() => null);
|
|
89
|
-
if (isApiError(body) && isErrorCode(body.code)) {
|
|
90
|
-
return new CmsClientError(response.status, body.code, body.error);
|
|
91
|
-
}
|
|
92
|
-
if (response.status === 401) {
|
|
93
|
-
return new CmsClientError(401, 'unauthorized', 'Your session has expired. Please reload.');
|
|
94
|
-
}
|
|
95
|
-
return new CmsClientError(response.status, 'unknown', `Request failed (${response.status})`);
|
|
96
|
-
}
|
|
97
|
-
async function readErrorMessage(response, fallback) {
|
|
98
|
-
const body = await response.json().catch(() => null);
|
|
99
|
-
if (isApiError(body))
|
|
100
|
-
return body.error;
|
|
101
|
-
if (typeof body === 'object' && body !== null && 'error' in body) {
|
|
102
|
-
const err = body.error;
|
|
103
|
-
if (typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string') {
|
|
104
|
-
return err.message;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return fallback;
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
getProject() {
|
|
111
|
-
return request('/project');
|
|
112
|
-
},
|
|
113
|
-
getCollections() {
|
|
114
|
-
return request('/collections');
|
|
115
|
-
},
|
|
116
|
-
getEntries(collection, options = {}) {
|
|
117
|
-
const params = new URLSearchParams();
|
|
118
|
-
if (options.fields !== undefined)
|
|
119
|
-
params.set('fields', options.fields);
|
|
120
|
-
if (options.draft !== undefined)
|
|
121
|
-
params.set('draft', options.draft);
|
|
122
|
-
if (options.cursor !== undefined)
|
|
123
|
-
params.set('cursor', options.cursor);
|
|
124
|
-
if (options.limit !== undefined)
|
|
125
|
-
params.set('limit', String(options.limit));
|
|
126
|
-
const query = params.toString();
|
|
127
|
-
const suffix = query === '' ? '' : `?${query}`;
|
|
128
|
-
return request(`/collections/${encodeURIComponent(collection)}/entries${suffix}`);
|
|
129
|
-
},
|
|
130
|
-
getEntry(collection, slug) {
|
|
131
|
-
return request(`/collections/${encodeURIComponent(collection)}/entries/${encodeURIComponent(slug)}`);
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read-only field rendering, driven by a collection's `FieldDefinition`.
|
|
3
|
-
*
|
|
4
|
-
* Scalar types (text/number/boolean/date/select/image/url/email/tel/color) show
|
|
5
|
-
* their value directly; structural types (array/object/reference) show their
|
|
6
|
-
* structure. Nothing here mutates — editing arrives in F3.2.
|
|
7
|
-
*/
|
|
8
|
-
import type { FieldDefinition } from '@nuasite/cms-types';
|
|
9
|
-
export declare function FieldValueView({ field, raw }: {
|
|
10
|
-
field: FieldDefinition;
|
|
11
|
-
raw: string | undefined;
|
|
12
|
-
}): import("react").JSX.Element;
|
|
13
|
-
export declare function FieldRow({ field, raw }: {
|
|
14
|
-
field: FieldDefinition;
|
|
15
|
-
raw: string | undefined;
|
|
16
|
-
}): import("react").JSX.Element;
|
|
17
|
-
//# sourceMappingURL=field-view.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"field-view.d.ts","sourceRoot":"","sources":["../../src/field-view.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAa,MAAM,oBAAoB,CAAA;AAgDpE,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,+BAmBjG;AAED,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,+BAU3F"}
|
package/dist/types/field-view.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read-only field rendering, driven by a collection's `FieldDefinition`.
|
|
3
|
-
*
|
|
4
|
-
* Scalar types (text/number/boolean/date/select/image/url/email/tel/color) show
|
|
5
|
-
* their value directly; structural types (array/object/reference) show their
|
|
6
|
-
* structure. Nothing here mutates — editing arrives in F3.2.
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* Frontmatter values arrive from the sidecar already stringified
|
|
10
|
-
* (`{ value: string; line: number }`). For structural types the string is a
|
|
11
|
-
* JSON payload; we parse it best-effort for a structured render and fall back to
|
|
12
|
-
* the raw string when it is not JSON.
|
|
13
|
-
*/
|
|
14
|
-
function parseStructured(raw) {
|
|
15
|
-
const trimmed = raw.trim();
|
|
16
|
-
if (trimmed === '')
|
|
17
|
-
return undefined;
|
|
18
|
-
if (!(trimmed.startsWith('{') || trimmed.startsWith('[')))
|
|
19
|
-
return raw;
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(trimmed);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return raw;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function StructuredValue({ raw }) {
|
|
28
|
-
const parsed = parseStructured(raw);
|
|
29
|
-
if (parsed === undefined) {
|
|
30
|
-
return <span className="nua-cadmin-field-empty">—</span>;
|
|
31
|
-
}
|
|
32
|
-
if (typeof parsed === 'string') {
|
|
33
|
-
return <span>{parsed}</span>;
|
|
34
|
-
}
|
|
35
|
-
return <pre className="nua-cadmin-field-structured">{JSON.stringify(parsed, null, 2)}</pre>;
|
|
36
|
-
}
|
|
37
|
-
function BooleanValue({ raw }) {
|
|
38
|
-
const on = raw === 'true' || raw === '1' || raw.toLowerCase() === 'yes';
|
|
39
|
-
return <span className={on ? 'nua-cadmin-bool-on' : 'nua-cadmin-bool-off'}>{on ? 'Yes' : 'No'}</span>;
|
|
40
|
-
}
|
|
41
|
-
function ImageValue({ raw }) {
|
|
42
|
-
if (raw === '')
|
|
43
|
-
return <span className="nua-cadmin-field-empty">—</span>;
|
|
44
|
-
const looksLikeUrl = /^(https?:\/\/|\/)/.test(raw);
|
|
45
|
-
return (<div>
|
|
46
|
-
{looksLikeUrl ? <img className="nua-cadmin-img" src={raw} alt=""/> : null}
|
|
47
|
-
<div className="nua-cadmin-cell-mono">{raw}</div>
|
|
48
|
-
</div>);
|
|
49
|
-
}
|
|
50
|
-
const STRUCTURAL_TYPES = new Set(['array', 'object', 'reference']);
|
|
51
|
-
export function FieldValueView({ field, raw }) {
|
|
52
|
-
if (raw === undefined || raw === '') {
|
|
53
|
-
return <div className="nua-cadmin-field-value nua-cadmin-field-empty">—</div>;
|
|
54
|
-
}
|
|
55
|
-
let content;
|
|
56
|
-
switch (field.type) {
|
|
57
|
-
case 'boolean':
|
|
58
|
-
content = <BooleanValue raw={raw}/>;
|
|
59
|
-
break;
|
|
60
|
-
case 'image':
|
|
61
|
-
case 'file':
|
|
62
|
-
content = <ImageValue raw={raw}/>;
|
|
63
|
-
break;
|
|
64
|
-
default:
|
|
65
|
-
content = STRUCTURAL_TYPES.has(field.type) ? <StructuredValue raw={raw}/> : <span>{raw}</span>;
|
|
66
|
-
}
|
|
67
|
-
return <div className="nua-cadmin-field-value">{content}</div>;
|
|
68
|
-
}
|
|
69
|
-
export function FieldRow({ field, raw }) {
|
|
70
|
-
return (<div className="nua-cadmin-field">
|
|
71
|
-
<div className="nua-cadmin-field-label">
|
|
72
|
-
<span>{field.name}</span>
|
|
73
|
-
<span className="nua-cadmin-field-type">{field.type}{field.required ? " · required" : ""}</span>
|
|
74
|
-
</div>
|
|
75
|
-
<FieldValueView field={field} raw={raw}/>
|
|
76
|
-
</div>);
|
|
77
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure draft model + field coercion for the entry editor (cms-headless F3.2).
|
|
3
|
-
*
|
|
4
|
-
* The sidecar speaks two slightly different frontmatter shapes:
|
|
5
|
-
* - `GET …/entries/:slug` returns `frontmatter: Record<string, { value: string; line: number }>`,
|
|
6
|
-
* where `value` is already stringified (objects/arrays are JSON).
|
|
7
|
-
* - `PATCH …` accepts `frontmatter?: Record<string, unknown>` of *native* values (merged), and a
|
|
8
|
-
* `409` `serverFrontmatter` is likewise native (not stringified).
|
|
9
|
-
*
|
|
10
|
-
* The editor works on a single native draft (`EntryDraft`): `frontmatter` is a
|
|
11
|
-
* `Record<string, unknown>` of native JS values keyed by field name, plus the
|
|
12
|
-
* markdown `body`. This module converts to/from the wire and coerces raw input
|
|
13
|
-
* (form strings) into the native value a `FieldType` expects. Keeping it pure
|
|
14
|
-
* (no React/DOM) makes the mapping unit-testable.
|
|
15
|
-
*/
|
|
16
|
-
import type { CollectionEntry, FieldDefinition, FieldType } from '@nuasite/cms-types';
|
|
17
|
-
/** The editor's in-memory state: native frontmatter values + the markdown body. */
|
|
18
|
-
export interface EntryDraft {
|
|
19
|
-
frontmatter: Record<string, unknown>;
|
|
20
|
-
body: string;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Parse one stringified frontmatter `value` (from `GET …/entries/:slug`) into the
|
|
24
|
-
* native value a field of `type` expects. Structural types (object/array) and
|
|
25
|
-
* unknowns fall back to a best-effort `JSON.parse`; scalars are coerced per type.
|
|
26
|
-
*/
|
|
27
|
-
export declare function parseWireValue(type: FieldType, raw: string): unknown;
|
|
28
|
-
/**
|
|
29
|
-
* Build a native draft from a loaded entry, driven by the collection's fields.
|
|
30
|
-
* Frontmatter keys present on the entry but absent from the inferred schema are
|
|
31
|
-
* preserved verbatim (as raw strings) so a save never silently drops them.
|
|
32
|
-
*/
|
|
33
|
-
export declare function draftFromEntry(entry: CollectionEntry, fields: FieldDefinition[]): EntryDraft;
|
|
34
|
-
/**
|
|
35
|
-
* Build a fresh draft for a create form from the collection's fields, seeding
|
|
36
|
-
* each field with its `defaultValue` (when present) or a type-appropriate blank.
|
|
37
|
-
*/
|
|
38
|
-
export declare function draftForCreate(fields: FieldDefinition[]): EntryDraft;
|
|
39
|
-
/** A type-appropriate empty value used to seed create forms. */
|
|
40
|
-
export declare function blankValue(type: FieldType): unknown;
|
|
41
|
-
/**
|
|
42
|
-
* Adopt a server-provided native frontmatter map (from a `409` `serverFrontmatter`)
|
|
43
|
-
* into a draft, re-coercing per field where a definition exists.
|
|
44
|
-
*/
|
|
45
|
-
export declare function draftFromServerFrontmatter(serverFrontmatter: Record<string, unknown>, serverBody: string | undefined, fields: FieldDefinition[]): EntryDraft;
|
|
46
|
-
/**
|
|
47
|
-
* Coerce a raw form-control string into the native value a field expects. Used by
|
|
48
|
-
* the widgets, whose `<input>` values are always strings.
|
|
49
|
-
*/
|
|
50
|
-
export declare function coerceInput(type: FieldType, raw: string): unknown;
|
|
51
|
-
/** Render a native value back to a string for a text/number/date/select control. */
|
|
52
|
-
export declare function valueToInput(value: unknown): string;
|
|
53
|
-
/** Read a value as a boolean for toggle widgets, tolerating string encodings. */
|
|
54
|
-
export declare function valueToBoolean(value: unknown): boolean;
|
|
55
|
-
/** Read a value as an array of items for repeater widgets. */
|
|
56
|
-
export declare function valueToArray(value: unknown): unknown[];
|
|
57
|
-
/** Read a value as an object for nested-group widgets. */
|
|
58
|
-
export declare function valueToObject(value: unknown): Record<string, unknown>;
|
|
59
|
-
/** Immutably set a top-level frontmatter key in a draft. */
|
|
60
|
-
export declare function setDraftField(draft: EntryDraft, name: string, value: unknown): EntryDraft;
|
|
61
|
-
//# sourceMappingURL=form-model.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"form-model.d.ts","sourceRoot":"","sources":["../../src/form-model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAErF,mFAAmF;AACnF,MAAM,WAAW,UAAU;IAC1B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,IAAI,EAAE,MAAM,CAAA;CACZ;AAMD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAgBpE;AAcD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,UAAU,CAQ5F;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,UAAU,CAWpE;AAED,gEAAgE;AAChE,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAWnD;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACzC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1C,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,eAAe,EAAE,GACvB,UAAU,CAUZ;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAkBjE;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKnD;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAItD;AAED,8DAA8D;AAC9D,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,CAEtD;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAErE;AAED,4DAA4D;AAC5D,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,UAAU,CAEzF"}
|
package/dist/types/index.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@nuasite/collections-admin` — read-only collections SPA over the cms-sidecar
|
|
3
|
-
* `/cms/v1` HTTP contract (cms-headless F3.1). Host-agnostic: mount
|
|
4
|
-
* `<CollectionsAdminApp apiBase={…} />` and it drives its own internal view-state
|
|
5
|
-
* navigation. Self-contained styles ship at `./styles.css` (imported by the app).
|
|
6
|
-
*/
|
|
7
|
-
export { CollectionsAdminApp } from './app';
|
|
8
|
-
export { CmsClientError, createClient, } from './client';
|