viur-ui 0.0.1

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 (114) hide show
  1. package/README.md +79 -0
  2. package/index.d.ts +351 -0
  3. package/index.js +46 -0
  4. package/package.json +84 -0
  5. package/src/components/Browser/Browser.vue +496 -0
  6. package/src/components/Browser/TreeNav.vue +408 -0
  7. package/src/components/Browser/ViurBrowser.d.vue.ts +64 -0
  8. package/src/components/Browser/ViurBrowser.vue +271 -0
  9. package/src/components/Browser/dragSession.ts +134 -0
  10. package/src/components/Browser/index.ts +14 -0
  11. package/src/components/Browser/types.ts +30 -0
  12. package/src/components/DataTable/ColumnFilterPanel.vue +225 -0
  13. package/src/components/DataTable/DataTable.vue +3155 -0
  14. package/src/components/DataTable/ViurDataTable.d.vue.ts +72 -0
  15. package/src/components/DataTable/ViurDataTable.vue +331 -0
  16. package/src/components/DataTable/columns.ts +400 -0
  17. package/src/components/DataTable/index.ts +60 -0
  18. package/src/components/DataTable/rows.ts +446 -0
  19. package/src/components/DataTable/types.ts +267 -0
  20. package/src/components/DataTable/useColumnLayout.ts +294 -0
  21. package/src/components/DataTable/useGridNavigation.ts +193 -0
  22. package/src/components/DataTable/useVirtualRows.ts +126 -0
  23. package/src/components/DataTable/viurColumns.ts +330 -0
  24. package/src/forms/ViForm.d.vue.ts +106 -0
  25. package/src/forms/ViForm.vue +592 -0
  26. package/src/forms/ViFormCategory.vue +57 -0
  27. package/src/forms/bones/Bone.vue +1023 -0
  28. package/src/forms/bones/BoneActions.vue +127 -0
  29. package/src/forms/bones/BoneLabel.vue +122 -0
  30. package/src/forms/bones/BoneMultipleList.vue +91 -0
  31. package/src/forms/bones/BoneMultipleWrapper.vue +105 -0
  32. package/src/forms/bones/BoneNestedWrapper.vue +155 -0
  33. package/src/forms/bones/BoneSingleEntry.vue +51 -0
  34. package/src/forms/bones/FilterBone.vue +694 -0
  35. package/src/forms/bones/actionbar/defaultBar.vue +98 -0
  36. package/src/forms/bones/actionbar/fileBar.vue +165 -0
  37. package/src/forms/bones/actionbar/relationalBar.vue +125 -0
  38. package/src/forms/bones/actions/ActionDialog.vue +70 -0
  39. package/src/forms/bones/actions/Debug.vue +106 -0
  40. package/src/forms/bones/actions/Delete.vue +13 -0
  41. package/src/forms/bones/actions/DescribeImage.vue +201 -0
  42. package/src/forms/bones/actions/Download.vue +27 -0
  43. package/src/forms/bones/actions/Placeholder.vue +14 -0
  44. package/src/forms/bones/actions/Translation.vue +238 -0
  45. package/src/forms/bones/actions/index.js +280 -0
  46. package/src/forms/bones/bone.d.ts +45 -0
  47. package/src/forms/bones/country_information.json +779 -0
  48. package/src/forms/bones/index.js +225 -0
  49. package/src/forms/bones/uploadAdapter.js +46 -0
  50. package/src/forms/bones/utils.js +284 -0
  51. package/src/forms/bones/view/base_item.vue +35 -0
  52. package/src/forms/bones/view/boneLogic.js +202 -0
  53. package/src/forms/bones/view/index.js +5 -0
  54. package/src/forms/bones/widgets/booleanBone.vue +83 -0
  55. package/src/forms/bones/widgets/booleanBoneChoose.vue +57 -0
  56. package/src/forms/bones/widgets/booleanBoneSelect.vue +58 -0
  57. package/src/forms/bones/widgets/codeBone.vue +214 -0
  58. package/src/forms/bones/widgets/colorBone.vue +75 -0
  59. package/src/forms/bones/widgets/dateBone.vue +114 -0
  60. package/src/forms/bones/widgets/emailBone.vue +100 -0
  61. package/src/forms/bones/widgets/fileBone.vue +273 -0
  62. package/src/forms/bones/widgets/jsonBone.vue +63 -0
  63. package/src/forms/bones/widgets/keyBone.vue +71 -0
  64. package/src/forms/bones/widgets/numericBone.vue +125 -0
  65. package/src/forms/bones/widgets/passwordBone.vue +215 -0
  66. package/src/forms/bones/widgets/phoneBone.vue +173 -0
  67. package/src/forms/bones/widgets/rawBone.vue +94 -0
  68. package/src/forms/bones/widgets/recordBone.vue +64 -0
  69. package/src/forms/bones/widgets/relationalBone.vue +164 -0
  70. package/src/forms/bones/widgets/relationalBoneSelect.vue +120 -0
  71. package/src/forms/bones/widgets/selectBone.vue +122 -0
  72. package/src/forms/bones/widgets/selectBoneChoose.vue +104 -0
  73. package/src/forms/bones/widgets/spatialBone.vue +111 -0
  74. package/src/forms/bones/widgets/stringBone.vue +139 -0
  75. package/src/forms/bones/widgets/textBone.vue +641 -0
  76. package/src/forms/install.d.ts +4 -0
  77. package/src/forms/install.js +43 -0
  78. package/src/forms/layouts/LayloutList2Columns.vue +57 -0
  79. package/src/forms/layouts/LayoutCategory.vue +38 -0
  80. package/src/forms/layouts/LayoutList.vue +31 -0
  81. package/src/forms/utils.js +601 -0
  82. package/src/public/assets/icons/fontawesome-manifest.json +8 -0
  83. package/src/public/assets/icons/regular/file.svg +1 -0
  84. package/src/public/assets/icons/solid/arrows-left-right.svg +1 -0
  85. package/src/public/assets/icons/solid/bug.svg +1 -0
  86. package/src/public/assets/icons/solid/check.svg +1 -0
  87. package/src/public/assets/icons/solid/circle-info.svg +1 -0
  88. package/src/public/assets/icons/solid/compress.svg +1 -0
  89. package/src/public/assets/icons/solid/download.svg +1 -0
  90. package/src/public/assets/icons/solid/expand.svg +1 -0
  91. package/src/public/assets/icons/solid/grip-vertical.svg +1 -0
  92. package/src/public/assets/icons/solid/plus.svg +1 -0
  93. package/src/public/assets/icons/solid/question.svg +1 -0
  94. package/src/public/assets/icons/solid/triangle-exclamation.svg +1 -0
  95. package/src/public/assets/icons/solid/upload.svg +1 -0
  96. package/src/public/assets/icons/solid/xmark.svg +1 -0
  97. package/src/request/cache.js +183 -0
  98. package/src/request/configStore.js +516 -0
  99. package/src/request/envelope.js +211 -0
  100. package/src/request/index.js +35 -0
  101. package/src/request/messageStore.js +201 -0
  102. package/src/request/request.js +752 -0
  103. package/src/request/skel.js +90 -0
  104. package/src/theme/colorStore.js +168 -0
  105. package/src/translations/de.js +178 -0
  106. package/src/translations/en.js +177 -0
  107. package/src/user/LoginScreen.d.vue.ts +54 -0
  108. package/src/user/LoginScreen.vue +627 -0
  109. package/src/user/google.js +75 -0
  110. package/src/user/index.js +3 -0
  111. package/src/user/store.d.ts +59 -0
  112. package/src/user/store.js +183 -0
  113. package/src/webawesomeCompat.css +72 -0
  114. package/src/webawesomeConfig.js +61 -0
