@stonecrop/desktop 0.18.0 → 0.19.0
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/desktop.js +445 -420
- package/dist/desktop.js.map +1 -1
- package/package.json +6 -6
- package/src/components/Desktop.vue +103 -15
|
@@ -43,7 +43,13 @@
|
|
|
43
43
|
// when the two were written separately they disagreed and every guard over there went dead.
|
|
44
44
|
import { DRAFT_RECORD_ID, isDraftRecordId, useStonecrop, useValidationStore } from '@stonecrop/stonecrop'
|
|
45
45
|
import { AForm, type AFormLinkNavigator, type ResolvedField, type ResolvedTable } from '@stonecrop/aform'
|
|
46
|
-
import
|
|
46
|
+
import {
|
|
47
|
+
type ColumnSchema,
|
|
48
|
+
componentLinkExpansion,
|
|
49
|
+
type DoctypeField,
|
|
50
|
+
flattenFields,
|
|
51
|
+
linkDisplayFieldname,
|
|
52
|
+
} from '@stonecrop/schema'
|
|
47
53
|
import { computed, onMounted, onUnmounted, provide, ref, unref, watch } from 'vue'
|
|
48
54
|
|
|
49
55
|
import ActionSet from './ActionSet.vue'
|
|
@@ -59,6 +65,77 @@ import type {
|
|
|
59
65
|
LoadRecordEventPayload,
|
|
60
66
|
} from '../types'
|
|
61
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The link fields a doctype declares, split by what their value actually holds.
|
|
70
|
+
*
|
|
71
|
+
* Both sets come from the doctype rather than from the shape of a key or a value, because neither
|
|
72
|
+
* shape is decisive. A link's display text arrives as a sibling key (`customerId__display`) in the
|
|
73
|
+
* same payload the user is editing, so a key ending in `__display` is not necessarily one; and an
|
|
74
|
+
* *inline* link's value (`{ id, displayText }`) is indistinguishable by inspection from an
|
|
75
|
+
* *expanded* one (`{ id, ...the whole target record }`).
|
|
76
|
+
*
|
|
77
|
+
* `inlineLinks` is the narrower set and the load-bearing one. Only an inline link's value is a
|
|
78
|
+
* scalar id wearing a label, so only it may be unwrapped back to that id on write. Unwrapping an
|
|
79
|
+
* expanded link instead replaces the embedded record with its own id — destroying the nested data
|
|
80
|
+
* and discarding the edit that triggered the write.
|
|
81
|
+
*
|
|
82
|
+
* `componentLinkExpansion` is the same rule the adapter reads when it decides which links to
|
|
83
|
+
* expand, so the two sides agree on what a field's value contains. A component it does not
|
|
84
|
+
* recognise is left out: an unrecognised link is passed through untouched rather than rewritten on
|
|
85
|
+
* a guess.
|
|
86
|
+
*/
|
|
87
|
+
function linkFieldSets(fields: DoctypeField[]): { inlineLinks: Set<string>; displayKeys: Set<string> } {
|
|
88
|
+
const links = flattenFields(fields).filter(field => field.kind === 'field' && Boolean(field.doctype))
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
inlineLinks: new Set(
|
|
92
|
+
links.filter(field => componentLinkExpansion(field.component) === 'inline').map(field => field.fieldname)
|
|
93
|
+
),
|
|
94
|
+
displayKeys: new Set(links.map(field => linkDisplayFieldname(field.fieldname))),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fold a link's pre-resolved `fieldname__display` sibling into the value AForm renders.
|
|
100
|
+
*
|
|
101
|
+
* Both this and the record payload are flat — a fieldset is a layout grouping, not a scope — so
|
|
102
|
+
* the fields come through `flattenFields` rather than a descent written here. `@stonecrop/schema`
|
|
103
|
+
* owns that descent and the adapter reads the same one, which is what keeps the keys this looks
|
|
104
|
+
* for and the keys the server writes from drifting apart.
|
|
105
|
+
*/
|
|
106
|
+
function applyLinkDisplayFields(flat: Record<string, any>, fields: DoctypeField[]): void {
|
|
107
|
+
const { inlineLinks, displayKeys } = linkFieldSets(fields)
|
|
108
|
+
|
|
109
|
+
for (const fieldname of inlineLinks) {
|
|
110
|
+
const raw = flat[fieldname]
|
|
111
|
+
const display = flat[linkDisplayFieldname(fieldname)]
|
|
112
|
+
if (
|
|
113
|
+
raw != null &&
|
|
114
|
+
(typeof raw === 'string' || typeof raw === 'number') &&
|
|
115
|
+
display != null &&
|
|
116
|
+
(typeof display === 'string' || typeof display === 'number')
|
|
117
|
+
) {
|
|
118
|
+
flat[fieldname] = { id: raw, displayText: String(display) }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Dropped for every link, not just the inline ones this folded: the sibling is a delivery
|
|
123
|
+
// detail of the read and was never a field of the record.
|
|
124
|
+
for (const key of displayKeys) delete flat[key]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Unwrap an inline link's value back to the id that gets persisted. Applied only to fields in
|
|
129
|
+
* `inlineLinks` — the display text is a rendering concern and was never part of the record, while
|
|
130
|
+
* an expanded link's object is the record and must survive the round trip intact.
|
|
131
|
+
*/
|
|
132
|
+
function unwrapLinkValue(value: unknown): unknown {
|
|
133
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value) && 'id' in value) {
|
|
134
|
+
return (value as { id: unknown }).id
|
|
135
|
+
}
|
|
136
|
+
return value
|
|
137
|
+
}
|
|
138
|
+
|
|
62
139
|
const { availableDoctypes = [], routeAdapter } = defineProps<{
|
|
63
140
|
availableDoctypes?: string[]
|
|
64
141
|
/**
|
|
@@ -182,6 +259,7 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
182
259
|
// so we nest fieldset children here before AForm renders them.
|
|
183
260
|
const doctype = stonecrop.value.registry.registry[currentDoctype.value]
|
|
184
261
|
if (doctype) {
|
|
262
|
+
applyLinkDisplayFields(flat, doctype.getSchemaArray())
|
|
185
263
|
for (const field of doctype.getSchemaArray()) {
|
|
186
264
|
if (field.kind === 'fieldset') {
|
|
187
265
|
const nested: Record<string, any> = {}
|
|
@@ -216,6 +294,7 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
216
294
|
}
|
|
217
295
|
}
|
|
218
296
|
}
|
|
297
|
+
const { inlineLinks, displayKeys } = linkFieldSets(doctype ? doctype.getSchemaArray() : [])
|
|
219
298
|
|
|
220
299
|
// Two-pass flatten: non-fieldset keys first, then fieldset children.
|
|
221
300
|
// Fieldset children must be applied last — AForm may emit stale flat copies
|
|
@@ -224,6 +303,7 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
224
303
|
const flatData: Record<string, any> = {}
|
|
225
304
|
const fieldsetValues: Record<string, any>[] = []
|
|
226
305
|
for (const [key, value] of Object.entries(newData)) {
|
|
306
|
+
if (displayKeys.has(key)) continue
|
|
227
307
|
if (fieldsetNames.has(key) && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
228
308
|
fieldsetValues.push(value)
|
|
229
309
|
} else {
|
|
@@ -243,9 +323,10 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
243
323
|
// cached object is what made a draft's edits vanish on any invalidation.
|
|
244
324
|
const next = { ...draftRecord.value }
|
|
245
325
|
for (const [fieldname, value] of Object.entries(flatData)) {
|
|
246
|
-
if (value === undefined) continue
|
|
247
|
-
|
|
248
|
-
|
|
326
|
+
if (value === undefined || displayKeys.has(fieldname)) continue
|
|
327
|
+
const normalized = inlineLinks.has(fieldname) ? unwrapLinkValue(value) : value
|
|
328
|
+
if (next[fieldname] !== normalized) {
|
|
329
|
+
next[fieldname] = normalized
|
|
249
330
|
changedFields.push(fieldname)
|
|
250
331
|
}
|
|
251
332
|
}
|
|
@@ -253,11 +334,12 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
253
334
|
} else {
|
|
254
335
|
const hstStore = stonecrop.value.getStore()
|
|
255
336
|
for (const [fieldname, value] of Object.entries(flatData)) {
|
|
256
|
-
if (value === undefined) continue
|
|
337
|
+
if (value === undefined || displayKeys.has(fieldname)) continue
|
|
338
|
+
const normalized = inlineLinks.has(fieldname) ? unwrapLinkValue(value) : value
|
|
257
339
|
const fieldPath = `${currentDoctype.value}.${currentRecordId.value}.${fieldname}`
|
|
258
340
|
const currentValue = hstStore.has(fieldPath) ? hstStore.get(fieldPath) : undefined
|
|
259
|
-
if (currentValue !==
|
|
260
|
-
hstStore.set(fieldPath,
|
|
341
|
+
if (currentValue !== normalized) {
|
|
342
|
+
hstStore.set(fieldPath, normalized)
|
|
261
343
|
changedFields.push(fieldname)
|
|
262
344
|
}
|
|
263
345
|
}
|
|
@@ -960,23 +1042,29 @@ provide('aformLinkNavigator', {
|
|
|
960
1042
|
},
|
|
961
1043
|
} satisfies AFormLinkNavigator)
|
|
962
1044
|
|
|
963
|
-
function toDisplayString(rec: Record<string, unknown> | undefined): string | undefined {
|
|
964
|
-
if (!rec) return undefined
|
|
965
|
-
const val = rec.name ?? rec.title ?? rec.displayText
|
|
966
|
-
return typeof val === 'string' || typeof val === 'number' ? String(val) : undefined
|
|
967
|
-
}
|
|
968
|
-
|
|
969
1045
|
// Provide a resolver for AFormLink to look up display text by doctype + id.
|
|
970
1046
|
// Checks HST first (sync); falls back to an async client fetch if not cached.
|
|
1047
|
+
// Uses the target doctype's declared displayField — no heuristic field guessing.
|
|
971
1048
|
provide('aformLinkResolver', async (doctypeSlug: string, id: string): Promise<string | undefined> => {
|
|
972
1049
|
if (!stonecrop.value) return undefined
|
|
973
1050
|
try {
|
|
1051
|
+
const meta = await stonecrop.value.getMeta({ path: `/${doctypeSlug}`, segments: [doctypeSlug] })
|
|
1052
|
+
const displayField = meta?.displayField
|
|
1053
|
+
if (!displayField) return undefined
|
|
1054
|
+
|
|
1055
|
+
const readDisplay = (rec: Record<string, unknown> | undefined): string | undefined => {
|
|
1056
|
+
if (!rec) return undefined
|
|
1057
|
+
const val = rec[displayField]
|
|
1058
|
+
return typeof val === 'string' || typeof val === 'number' ? String(val) : undefined
|
|
1059
|
+
}
|
|
1060
|
+
|
|
974
1061
|
const cached = stonecrop.value.getRecordById(doctypeSlug, id)?.get('') as Record<string, unknown> | undefined
|
|
975
|
-
const cachedDisplay =
|
|
1062
|
+
const cachedDisplay = readDisplay(cached)
|
|
976
1063
|
if (cachedDisplay != null) return cachedDisplay
|
|
1064
|
+
|
|
977
1065
|
await stonecrop.value.getRecord(doctypeSlug, id)
|
|
978
1066
|
const fetched = stonecrop.value.getRecordById(doctypeSlug, id)?.get('') as Record<string, unknown> | undefined
|
|
979
|
-
return
|
|
1067
|
+
return readDisplay(fetched)
|
|
980
1068
|
} catch {
|
|
981
1069
|
return undefined
|
|
982
1070
|
}
|