@nuasite/collections-admin 0.43.0-beta.3 → 0.43.0-beta.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/src/form-model.ts DELETED
@@ -1,182 +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
-
17
- import type { CollectionEntry, FieldDefinition, FieldType } from '@nuasite/cms-types'
18
-
19
- /** The editor's in-memory state: native frontmatter values + the markdown body. */
20
- export interface EntryDraft {
21
- frontmatter: Record<string, unknown>
22
- body: string
23
- }
24
-
25
- function isRecord(value: unknown): value is Record<string, unknown> {
26
- return typeof value === 'object' && value !== null && !Array.isArray(value)
27
- }
28
-
29
- /**
30
- * Parse one stringified frontmatter `value` (from `GET …/entries/:slug`) into the
31
- * native value a field of `type` expects. Structural types (object/array) and
32
- * unknowns fall back to a best-effort `JSON.parse`; scalars are coerced per type.
33
- */
34
- export function parseWireValue(type: FieldType, raw: string): unknown {
35
- switch (type) {
36
- case 'boolean':
37
- return raw === 'true' || raw === '1' || raw.toLowerCase() === 'yes'
38
- case 'number':
39
- case 'year':
40
- case 'month': {
41
- const n = Number(raw)
42
- return raw.trim() === '' || Number.isNaN(n) ? raw : n
43
- }
44
- case 'array':
45
- case 'object':
46
- return parseJsonLoose(raw)
47
- default:
48
- return raw
49
- }
50
- }
51
-
52
- /** `JSON.parse` for structural values, falling back to the raw string when invalid. */
53
- function parseJsonLoose(raw: string): unknown {
54
- const trimmed = raw.trim()
55
- if (trimmed === '') return undefined
56
- if (!(trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"'))) return raw
57
- try {
58
- return JSON.parse(trimmed)
59
- } catch {
60
- return raw
61
- }
62
- }
63
-
64
- /**
65
- * Build a native draft from a loaded entry, driven by the collection's fields.
66
- * Frontmatter keys present on the entry but absent from the inferred schema are
67
- * preserved verbatim (as raw strings) so a save never silently drops them.
68
- */
69
- export function draftFromEntry(entry: CollectionEntry, fields: FieldDefinition[]): EntryDraft {
70
- const byName = new Map(fields.map(f => [f.name, f] as const))
71
- const frontmatter: Record<string, unknown> = {}
72
- for (const [key, cell] of Object.entries(entry.frontmatter)) {
73
- const field = byName.get(key)
74
- frontmatter[key] = field ? parseWireValue(field.type, cell.value) : cell.value
75
- }
76
- return { frontmatter, body: entry.body }
77
- }
78
-
79
- /**
80
- * Build a fresh draft for a create form from the collection's fields, seeding
81
- * each field with its `defaultValue` (when present) or a type-appropriate blank.
82
- */
83
- export function draftForCreate(fields: FieldDefinition[]): EntryDraft {
84
- const frontmatter: Record<string, unknown> = {}
85
- for (const field of fields) {
86
- if (field.hidden) continue
87
- if (field.defaultValue !== undefined) {
88
- frontmatter[field.name] = field.defaultValue
89
- continue
90
- }
91
- frontmatter[field.name] = blankValue(field.type)
92
- }
93
- return { frontmatter, body: '' }
94
- }
95
-
96
- /** A type-appropriate empty value used to seed create forms. */
97
- export function blankValue(type: FieldType): unknown {
98
- switch (type) {
99
- case 'boolean':
100
- return false
101
- case 'array':
102
- return []
103
- case 'object':
104
- return {}
105
- default:
106
- return ''
107
- }
108
- }
109
-
110
- /**
111
- * Adopt a server-provided native frontmatter map (from a `409` `serverFrontmatter`)
112
- * into a draft, re-coercing per field where a definition exists.
113
- */
114
- export function draftFromServerFrontmatter(
115
- serverFrontmatter: Record<string, unknown>,
116
- serverBody: string | undefined,
117
- fields: FieldDefinition[],
118
- ): EntryDraft {
119
- const byName = new Map(fields.map(f => [f.name, f] as const))
120
- const frontmatter: Record<string, unknown> = {}
121
- for (const [key, value] of Object.entries(serverFrontmatter)) {
122
- const field = byName.get(key)
123
- // Server values are already native; only re-coerce when the value arrived as
124
- // a string for a numeric/boolean field (e.g. YAML quirks).
125
- frontmatter[key] = field && typeof value === 'string' ? parseWireValue(field.type, value) : value
126
- }
127
- return { frontmatter, body: serverBody ?? '' }
128
- }
129
-
130
- /**
131
- * Coerce a raw form-control string into the native value a field expects. Used by
132
- * the widgets, whose `<input>` values are always strings.
133
- */
134
- export function coerceInput(type: FieldType, raw: string): unknown {
135
- switch (type) {
136
- case 'boolean':
137
- return raw === 'true'
138
- case 'number': {
139
- if (raw.trim() === '') return undefined
140
- const n = Number(raw)
141
- return Number.isNaN(n) ? raw : n
142
- }
143
- case 'year':
144
- case 'month': {
145
- if (raw.trim() === '') return undefined
146
- const n = Number(raw)
147
- return Number.isNaN(n) ? raw : n
148
- }
149
- default:
150
- return raw
151
- }
152
- }
153
-
154
- /** Render a native value back to a string for a text/number/date/select control. */
155
- export function valueToInput(value: unknown): string {
156
- if (value === undefined || value === null) return ''
157
- if (typeof value === 'string') return value
158
- if (typeof value === 'number' || typeof value === 'boolean') return String(value)
159
- return JSON.stringify(value)
160
- }
161
-
162
- /** Read a value as a boolean for toggle widgets, tolerating string encodings. */
163
- export function valueToBoolean(value: unknown): boolean {
164
- if (typeof value === 'boolean') return value
165
- if (typeof value === 'string') return value === 'true' || value === '1' || value.toLowerCase() === 'yes'
166
- return Boolean(value)
167
- }
168
-
169
- /** Read a value as an array of items for repeater widgets. */
170
- export function valueToArray(value: unknown): unknown[] {
171
- return Array.isArray(value) ? value : []
172
- }
173
-
174
- /** Read a value as an object for nested-group widgets. */
175
- export function valueToObject(value: unknown): Record<string, unknown> {
176
- return isRecord(value) ? value : {}
177
- }
178
-
179
- /** Immutably set a top-level frontmatter key in a draft. */
180
- export function setDraftField(draft: EntryDraft, name: string, value: unknown): EntryDraft {
181
- return { ...draft, frontmatter: { ...draft.frontmatter, [name]: value } }
182
- }