viur-ui 0.0.1 → 0.0.3

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.
Files changed (98) hide show
  1. package/README.md +67 -41
  2. package/index.d.ts +18 -9
  3. package/index.js +6 -3
  4. package/package.json +6 -6
  5. package/src/components/Browser/Browser.vue +58 -58
  6. package/src/components/Browser/TreeNav.vue +32 -32
  7. package/src/components/Browser/ViurBrowser.d.vue.ts +21 -21
  8. package/src/components/Browser/ViurBrowser.vue +47 -44
  9. package/src/components/Browser/dragSession.ts +27 -27
  10. package/src/components/Browser/index.ts +3 -3
  11. package/src/components/Browser/types.ts +9 -9
  12. package/src/components/DataTable/ColumnFilterPanel.vue +7 -7
  13. package/src/components/DataTable/DataTable.vue +359 -351
  14. package/src/components/DataTable/ViurDataTable.d.vue.ts +26 -26
  15. package/src/components/DataTable/ViurDataTable.vue +51 -50
  16. package/src/components/DataTable/columns.ts +30 -30
  17. package/src/components/DataTable/index.ts +12 -12
  18. package/src/components/DataTable/rows.ts +71 -72
  19. package/src/components/DataTable/types.ts +55 -55
  20. package/src/components/DataTable/useColumnLayout.ts +28 -28
  21. package/src/components/DataTable/useGridNavigation.ts +74 -15
  22. package/src/components/DataTable/useVirtualRows.ts +10 -10
  23. package/src/components/DataTable/viurColumns.ts +69 -69
  24. package/src/forms/ViForm.d.vue.ts +32 -32
  25. package/src/forms/ViForm.vue +17 -16
  26. package/src/forms/ViFormCategory.vue +3 -1
  27. package/src/forms/bones/Bone.vue +125 -108
  28. package/src/forms/bones/BoneActions.vue +21 -21
  29. package/src/forms/bones/BoneLabel.vue +1 -1
  30. package/src/forms/bones/BoneMultipleWrapper.vue +3 -3
  31. package/src/forms/bones/BoneNestedWrapper.vue +3 -3
  32. package/src/forms/bones/BoneSingleEntry.vue +2 -2
  33. package/src/forms/bones/FilterBone.vue +3 -3
  34. package/src/forms/bones/actions/ActionDialog.vue +2 -2
  35. package/src/forms/bones/actions/Debug.vue +6 -6
  36. package/src/forms/bones/actions/Delete.vue +2 -2
  37. package/src/forms/bones/actions/DescribeImage.vue +4 -3
  38. package/src/forms/bones/actions/Download.vue +4 -4
  39. package/src/forms/bones/actions/Placeholder.vue +2 -2
  40. package/src/forms/bones/actions/index.js +72 -74
  41. package/src/forms/bones/bone.d.ts +19 -19
  42. package/src/forms/bones/uploadAdapter.js +3 -3
  43. package/src/forms/bones/utils.js +40 -41
  44. package/src/forms/bones/view/base_item.vue +1 -1
  45. package/src/forms/bones/view/boneLogic.js +1 -1
  46. package/src/forms/bones/widgets/booleanBone.vue +19 -19
  47. package/src/forms/bones/widgets/booleanBoneChoose.vue +2 -2
  48. package/src/forms/bones/widgets/booleanBoneSelect.vue +3 -3
  49. package/src/forms/bones/widgets/codeBone.vue +22 -22
  50. package/src/forms/bones/widgets/colorBone.vue +18 -18
  51. package/src/forms/bones/widgets/dateBone.vue +22 -22
  52. package/src/forms/bones/widgets/emailBone.vue +20 -20
  53. package/src/forms/bones/widgets/fileBone.vue +32 -31
  54. package/src/forms/bones/widgets/keyBone.vue +21 -21
  55. package/src/forms/bones/widgets/numericBone.vue +21 -21
  56. package/src/forms/bones/widgets/passwordBone.vue +34 -31
  57. package/src/forms/bones/widgets/phoneBone.vue +20 -20
  58. package/src/forms/bones/widgets/rawBone.vue +19 -19
  59. package/src/forms/bones/widgets/recordBone.vue +16 -16
  60. package/src/forms/bones/widgets/relationalBone.vue +26 -26
  61. package/src/forms/bones/widgets/selectBone.vue +23 -23
  62. package/src/forms/bones/widgets/selectBoneChoose.vue +2 -2
  63. package/src/forms/bones/widgets/spatialBone.vue +21 -21
  64. package/src/forms/bones/widgets/stringBone.vue +27 -27
  65. package/src/forms/bones/widgets/textBone.vue +28 -26
  66. package/src/forms/install.d.ts +1 -1
  67. package/src/forms/install.js +7 -0
  68. package/src/forms/layouts/LayloutList2Columns.vue +1 -1
  69. package/src/forms/layouts/LayoutCategory.vue +1 -1
  70. package/src/forms/layouts/LayoutList.vue +1 -1
  71. package/src/forms/utils.js +22 -22
  72. package/src/request/configStore.js +4 -4
  73. package/src/request/envelope.js +6 -6
  74. package/src/request/request.js +211 -121
  75. package/src/translations/de.js +31 -0
  76. package/src/translations/en.js +31 -0
  77. package/src/user/LoginScreen.d.vue.ts +24 -24
  78. package/src/user/LoginScreen.vue +6 -7
  79. package/src/user/store.d.ts +16 -16
  80. package/src/vite/safeHtml.js +73 -0
  81. package/src/vite/sanitizeVHtml.d.ts +4 -0
  82. package/src/vite/sanitizeVHtml.js +15 -0
  83. package/src/webawesomeCompat.css +0 -40
  84. package/src/public/assets/icons/fontawesome-manifest.json +0 -8
  85. package/src/public/assets/icons/regular/file.svg +0 -1
  86. package/src/public/assets/icons/solid/arrows-left-right.svg +0 -1
  87. package/src/public/assets/icons/solid/bug.svg +0 -1
  88. package/src/public/assets/icons/solid/check.svg +0 -1
  89. package/src/public/assets/icons/solid/circle-info.svg +0 -1
  90. package/src/public/assets/icons/solid/compress.svg +0 -1
  91. package/src/public/assets/icons/solid/download.svg +0 -1
  92. package/src/public/assets/icons/solid/expand.svg +0 -1
  93. package/src/public/assets/icons/solid/grip-vertical.svg +0 -1
  94. package/src/public/assets/icons/solid/plus.svg +0 -1
  95. package/src/public/assets/icons/solid/question.svg +0 -1
  96. package/src/public/assets/icons/solid/triangle-exclamation.svg +0 -1
  97. package/src/public/assets/icons/solid/upload.svg +0 -1
  98. package/src/public/assets/icons/solid/xmark.svg +0 -1