@@ -0,0 +1,211 @@
1
+ // --------------------------------------------------------------------------- //
2
+ // Envelope normalization — v1 → v2 //
3
+ // --------------------------------------------------------------------------- //
4
+ //
5
+ // viur-ui works PRIMARILY on the viur-actions "envelope v2" response shape
6
+ // (see sources/viur-actions/src/viur/actions/envelope.py). To also keep
7
+ // supporting the classic v1 ViUR JSON protocol (`values` / `skellist` /
8
+ // `structure` / `errors` / `action`), we normalize every response UP to the
9
+ // canonical v2 envelope at the `.json()`/parse boundary.
10
+ //
11
+ // This is the exact inverse of vi-vue-utils, which normalizes v2 DOWN to v1.
12
+ // There, v1 is canonical and v2 responses are adapted; here, v2 is canonical
13
+ // and v1 responses are adapted.
14
+ //
15
+ // The normalization is transparent for bodies that are already v2 (they carry
16
+ // the JSON number `version: 2`) and for non-ViUR payloads (skey arrays, the
17
+ // version-agnostic `getStructure` map, custom endpoints) — those pass through
18
+ // unchanged.
19
+ //
20
+ // By default the request layer hits the unversioned `/json/…` endpoint, which
21
+ // the server aliases to the newest wire format (envelope v2). A specific
22
+ // version can be pinned via the separate `version` parameter (`2`/`"v2"` for
23
+ // v2, `1`/`"v1"` for classic v1) — see `create_url` in request.js. Either way,
24
+ // this module guarantees viur-ui consumes a v2-shaped body.
25
+
26
+ /**
27
+ * Canonical v2 envelope field set (see viur-actions envelope.py):
28
+ * version, action, status, step, step_status, steps, follow,
29
+ * datatype, module, structure, data, errors
30
+ * plus a list-only `meta: { cursor, orders }`.
31
+ */
32
+
33
+ /** v1 actions whose selected value IS the url of the next step. */
34
+ const SELECTION_ACTIONS = ["select_authentication_provider", "select_secondfactor_provider"]
35
+
36
+ /**
37
+ * Target url of the next step in the CLASSIC v1 login.
38
+ *
39
+ * v2 (viur-actions) drives a multi-step action itself: every response carries
40
+ * the `follow` url of the next step. v1 has no such field — there the target IS
41
+ * the value the user picked: `/user/login` renders a single select whose option
42
+ * KEYS are the provider urls ("/vi/user/auth_userpassword/login", …), and a POST
43
+ * echoes the chosen key back in the body. So the selection doubles as the
44
+ * follow.
45
+ *
46
+ * The same shape appears twice in the flow: choosing the PRIMARY provider
47
+ * (`select_authentication_provider`) and choosing the SECOND FACTOR
48
+ * (`select_secondfactor_provider`).
49
+ *
50
+ * Only applied to a body that really is such a selection: one of those actions,
51
+ * a select bone, and a chosen value that is one of that bone's own option keys.
52
+ * Returns null otherwise.
53
+ *
54
+ * @param {*} body normalized (v2-shaped) body
55
+ * @returns {string|null}
56
+ */
57
+ export function v1SelectionFollow(body) {
58
+ if (!body || typeof body !== "object" || !SELECTION_ACTIONS.includes(body["action"])) {
59
+ return null
60
+ }
61
+ const values = body["data"] ?? body["values"] ?? {}
62
+ const structure = body["structure"] ?? {}
63
+ for (const [name, bone] of Object.entries(structure)) {
64
+ if (!String(bone?.["type"] ?? "").startsWith("select")) continue
65
+ const chosen = values?.[name]
66
+ if (typeof chosen !== "string" || !/^(\/|https?:\/\/)/.test(chosen)) continue
67
+ // Nur wenn der Wert tatsächlich eine der angebotenen Ziel-URLs ist.
68
+ if (bone?.["values"] && Object.prototype.hasOwnProperty.call(bone["values"], chosen)) return chosen
69
+ }
70
+ return null
71
+ }
72
+
73
+ /**
74
+ * Normalize the `errors` array so consumers can rely on `fieldPath` being a
75
+ * LIST of path segments (that is how viur-core declares it and how the bones
76
+ * match errors to fields: `fieldPath[0] === boneName`).
77
+ *
78
+ * Some server paths hand a plain string instead — viur-core's userpassword
79
+ * login, for example, builds its "Invalid username or password provided" error
80
+ * with the bare bone name. A string would then be indexed character-wise and
81
+ * the error would silently never reach any field.
82
+ *
83
+ * @param {*} errors
84
+ * @returns {Array}
85
+ */
86
+ function normalizeErrors(errors) {
87
+ if (!Array.isArray(errors)) return []
88
+ return errors.map((error) => {
89
+ if (!error || typeof error !== "object") return error
90
+ const fieldPath = error["fieldPath"]
91
+ if (fieldPath == null || Array.isArray(fieldPath)) return error
92
+ return { ...error, fieldPath: String(fieldPath).split(".").filter(Boolean) }
93
+ })
94
+ }
95
+
96
+ /**
97
+ * True if `data` is an already-parsed viur-actions envelope-v2 body.
98
+ * The wire carries `version` as the JSON number `2`; we also tolerate the
99
+ * string `"2"` defensively.
100
+ * @param {*} data
101
+ * @returns {boolean}
102
+ */
103
+ export function isEnvelopeV2(data) {
104
+ return (
105
+ !!data && typeof data === "object" && !Array.isArray(data) && (data["version"] === 2 || data["version"] === "2")
106
+ )
107
+ }
108
+
109
+ /**
110
+ * Map a classic v1 `action` string (+ errors) to a v2 `(action, status)` pair.
111
+ * Inverse of vi-vue-utils' `v1ActionString`.
112
+ *
113
+ * - `addSuccess` / `editSuccess` / `cloneSuccess` / `deleteSuccess` / `moveSuccess` → the plain
114
+ * verb with `status: "success"`.
115
+ * - `view` / `list` → the verb with `status: "success"` (a completed read).
116
+ * - any other verb (`add` / `edit` / `clone` / custom): `status: "rejected"`
117
+ * when the response carries errors, else `status: "init"` (a freshly rendered
118
+ * or in-flight form).
119
+ *
120
+ * @param {string|undefined} action v1 action string
121
+ * @param {Array|undefined} errors v1 errors array
122
+ * @returns {{ action: string, status: string }}
123
+ */
124
+ function v2ActionStatus(action, errors) {
125
+ switch (action) {
126
+ case "addSuccess":
127
+ return { action: "add", status: "success" }
128
+ case "editSuccess":
129
+ return { action: "edit", status: "success" }
130
+ case "cloneSuccess":
131
+ return { action: "clone", status: "success" }
132
+ case "deleteSuccess":
133
+ return { action: "delete", status: "success" }
134
+ case "moveSuccess":
135
+ return { action: "move", status: "success" }
136
+ }
137
+
138
+ if (action === "view" || action === "list") {
139
+ return { action, status: "success" }
140
+ }
141
+
142
+ const status = Array.isArray(errors) && errors.length > 0 ? "rejected" : "init"
143
+ return { action: action ?? null, status }
144
+ }
145
+
146
+ /**
147
+ * Normalize a parsed response body to the canonical v2 envelope shape.
148
+ *
149
+ * - Bodies already in v2 pass through (only `errors`/`meta` are defensively
150
+ * ensured).
151
+ * - Classic v1 bodies (carrying `skellist`, `values`, or a v1 `action`) are
152
+ * lifted to v2: `data` (from `skellist`/`values`), `datatype`, `structure`,
153
+ * `errors`, `follow`, `(action, status)` and — for lists — `meta`. The
154
+ * original v1 fields are kept alongside, so the result is a superset and the
155
+ * transform is idempotent.
156
+ * - Everything else (arrays, primitives, non-ViUR objects such as the skey list
157
+ * or the `getStructure` map) passes through unchanged.
158
+ *
159
+ * @param {*} data parsed response body
160
+ * @returns {*} v2-shaped body (or the input unchanged)
161
+ */
162
+ export function normalizeEnvelope(data) {
163
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
164
+ return data
165
+ }
166
+
167
+ if (isEnvelopeV2(data)) {
168
+ const out = { ...data }
169
+ out["version"] = 2
170
+ out["errors"] = normalizeErrors(data["errors"])
171
+ if (data["datatype"] === "list" && !data["meta"]) {
172
+ out["meta"] = { cursor: data["cursor"] ?? null, orders: data["orders"] || [] }
173
+ }
174
+ return out
175
+ }
176
+
177
+ const isList = Array.isArray(data["skellist"])
178
+ const isEntity = "values" in data
179
+ const hasV1Action = typeof data["action"] === "string"
180
+
181
+ // Not a ViUR structured response (skey, getStructure map, custom payload):
182
+ // leave untouched.
183
+ if (!isList && !isEntity && !hasV1Action) {
184
+ return data
185
+ }
186
+
187
+ const out = { ...data }
188
+ out["version"] = 2
189
+ const { action, status } = v2ActionStatus(data["action"], data["errors"])
190
+ out["action"] = action
191
+ out["status"] = status
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)
198
+ out["follow"] = data["follow"] ?? data["next_url"] ?? data["params"]?.["action_url"] ?? null
199
+ out["structure"] = data["structure"] ?? null
200
+
201
+ if (isList) {
202
+ out["datatype"] = "list"
203
+ out["data"] = data["skellist"]
204
+ out["meta"] = { cursor: data["cursor"] ?? null, orders: data["orders"] || [] }
205
+ } else {
206
+ out["datatype"] = "entity"
207
+ out["data"] = isEntity ? data["values"] : (data["data"] ?? null)
208
+ }
209
+
210
+ return out
211
+ }
@@ -0,0 +1,35 @@
1
+ export {
2
+ useAddFetch,
3
+ useCloneFetch,
4
+ usePreviewFetch,
5
+ useMoveFetch,
6
+ useListRootNodesFetch,
7
+ useDeleteFetch,
8
+ useEditFetch,
9
+ useListFetch,
10
+ useListFetchAll,
11
+ useTreeFetch,
12
+ useViewFetch,
13
+ useStructureFetch,
14
+ uploadFile,
15
+ downloadUrlFor,
16
+ serveUrlFor,
17
+ resetState,
18
+ HTTPError,
19
+ createControllerFetch,
20
+ } from "./request.js"
21
+ export { getCached, setCached, invalidateByKey, clearCache } from "./cache.js"
22
+ export { skelDataToFormData, normalizeStructure } from "./skel.js"
23
+ export { normalizeEnvelope, isEnvelopeV2, v1SelectionFollow } from "./envelope.js"
24
+ export { useConfigStore, canAccessAdminConfig } from "./configStore.js"
25
+ export {
26
+ buildModuleTree,
27
+ flattenModuleTree,
28
+ findModule,
29
+ visibleModuleTree,
30
+ hasModuleAccess,
31
+ handlerType,
32
+ normalizeModulePath,
33
+ baseModuleName,
34
+ } from "./configStore.js"
35
+ export { useMessageStore, getMessageStore, notify, requestMessage } from "./messageStore.js"
@@ -0,0 +1,201 @@
1
+ import { defineStore, getActivePinia } from "pinia"
2
+ import { reactive, computed } from "vue"
3
+
4
+ // --------------------------------------------------------------------------- //
5
+ // Message store — user feedback (toasts / notifications) //
6
+ // --------------------------------------------------------------------------- //
7
+ //
8
+ // A UI-agnostic queue of feedback messages: "entry saved", "login failed",
9
+ // "request failed (500)". The REQUEST LAYER feeds it automatically (see
10
+ // `notify()` and request.js): a successful POST (add/edit/delete/clone/move/
11
+ // login/…) pushes a success, a rejected envelope or a HTTP error pushes an
12
+ // error. Components may push their own messages as well.
13
+ //
14
+ // Opt-in by existence: `notify()` only does something when a Pinia instance is
15
+ // active (`getActivePinia()`), i.e. the app installed Pinia. Without Pinia the
16
+ // request layer works exactly as before. A single form can switch it off via
17
+ // `<ViForm :use-message-store="false">`, any request via `ctx.messages = false`.
18
+ //
19
+ // Messages carry a translation `code` (e.g. "messages.request.edit.success")
20
+ // plus a generic `fallbackCode`, so the rendering component decides the text:
21
+ // const text = te(m.code) ? t(m.code, m.params) : t(m.fallbackCode, m.params)
22
+ // Optional `text` holds a ready-made string (e.g. a server error message).
23
+
24
+ const DEFAULT_TIMEOUTS = { success: 5000, info: 5000, warning: 8000, error: 0 } // 0 = sticky
25
+
26
+ let _nextId = 1
27
+
28
+ export const useMessageStore = defineStore("viurMessages", () => {
29
+ const state = reactive({
30
+ messages: [],
31
+ timeouts: { ...DEFAULT_TIMEOUTS },
32
+ // keep at most this many messages; the oldest are dropped
33
+ max: 50,
34
+ })
35
+ const _timers = new Map()
36
+
37
+ const latest = computed(() => state.messages.at(-1) ?? null)
38
+ const errors = computed(() => state.messages.filter((m) => m.type === "error"))
39
+ const count = computed(() => state.messages.length)
40
+
41
+ function configure({ timeouts, max } = {}) {
42
+ if (timeouts) state.timeouts = { ...state.timeouts, ...timeouts }
43
+ if (typeof max === "number") state.max = max
44
+ }
45
+
46
+ /**
47
+ * Add a message. Returns the stored message (with `id`).
48
+ * @param {object} message
49
+ * @param {"success"|"error"|"warning"|"info"} [message.type]
50
+ * @param {string} [message.code] translation key
51
+ * @param {string} [message.fallbackCode] generic translation key
52
+ * @param {string|null} [message.text] ready-made text (wins over code in UIs)
53
+ * @param {object} [message.params] translation params
54
+ * @param {string|null} [message.module] controller path ("user", "shop.order")
55
+ * @param {string|null} [message.action] envelope action / endpoint
56
+ * @param {string|null} [message.key] entity key
57
+ * @param {Array} [message.errors] envelope errors
58
+ * @param {number|null} [message.status] HTTP status
59
+ * @param {number} [message.timeout] ms until auto-dismiss (0 = sticky)
60
+ */
61
+ function push(message) {
62
+ const type = message.type ?? "info"
63
+ const entry = {
64
+ id: _nextId++,
65
+ type,
66
+ code: message.code ?? null,
67
+ fallbackCode: message.fallbackCode ?? `messages.request.default.${type}`,
68
+ text: message.text ?? null,
69
+ params: message.params ?? {},
70
+ module: message.module ?? null,
71
+ action: message.action ?? null,
72
+ key: message.key ?? null,
73
+ errors: message.errors ?? [],
74
+ status: message.status ?? null,
75
+ timeout: message.timeout ?? state.timeouts[type] ?? 0,
76
+ timestamp: Date.now(),
77
+ }
78
+ state.messages.push(entry)
79
+ while (state.messages.length > state.max) {
80
+ const dropped = state.messages.shift()
81
+ _clearTimer(dropped.id)
82
+ }
83
+ if (entry.timeout > 0 && typeof setTimeout === "function") {
84
+ _timers.set(
85
+ entry.id,
86
+ setTimeout(() => remove(entry.id), entry.timeout)
87
+ )
88
+ }
89
+ return entry
90
+ }
91
+
92
+ const _shortcut =
93
+ (type) =>
94
+ (codeOrMessage, extra = {}) =>
95
+ push(typeof codeOrMessage === "string" ? { type, code: codeOrMessage, ...extra } : { ...codeOrMessage, type })
96
+ const success = _shortcut("success")
97
+ const error = _shortcut("error")
98
+ const warning = _shortcut("warning")
99
+ const info = _shortcut("info")
100
+
101
+ function _clearTimer(id) {
102
+ const timer = _timers.get(id)
103
+ if (timer) {
104
+ clearTimeout(timer)
105
+ _timers.delete(id)
106
+ }
107
+ }
108
+
109
+ function remove(id) {
110
+ _clearTimer(id)
111
+ const index = state.messages.findIndex((m) => m.id === id)
112
+ if (index !== -1) state.messages.splice(index, 1)
113
+ }
114
+
115
+ function clear(type = null) {
116
+ const keep = type ? state.messages.filter((m) => m.type !== type) : []
117
+ for (const m of state.messages) if (!keep.includes(m)) _clearTimer(m.id)
118
+ state.messages = keep
119
+ }
120
+
121
+ return { state, latest, errors, count, configure, push, success, error, warning, info, remove, clear }
122
+ })
123
+
124
+ /**
125
+ * The message store IF the app installed Pinia — else null. This is what makes
126
+ * the feedback opt-in by existence.
127
+ * @returns {ReturnType<typeof useMessageStore>|null}
128
+ */
129
+ export function getMessageStore() {
130
+ try {
131
+ if (!getActivePinia()) return null
132
+ return useMessageStore()
133
+ } catch {
134
+ return null
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Push a message when a store exists; no-op otherwise. Used by the request
140
+ * layer so it never depends on Pinia being installed.
141
+ * @returns {object|null} the stored message or null
142
+ */
143
+ export function notify(message) {
144
+ const store = getMessageStore()
145
+ return store ? store.push(message) : null
146
+ }
147
+
148
+ /**
149
+ * Build the feedback message for a finished request from its normalized
150
+ * envelope (or a HTTP error). Returns null when nothing should be shown
151
+ * (GET reads, form renders, intermediate multistep steps).
152
+ *
153
+ * @param {{ method: string, endpoint: string, module: string|null, body?: any, error?: Error|null }} info
154
+ */
155
+ export function requestMessage({ method, endpoint, module, body = undefined, error = null }) {
156
+ if (error) {
157
+ const status = error.statusCode ?? error.status ?? null
158
+ return {
159
+ type: "error",
160
+ code: `messages.request.${endpoint}.error`,
161
+ fallbackCode: "messages.request.default.error",
162
+ text: error.message && !/^\d{3}$/.test(error.message) ? error.message : null,
163
+ params: { status, module, action: endpoint },
164
+ module,
165
+ action: endpoint,
166
+ status,
167
+ }
168
+ }
169
+ if (method.toUpperCase() === "POST") {
170
+ const isObject = body && typeof body === "object"
171
+ const action = (isObject && body.action) || endpoint
172
+ // a non-envelope body on a POST (e.g. v1 delete answers the string "OKAY")
173
+ // counts as success
174
+ const status = isObject ? body.status : "success"
175
+ const key = isObject ? (body.data?.key ?? null) : null
176
+ if (status === "success") {
177
+ return {
178
+ type: "success",
179
+ code: `messages.request.${action}.success`,
180
+ fallbackCode: "messages.request.default.success",
181
+ params: { module, action },
182
+ module,
183
+ action,
184
+ key,
185
+ }
186
+ }
187
+ if (status === "rejected") {
188
+ return {
189
+ type: "error",
190
+ code: `messages.request.${action}.rejected`,
191
+ fallbackCode: "messages.request.default.rejected",
192
+ params: { module, action, count: body.errors?.length ?? 0 },
193
+ module,
194
+ action,
195
+ key,
196
+ errors: body.errors ?? [],
197
+ }
198
+ }
199
+ }
200
+ return null
201
+ }