@@ -64,7 +64,7 @@ export function v1SelectionFollow(body) {
64
64
  if (!String(bone?.["type"] ?? "").startsWith("select")) continue
65
65
  const chosen = values?.[name]
66
66
  if (typeof chosen !== "string" || !/^(\/|https?:\/\/)/.test(chosen)) continue
67
- // Nur wenn der Wert tatsächlich eine der angebotenen Ziel-URLs ist.
67
+ // Only if the value actually is one of the offered target URLs.
68
68
  if (bone?.["values"] && Object.prototype.hasOwnProperty.call(bone["values"], chosen)) return chosen
69
69
  }
70
70
  return null
@@ -190,11 +190,11 @@ export function normalizeEnvelope(data) {
190
190
  out["action"] = action
191
191
  out["status"] = status
192
192
  out["errors"] = normalizeErrors(data["errors"])
193
- // Ziel des Schritts. v1 legt es je nach Stufe anderswo ab:
194
- // next_url – vom Server gesetzt (z. B. Auswahl des zweiten Faktors)
195
- // params.action_url – bei einem gerenderten Schritt, der selbst ein Ziel
196
- // mitbringt (viur-core: der OTP-Schritt des zweiten
197
- // Faktors kommt als `edit` mit action_url)
193
+ // Target of the step. v1 puts it in different places depending on the stage:
194
+ // next_url – set by the server (e.g. choice of the second factor)
195
+ // params.action_url – for a rendered step that brings its own target
196
+ // (viur-core: the OTP step of the second factor
197
+ // comes as `edit` with action_url)
198
198
  out["follow"] = data["follow"] ?? data["next_url"] ?? data["params"]?.["action_url"] ?? null
199
199
  out["structure"] = data["structure"] ?? null
200
200
 
@@ -62,31 +62,43 @@ let _skeyPending = null
62
62
 
63
63
  async function _refillSkeys(renderer) {
64
64
  const base = import.meta.env.VITE_API_URL ?? window.location.origin
65
- let response = await fetch(`${base}/${renderer}/skey?amount=${SKEY_BATCH_SIZE}`, { credentials: "include" })
66
- if (!response.ok) {
67
- response = await fetch(`${base}/${renderer}/skey`, { credentials: "include" })
68
- if (!response.ok) throw new Error("Failed to fetch skey token")
65
+ try {
66
+ let response = await fetch(`${base}/${renderer}/skey?amount=${SKEY_BATCH_SIZE}`, { credentials: "include" })
67
+ if (!response.ok) {
68
+ response = await fetch(`${base}/${renderer}/skey`, { credentials: "include" })
69
+ if (!response.ok) throw new Error("Failed to fetch skey token")
70
+ }
71
+ let data = await response.json()
72
+ if (!Array.isArray(data)) data = [data]
73
+ _skeyCache.push(...data)
74
+ } finally {
75
+ // also on failure: otherwise every later caller awaits the same rejected promise
76
+ _skeyPending = null
69
77
  }
70
- let data = await response.json()
71
- if (!Array.isArray(data)) data = [data]
72
- _skeyCache.push(...data)
73
- _skeyPending = null
74
78
  }
75
79
 
76
80
  async function fetchSkeyToken(renderer) {
77
- if (_skeyCache.length === 0) {
78
- if (!_skeyPending) _skeyPending = _refillSkeys(renderer)
79
- await _skeyPending
81
+ try {
82
+ if (_skeyCache.length === 0) {
83
+ if (!_skeyPending) _skeyPending = _refillSkeys(renderer)
84
+ await _skeyPending
85
+ }
86
+ return _skeyCache.shift()
87
+ } catch (error) {
88
+ throw error
80
89
  }
81
- return _skeyCache.shift()
82
90
  }
83
91
 
84
92
  async function executeAndThrow(fetchResult, executeImpl) {
85
- await executeImpl()
86
- if (fetchResult.error.value) {
87
- throw fetchResult.error.value
93
+ try {
94
+ await executeImpl()
95
+ if (fetchResult.error.value) {
96
+ throw fetchResult.error.value
97
+ }
98
+ return fetchResult
99
+ } catch (error) {
100
+ throw error
88
101
  }
89
- return fetchResult
90
102
  }
91
103
 
92
104
  function createControllerListFetch({ all = false, secure = false } = {}) {
@@ -115,6 +127,11 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
115
127
  const orders = ref([])
116
128
  const params = ref({ ...initialParams })
117
129
  let _isNext = false
130
+ // Bumped by execute()/reset(). A request remembers the generation it started
131
+ // in; once a newer one exists it must not write anymore. Aborting alone is
132
+ // not enough: after the list response arrived, afterFetch still awaits the
133
+ // structure round trip and would then write into the already cleared state.
134
+ let _generation = 0
118
135
 
119
136
  function _buildUrl() {
120
137
  const base = url ?? create_url({ controller, endpoint: group ? `list/${group}` : "list", renderer, version })
@@ -126,16 +143,16 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
126
143
  }
127
144
 
128
145
  /**
129
- * Struktur der Liste holen.
146
+ * Fetch the structure of the list.
130
147
  *
131
- * Zwei Antwortformen sind unterwegs:
132
- * • aktuell: `/<modul>/structure` rendert wie jede andere Aktion, also
148
+ * Two response shapes are in circulation:
149
+ * • current: `/<modul>/structure` renders like any other action, i.e.
133
150
  * `{ action: "structure.view", values, structure }`
134
- * • historisch (getStructure): `{ viewSkel: {…}, editSkel: {…} }`
135
- * Zusätzlich kann die Struktur eine PAAR-LISTE sein
136
- * (`json.bone.structure.keytuples`) — normalizeStructure glättet das.
151
+ * • historic (getStructure): `{ viewSkel: {…}, editSkel: {…} }`
152
+ * Additionally, the structure can be a PAIR LIST
153
+ * (`json.bone.structure.keytuples`) — normalizeStructure flattens that.
137
154
  */
138
- async function _fetchStructure() {
155
+ async function _fetchStructure(generation) {
139
156
  const skeltype =
140
157
  params.value.skelType === "node"
141
158
  ? "viewNodeSkel"
@@ -146,11 +163,15 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
146
163
  if (params.value.skelType) {
147
164
  url += `${url.includes("?") ? "&" : "?"}skelType=${encodeURIComponent(params.value.skelType)}`
148
165
  }
149
- const resp = await fetch(url, { credentials: "include", headers: { Accept: "application/json" } })
150
- if (!resp.ok) return
151
- const body = normalizeEnvelope(await resp.json())
152
- const raw = body?.["structure"] ?? body?.[skeltype]
153
- if (raw) structure.value = normalizeStructure(raw)
166
+ try {
167
+ const resp = await fetch(url, { credentials: "include", headers: { Accept: "application/json" } })
168
+ if (!resp.ok) return
169
+ const body = normalizeEnvelope(await resp.json())
170
+ const raw = body?.["structure"] ?? body?.[skeltype]
171
+ if (raw && generation === _generation) structure.value = normalizeStructure(raw)
172
+ } catch {
173
+ // the structure is optional, the list stays usable without it
174
+ }
154
175
  }
155
176
 
156
177
  const listUrl = ref(_buildUrl())
@@ -166,7 +187,9 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
166
187
  const extraHeaders = typeof headers === "function" ? headers() : headers
167
188
  if (extraHeaders) options.headers = { ...options.headers, ...extraHeaders }
168
189
  if (cached && !_isNext) {
169
- const hit = await getCached(url, ttl)
190
+ const hit = await getCached(url, ttl).catch((error) => {
191
+ throw error
192
+ })
170
193
  if (hit) {
171
194
  data.value = hit.data ?? hit.skellist ?? hit
172
195
  cursor.value = hit.meta?.cursor ?? hit.cursor ?? ""
@@ -175,22 +198,34 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
175
198
  return { options, url }
176
199
  }
177
200
  }
178
- if (!secure) return { options, url }
179
- const skey = await fetchSkeyToken(renderer)
180
- const separator = url.includes("?") ? "&" : "?"
181
- return { url: `${url}${separator}skey=${skey}`, options }
201
+ const generation = _generation
202
+ if (!secure) return { options, url, generation }
203
+ try {
204
+ const skey = await fetchSkeyToken(renderer)
205
+ const separator = url.includes("?") ? "&" : "?"
206
+ return { url: `${url}${separator}skey=${skey}`, options, generation }
207
+ } catch (error) {
208
+ throw error
209
+ }
182
210
  },
183
211
  async afterFetch(ctx) {
184
212
  // Normalize v1/v2 up to the canonical v2 envelope, then read the list
185
213
  // payload from `data` and pagination from `meta`.
214
+ const { generation } = ctx.context
215
+ if (generation !== _generation) return ctx
186
216
  const responseData = normalizeEnvelope(ctx.data)
187
217
  if (!_isNext) {
188
- await _fetchStructure()
218
+ try {
219
+ await _fetchStructure(generation)
220
+ } catch (error) {
221
+ throw error
222
+ }
223
+ if (generation !== _generation) return ctx
189
224
  }
190
225
  const list = responseData.data ?? []
191
- // Manche Installationen liefern die Struktur in der Liste mit
192
- // (Kompatibilitäts-Flag `json.bone.structure.inlists`) — dann ist der
193
- // separate Aufruf oben umsonst gewesen, aber diese hier ist aktueller.
226
+ // Some installations deliver the structure along with the list
227
+ // (compatibility flag `json.bone.structure.inlists`) — then the
228
+ // separate call above was in vain, but this one is more current.
194
229
  if (responseData.structure) {
195
230
  structure.value = normalizeStructure(responseData.structure)
196
231
  }
@@ -199,12 +234,19 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
199
234
  data.value = _isNext ? data.value.concat(list) : list
200
235
  if (cached && !_isNext) {
201
236
  const entityKeys = list.map((s) => s.key).filter(Boolean)
202
- await setCached(ctx.url ?? listUrl.value, responseData, entityKeys)
237
+ await setCached(ctx.url ?? listUrl.value, responseData, entityKeys).catch((error) => {
238
+ throw error
239
+ })
203
240
  }
204
241
  ctx.data = data.value
205
242
  return ctx
206
243
  },
207
244
  onFetchError(ctx) {
245
+ // A superseded or aborted request is no failure: keep the current
246
+ // error as it is (null for the run that replaced this one).
247
+ if (ctx.context.generation !== _generation || ctx.error?.name === "AbortError") {
248
+ return { ...ctx, error: error.value }
249
+ }
208
250
  ctx.error = new HTTPError(ctx.response?.status, ctx.response?.statusText, ctx.error?.message, ctx.response)
209
251
  if (messages) {
210
252
  notify(
@@ -218,8 +260,8 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
218
260
 
219
261
  const { isFetching, isFinished, error, execute: _execute, abort } = useViurFetch(listUrl, { immediate }).json()
220
262
 
221
- /** Alles geladen: letzte Antwort da und kein Cursor mehr offen. Ab hier
222
- * dürfen Sortierung/Filter lokal auf dem Datensatz laufen. */
263
+ /** Everything loaded: last response is in and no cursor left open. From here
264
+ * on, sorting/filtering may run locally on the dataset. */
223
265
  const complete = computed(() => isFinished.value && !cursor.value)
224
266
 
225
267
  const fetchResult = {
@@ -245,11 +287,16 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
245
287
  params.value = { ...params.value, cursor: cursor.value }
246
288
  _isNext = true
247
289
  listUrl.value = _buildUrl()
248
- await executeAndThrow(fetchResult, _execute)
290
+ try {
291
+ await executeAndThrow(fetchResult, _execute)
292
+ } catch {
293
+ // already in `error`; rethrowing here would only be an unhandled rejection
294
+ }
249
295
  }
250
296
  })
251
297
 
252
298
  function execute() {
299
+ _generation++
253
300
  _isNext = false
254
301
  abort()
255
302
  data.value = []
@@ -262,37 +309,49 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
262
309
  return executeAndThrow(fetchResult, _execute)
263
310
  }
264
311
 
265
- /** Eine Seite nachladen; erkennt das ViUR-Ende (leere Seite mit derselben
266
- * Cursor-Marke) und löscht dann den Cursor. Liefert true bei Fortschritt. */
312
+ /** Load one more page; detects the ViUR end (empty page with the same
313
+ * cursor marker) and then clears the cursor. Returns true on progress. */
267
314
  async function _advance() {
268
- if (!cursor.value) return false
269
- const before = data.value.length
270
- const cursorBefore = cursor.value
271
- params.value = { ...params.value, cursor: cursor.value }
272
- _isNext = true
273
- listUrl.value = _buildUrl()
274
- await executeAndThrow(fetchResult, _execute)
275
- if (data.value.length === before && cursor.value === cursorBefore) {
276
- cursor.value = ""
277
- return false
315
+ try {
316
+ if (!cursor.value) return false
317
+ const before = data.value.length
318
+ const cursorBefore = cursor.value
319
+ params.value = { ...params.value, cursor: cursor.value }
320
+ _isNext = true
321
+ listUrl.value = _buildUrl()
322
+ await executeAndThrow(fetchResult, _execute)
323
+ if (data.value.length === before && cursor.value === cursorBefore) {
324
+ cursor.value = ""
325
+ return false
326
+ }
327
+ return true
328
+ } catch (error) {
329
+ throw error
278
330
  }
279
- return true
280
331
  }
281
332
 
282
- /** Die nächsten `pages` Seiten laden (Standard: eine) — z. B. direkt als
283
- * Handler der DataTable-Cursor-Leiste: `@load-more="({ pages }) =>
333
+ /** Load the next `pages` pages (default: one) — e.g. directly as the
334
+ * handler of the DataTable cursor bar: `@load-more="({ pages }) =>
284
335
  * pages === 'all' ? list.nextAll() : list.next(pages)"`. */
285
336
  async function next(pages = 1) {
286
- let remaining = pages
287
- while (remaining-- > 0) {
288
- if (!(await _advance())) return
337
+ try {
338
+ let remaining = pages
339
+ while (remaining-- > 0) {
340
+ if (!(await _advance())) return
341
+ }
342
+ } catch (error) {
343
+ throw error
289
344
  }
290
345
  }
291
346
 
292
- /** Alle restlichen Seiten laden ("Alles laden"). */
347
+ /** Load all remaining pages ("load everything"). */
293
348
  async function nextAll() {
294
- while (await _advance()) {
295
- /* bis zum Ende */
349
+ try {
350
+ while (await _advance()) {
351
+ /* until the end */
352
+ }
353
+ } catch (error) {
354
+ throw error
296
355
  }
297
356
  }
298
357
 
@@ -302,6 +361,7 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
302
361
  }
303
362
 
304
363
  function reset() {
364
+ _generation++
305
365
  abort()
306
366
  data.value = []
307
367
  structure.value = {}
@@ -352,7 +412,7 @@ export function useTreeFetch(
352
412
  const isFinished = computed(() => nodes.isFinished.value && leaves.isFinished.value)
353
413
  const error = computed(() => nodes.error.value ?? leaves.error.value)
354
414
  const hasMore = computed(() => !!(nodes.cursor.value || leaves.cursor.value))
355
- /** Beide Listen vollständig geladen — Sortierung/Filter dürfen lokal laufen. */
415
+ /** Both lists fully loaded — sorting/filtering may run locally. */
356
416
  const complete = computed(() => isFinished.value && !hasMore.value)
357
417
 
358
418
  function _both(fn) {
@@ -363,27 +423,39 @@ export function useTreeFetch(
363
423
  return _both((l) => l.execute())
364
424
  }
365
425
 
366
- // Eine Seite nachladen: erst Nodes zu Ende, dann Leaves. Das ViUR-Ende
367
- // (leere Seite mit derselben Cursor-Marke) erkennt useListFetch.next()
368
- // selbst und löscht dann seinen Cursor — hier zählt nur der Fortschritt.
426
+ // Load one more page: nodes to the end first, then leaves. useListFetch.next()
427
+ // detects the ViUR end (empty page with the same cursor marker) itself and
428
+ // then clears its cursor — only progress counts here.
369
429
  async function _nextOf(list) {
370
- const before = list.data.value.length
371
- await list.next()
372
- return list.data.value.length > before
430
+ try {
431
+ const before = list.data.value.length
432
+ await list.next()
433
+ return list.data.value.length > before
434
+ } catch (error) {
435
+ throw error
436
+ }
373
437
  }
374
438
 
375
439
  async function next() {
376
- if (nodes.cursor.value && (await _nextOf(nodes))) return
377
- if (leaves.cursor.value) await _nextOf(leaves)
440
+ try {
441
+ if (nodes.cursor.value && (await _nextOf(nodes))) return
442
+ if (leaves.cursor.value) await _nextOf(leaves)
443
+ } catch (error) {
444
+ throw error
445
+ }
378
446
  }
379
447
 
380
- /** Alle restlichen Seiten beider Listen laden ("Alles laden"). */
448
+ /** Load all remaining pages of both lists ("load everything"). */
381
449
  async function nextAll() {
382
- while (nodes.cursor.value) {
383
- if (!(await _nextOf(nodes))) break
384
- }
385
- while (leaves.cursor.value) {
386
- if (!(await _nextOf(leaves))) break
450
+ try {
451
+ while (nodes.cursor.value) {
452
+ if (!(await _nextOf(nodes))) break
453
+ }
454
+ while (leaves.cursor.value) {
455
+ if (!(await _nextOf(leaves))) break
456
+ }
457
+ } catch (error) {
458
+ throw error
387
459
  }
388
460
  }
389
461
 
@@ -459,7 +531,12 @@ export function createControllerFetch({
459
531
  },
460
532
  options: {
461
533
  async beforeFetch({ options, url, cancel }) {
462
- const skeyToken = secure ? ctxSkey || (await fetchSkeyToken(renderer)) : null
534
+ let skeyToken = null
535
+ try {
536
+ skeyToken = secure ? ctxSkey || (await fetchSkeyToken(renderer)) : null
537
+ } catch (error) {
538
+ throw error
539
+ }
463
540
  const base = create_url({ controller, endpoint, renderer, version, lang })
464
541
  const extraQs = query ? (typeof query === "string" ? query : new URLSearchParams(query).toString()) : ""
465
542
 
@@ -473,7 +550,9 @@ export function createControllerFetch({
473
550
  _cacheKey = qs ? `${base}?${qs}` : base
474
551
 
475
552
  if (cached) {
476
- const hit = await getCached(_cacheKey, ttl)
553
+ const hit = await getCached(_cacheKey, ttl).catch((error) => {
554
+ throw error
555
+ })
477
556
  if (hit) {
478
557
  _cachedHit = hit
479
558
  cancel()
@@ -525,18 +604,25 @@ export function createControllerFetch({
525
604
  }
526
605
  if (cached && method.toUpperCase() === "GET" && _cacheKey) {
527
606
  const entityKey = ctx.data?.data?.key
528
- await setCached(_cacheKey, ctx.data, entityKey ? [entityKey] : [])
607
+ await setCached(_cacheKey, ctx.data, entityKey ? [entityKey] : []).catch((error) => {
608
+ throw error
609
+ })
529
610
  }
530
611
  if (invalidates) {
531
612
  const keys = []
532
613
  if (data.key) keys.push(data.key)
533
614
  if (ctx.data?.data?.key) keys.push(ctx.data.data.key)
534
- if (keys.length) await invalidateByKey(keys)
615
+ if (keys.length)
616
+ await invalidateByKey(keys).catch((error) => {
617
+ throw error
618
+ })
535
619
  }
536
620
  if (invalidatesAll) {
537
621
  const base = import.meta.env.VITE_API_URL ?? window.location.origin
538
622
  const controllerPath = Array.isArray(controller) ? controller.join("/") : controller
539
- await clearCache(`${base}/${renderer}/${controllerPath}/`)
623
+ await clearCache(`${base}/${renderer}/${controllerPath}/`).catch((error) => {
624
+ throw error
625
+ })
540
626
  }
541
627
  return ctx
542
628
  },
@@ -586,47 +672,51 @@ export async function uploadFile(
586
672
  file,
587
673
  { target = undefined, publicUpload = false, renderer = null, version = null } = {}
588
674
  ) {
589
- const _renderer = renderer ?? import.meta.env.VITE_DEFAULT_RENDERER ?? "json"
590
- const fetchOpts = { credentials: "include", headers: { Accept: "application/json" }, method: "POST" }
591
-
592
- // Step 1: get upload URL
593
- const filedata = {
594
- fileName: file.name,
595
- mimeType: file.type || "application/octet-stream",
596
- size: file.size.toString(),
597
- }
598
- if (target) filedata.node = target
599
- if (publicUpload) filedata.public = true
600
- const fd1 = _toFormData(filedata)
601
- fd1.append("skey", await fetchSkeyToken(_renderer))
675
+ try {
676
+ const _renderer = renderer ?? import.meta.env.VITE_DEFAULT_RENDERER ?? "json"
677
+ const fetchOpts = { credentials: "include", headers: { Accept: "application/json" }, method: "POST" }
678
+
679
+ // Step 1: get upload URL
680
+ const filedata = {
681
+ fileName: file.name,
682
+ mimeType: file.type || "application/octet-stream",
683
+ size: file.size.toString(),
684
+ }
685
+ if (target) filedata.node = target
686
+ if (publicUpload) filedata.public = true
687
+ const fd1 = _toFormData(filedata)
688
+ fd1.append("skey", await fetchSkeyToken(_renderer))
689
+
690
+ const getUrlResp = await fetch(
691
+ create_url({ controller: "file", endpoint: "getUploadURL", renderer: _renderer, version }),
692
+ {
693
+ ...fetchOpts,
694
+ body: fd1,
695
+ }
696
+ )
697
+ if (!getUrlResp.ok) throw new Error(`getUploadURL failed: ${getUrlResp.status}`)
698
+ // Envelope-agnostic: v2 carries the payload in `data`, v1 in `values`.
699
+ const getUrlData = normalizeEnvelope(await getUrlResp.json())
700
+ const { uploadUrl, uploadKey } = getUrlData.data ?? getUrlData.values ?? {}
602
701
 
603
- const getUrlResp = await fetch(
604
- create_url({ controller: "file", endpoint: "getUploadURL", renderer: _renderer, version }),
605
- {
702
+ // Step 2: upload raw bytes to external storage (no-cors, no credentials)
703
+ await fetch(uploadUrl, { method: "POST", body: file, mode: "no-cors" })
704
+
705
+ // Step 3: register file in ViUR
706
+ const fd3 = _toFormData({ key: uploadKey, skelType: "leaf" })
707
+ fd3.append("skey", await fetchSkeyToken(_renderer))
708
+
709
+ const addResp = await fetch(create_url({ controller: "file", endpoint: "add", renderer: _renderer, version }), {
606
710
  ...fetchOpts,
607
- body: fd1,
608
- }
609
- )
610
- if (!getUrlResp.ok) throw new Error(`getUploadURL failed: ${getUrlResp.status}`)
611
- // Envelope-agnostic: v2 carries the payload in `data`, v1 in `values`.
612
- const getUrlData = normalizeEnvelope(await getUrlResp.json())
613
- const { uploadUrl, uploadKey } = getUrlData.data ?? getUrlData.values ?? {}
614
-
615
- // Step 2: upload raw bytes to external storage (no-cors, no credentials)
616
- await fetch(uploadUrl, { method: "POST", body: file, mode: "no-cors" })
617
-
618
- // Step 3: register file in ViUR
619
- const fd3 = _toFormData({ key: uploadKey, skelType: "leaf" })
620
- fd3.append("skey", await fetchSkeyToken(_renderer))
621
-
622
- const addResp = await fetch(create_url({ controller: "file", endpoint: "add", renderer: _renderer, version }), {
623
- ...fetchOpts,
624
- body: fd3,
625
- })
626
- if (!addResp.ok) throw new Error(`file/add failed: ${addResp.status}`)
627
- const addData = normalizeEnvelope(await addResp.json())
628
- if (addData.status !== "success") throw new Error(`Upload failed: ${JSON.stringify(addData)}`)
629
- return addData.data ?? addData.values
711
+ body: fd3,
712
+ })
713
+ if (!addResp.ok) throw new Error(`file/add failed: ${addResp.status}`)
714
+ const addData = normalizeEnvelope(await addResp.json())
715
+ if (addData.status !== "success") throw new Error(`Upload failed: ${JSON.stringify(addData)}`)
716
+ return addData.data ?? addData.values
717
+ } catch (error) {
718
+ throw error
719
+ }
630
720
  }
631
721
 
632
722
  function _absolute(url) {
@@ -12,6 +12,7 @@ export default {
12
12
  list: "Auflisten",
13
13
  addEmpty: "Neuer Eintrag",
14
14
  no_entries: "Keine Einträge",
15
+ errors_summary: "Fehler",
15
16
  },
16
17
  lang: "Sprache",
17
18
  bones: {
@@ -27,6 +28,13 @@ export default {
27
28
  phone: {
28
29
  placeholder: "Telefon/Fax",
29
30
  },
31
+ password: {
32
+ mismatch: "Die eingegebenen Passwörter stimmen nicht überein.",
33
+ empty: "Das eingegebene Passwort ist leer.",
34
+ },
35
+ text: {
36
+ openInNewWindow: "Link in neuem Fenster öffnen",
37
+ },
30
38
  },
31
39
 
32
40
  errors: {
@@ -77,6 +85,10 @@ export default {
77
85
  collapseRow: "Zeile einklappen",
78
86
  expandRow: "Zeile ausklappen",
79
87
  noData: "Keine Daten",
88
+ noValues: "Keine Werte",
89
+ clear: "Leeren",
90
+ filterEquals: "genau …",
91
+ filterContains: "enthält …",
80
92
  noResults: "Keine Treffer",
81
93
  filterFrom: "von",
82
94
  filterTo: "bis",
@@ -108,6 +120,7 @@ export default {
108
120
  path: "Pfad",
109
121
  navigation: "Navigation",
110
122
  uploadHint: "Dateien hier ablegen",
123
+ name: "Name",
111
124
  },
112
125
  form: {
113
126
  next: "Weiter",
@@ -122,6 +135,7 @@ export default {
122
135
  original: "Original Sprache ({lang}):",
123
136
  target: "Zielsprache ({lang}):",
124
137
  run: "Übersetzen",
138
+ name: "Übersetzen",
125
139
  applyNext: "Übernehmen und nächste leere Sprache",
126
140
  apply: "Ziel für {lang} übernehmen",
127
141
  },
@@ -130,6 +144,23 @@ export default {
130
144
  instructions: "weitere Anweisungen",
131
145
  run: "Beschreibung erstellen",
132
146
  apply: "Beschreibung übernehmen",
147
+ name: "Bildbeschreibung erstellen",
148
+ title: "Bild beschreiben",
149
+ },
150
+ debug: {
151
+ name: "Debug",
152
+ title: "Bone Debug-Informationen anzeigen",
153
+ structure: "Bone-Struktur",
154
+ value: "Bone-Wert",
155
+ state: "Bone-Status",
156
+ context: "Kontext",
157
+ },
158
+ placeholder: {
159
+ name: "Platzhalter",
160
+ title: "Platzhalter (TODO: entfernen)",
161
+ },
162
+ delete: {
163
+ name: "Löschen",
133
164
  },
134
165
  },
135
166
  messages: {