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.
- package/README.md +79 -0
- package/index.d.ts +351 -0
- package/index.js +46 -0
- package/package.json +84 -0
- package/src/components/Browser/Browser.vue +496 -0
- package/src/components/Browser/TreeNav.vue +408 -0
- package/src/components/Browser/ViurBrowser.d.vue.ts +64 -0
- package/src/components/Browser/ViurBrowser.vue +271 -0
- package/src/components/Browser/dragSession.ts +134 -0
- package/src/components/Browser/index.ts +14 -0
- package/src/components/Browser/types.ts +30 -0
- package/src/components/DataTable/ColumnFilterPanel.vue +225 -0
- package/src/components/DataTable/DataTable.vue +3155 -0
- package/src/components/DataTable/ViurDataTable.d.vue.ts +72 -0
- package/src/components/DataTable/ViurDataTable.vue +331 -0
- package/src/components/DataTable/columns.ts +400 -0
- package/src/components/DataTable/index.ts +60 -0
- package/src/components/DataTable/rows.ts +446 -0
- package/src/components/DataTable/types.ts +267 -0
- package/src/components/DataTable/useColumnLayout.ts +294 -0
- package/src/components/DataTable/useGridNavigation.ts +193 -0
- package/src/components/DataTable/useVirtualRows.ts +126 -0
- package/src/components/DataTable/viurColumns.ts +330 -0
- package/src/forms/ViForm.d.vue.ts +106 -0
- package/src/forms/ViForm.vue +592 -0
- package/src/forms/ViFormCategory.vue +57 -0
- package/src/forms/bones/Bone.vue +1023 -0
- package/src/forms/bones/BoneActions.vue +127 -0
- package/src/forms/bones/BoneLabel.vue +122 -0
- package/src/forms/bones/BoneMultipleList.vue +91 -0
- package/src/forms/bones/BoneMultipleWrapper.vue +105 -0
- package/src/forms/bones/BoneNestedWrapper.vue +155 -0
- package/src/forms/bones/BoneSingleEntry.vue +51 -0
- package/src/forms/bones/FilterBone.vue +694 -0
- package/src/forms/bones/actionbar/defaultBar.vue +98 -0
- package/src/forms/bones/actionbar/fileBar.vue +165 -0
- package/src/forms/bones/actionbar/relationalBar.vue +125 -0
- package/src/forms/bones/actions/ActionDialog.vue +70 -0
- package/src/forms/bones/actions/Debug.vue +106 -0
- package/src/forms/bones/actions/Delete.vue +13 -0
- package/src/forms/bones/actions/DescribeImage.vue +201 -0
- package/src/forms/bones/actions/Download.vue +27 -0
- package/src/forms/bones/actions/Placeholder.vue +14 -0
- package/src/forms/bones/actions/Translation.vue +238 -0
- package/src/forms/bones/actions/index.js +280 -0
- package/src/forms/bones/bone.d.ts +45 -0
- package/src/forms/bones/country_information.json +779 -0
- package/src/forms/bones/index.js +225 -0
- package/src/forms/bones/uploadAdapter.js +46 -0
- package/src/forms/bones/utils.js +284 -0
- package/src/forms/bones/view/base_item.vue +35 -0
- package/src/forms/bones/view/boneLogic.js +202 -0
- package/src/forms/bones/view/index.js +5 -0
- package/src/forms/bones/widgets/booleanBone.vue +83 -0
- package/src/forms/bones/widgets/booleanBoneChoose.vue +57 -0
- package/src/forms/bones/widgets/booleanBoneSelect.vue +58 -0
- package/src/forms/bones/widgets/codeBone.vue +214 -0
- package/src/forms/bones/widgets/colorBone.vue +75 -0
- package/src/forms/bones/widgets/dateBone.vue +114 -0
- package/src/forms/bones/widgets/emailBone.vue +100 -0
- package/src/forms/bones/widgets/fileBone.vue +273 -0
- package/src/forms/bones/widgets/jsonBone.vue +63 -0
- package/src/forms/bones/widgets/keyBone.vue +71 -0
- package/src/forms/bones/widgets/numericBone.vue +125 -0
- package/src/forms/bones/widgets/passwordBone.vue +215 -0
- package/src/forms/bones/widgets/phoneBone.vue +173 -0
- package/src/forms/bones/widgets/rawBone.vue +94 -0
- package/src/forms/bones/widgets/recordBone.vue +64 -0
- package/src/forms/bones/widgets/relationalBone.vue +164 -0
- package/src/forms/bones/widgets/relationalBoneSelect.vue +120 -0
- package/src/forms/bones/widgets/selectBone.vue +122 -0
- package/src/forms/bones/widgets/selectBoneChoose.vue +104 -0
- package/src/forms/bones/widgets/spatialBone.vue +111 -0
- package/src/forms/bones/widgets/stringBone.vue +139 -0
- package/src/forms/bones/widgets/textBone.vue +641 -0
- package/src/forms/install.d.ts +4 -0
- package/src/forms/install.js +43 -0
- package/src/forms/layouts/LayloutList2Columns.vue +57 -0
- package/src/forms/layouts/LayoutCategory.vue +38 -0
- package/src/forms/layouts/LayoutList.vue +31 -0
- package/src/forms/utils.js +601 -0
- package/src/public/assets/icons/fontawesome-manifest.json +8 -0
- package/src/public/assets/icons/regular/file.svg +1 -0
- package/src/public/assets/icons/solid/arrows-left-right.svg +1 -0
- package/src/public/assets/icons/solid/bug.svg +1 -0
- package/src/public/assets/icons/solid/check.svg +1 -0
- package/src/public/assets/icons/solid/circle-info.svg +1 -0
- package/src/public/assets/icons/solid/compress.svg +1 -0
- package/src/public/assets/icons/solid/download.svg +1 -0
- package/src/public/assets/icons/solid/expand.svg +1 -0
- package/src/public/assets/icons/solid/grip-vertical.svg +1 -0
- package/src/public/assets/icons/solid/plus.svg +1 -0
- package/src/public/assets/icons/solid/question.svg +1 -0
- package/src/public/assets/icons/solid/triangle-exclamation.svg +1 -0
- package/src/public/assets/icons/solid/upload.svg +1 -0
- package/src/public/assets/icons/solid/xmark.svg +1 -0
- package/src/request/cache.js +183 -0
- package/src/request/configStore.js +516 -0
- package/src/request/envelope.js +211 -0
- package/src/request/index.js +35 -0
- package/src/request/messageStore.js +201 -0
- package/src/request/request.js +752 -0
- package/src/request/skel.js +90 -0
- package/src/theme/colorStore.js +168 -0
- package/src/translations/de.js +178 -0
- package/src/translations/en.js +177 -0
- package/src/user/LoginScreen.d.vue.ts +54 -0
- package/src/user/LoginScreen.vue +627 -0
- package/src/user/google.js +75 -0
- package/src/user/index.js +3 -0
- package/src/user/store.d.ts +59 -0
- package/src/user/store.js +183 -0
- package/src/webawesomeCompat.css +72 -0
- package/src/webawesomeConfig.js +61 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { createControllerFetch, normalizeEnvelope, normalizeStructure } from "../request/index.js"
|
|
2
|
+
import Logics from "logics-js"
|
|
3
|
+
import { inject, toRaw, reactive, markRaw } from "vue"
|
|
4
|
+
import Utils from "./bones/utils"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tiefe Kopie reiner Daten (Skel-Werte), die Vue-Proxies auf JEDER Ebene
|
|
8
|
+
* auflöst. structuredClone allein scheitert daran: ein Record-/using-Formular
|
|
9
|
+
* bekommt den reaktiven Wert des Eltern-Bones, und `{ ...proxy }` behält die
|
|
10
|
+
* verschachtelten Werte als Proxies. Andere Objekte (Date, File) bleiben geteilt.
|
|
11
|
+
*/
|
|
12
|
+
function rawCopy(value) {
|
|
13
|
+
const raw = toRaw(value)
|
|
14
|
+
if (Array.isArray(raw)) return raw.map(rawCopy)
|
|
15
|
+
if (raw && typeof raw === "object" && Object.getPrototypeOf(raw) === Object.prototype) {
|
|
16
|
+
return Object.fromEntries(Object.entries(raw).map(([key, entry]) => [key, rawCopy(entry)]))
|
|
17
|
+
}
|
|
18
|
+
return raw
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useFormUtils(props, state) {
|
|
22
|
+
function buildRequestSpec(urlOverride = null) {
|
|
23
|
+
let path = urlOverride
|
|
24
|
+
|
|
25
|
+
if (!path) {
|
|
26
|
+
path = `/${props.renderer}/${props.module}/${props.action}`
|
|
27
|
+
|
|
28
|
+
const isTree = ["node", "leaf"].includes(props.skeltype)
|
|
29
|
+
|
|
30
|
+
if (props.group) {
|
|
31
|
+
path += `/${props.group}`
|
|
32
|
+
} else if (isTree) {
|
|
33
|
+
path += `/${props.skeltype}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Nur mit Key anhängen: ein SINGLETON hat genau einen Datensatz, sein
|
|
37
|
+
// `edit` kennt keinen Key. Ohne diese Prüfung entstand `/edit/undefined`.
|
|
38
|
+
if (["edit", "clone"].includes(props.action) || (isTree && props.action === "add")) {
|
|
39
|
+
if (props.skelkey) {
|
|
40
|
+
path += `/${props.skelkey}`
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parsedUrl = new URL(path, window.location.origin)
|
|
46
|
+
const segments = parsedUrl.pathname.split("/").filter(Boolean)
|
|
47
|
+
const renderer = segments[0] || props.renderer
|
|
48
|
+
const requestSegments = segments[0] === renderer ? segments.slice(1) : segments
|
|
49
|
+
|
|
50
|
+
if (requestSegments.length < 2) {
|
|
51
|
+
throw new Error(`Invalid request path: ${path}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
renderer,
|
|
56
|
+
controller: requestSegments.slice(0, -1),
|
|
57
|
+
endpoint: requestSegments.at(-1),
|
|
58
|
+
// preserve any query string from a server-provided url (e.g. a `follow`
|
|
59
|
+
// step url) so it is passed through to the request
|
|
60
|
+
query: parsedUrl.search ? parsedUrl.search.slice(1) : "",
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function _envelopeFromResponse(response) {
|
|
65
|
+
// A cloned Response bypasses the request layer's .json() normalization, so
|
|
66
|
+
// lift it to the canonical v2 shape here too.
|
|
67
|
+
try {
|
|
68
|
+
return normalizeEnvelope(await response.clone().json())
|
|
69
|
+
} catch {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Apply a (normalized v2) envelope to the form state. Central place that maps
|
|
75
|
+
// the envelope onto skel / structure / errors and the multi-step fields.
|
|
76
|
+
function applyResponseState(data, { updateStructure = false, presetValues = false } = {}) {
|
|
77
|
+
data = data ?? {}
|
|
78
|
+
|
|
79
|
+
state.datatype = data["datatype"] ?? null
|
|
80
|
+
|
|
81
|
+
if (data["datatype"] === "list") {
|
|
82
|
+
// List envelope: structure is null — don't build a form. Expose the items
|
|
83
|
+
// for custom rendering via the `list` slot instead, and never crash on the
|
|
84
|
+
// null structure a list carries.
|
|
85
|
+
const items = data["data"] !== undefined ? data["data"] : data["skellist"]
|
|
86
|
+
state.list = Array.isArray(items) ? items : []
|
|
87
|
+
state.cursor = data["meta"]?.["cursor"] ?? data["cursor"] ?? null
|
|
88
|
+
state.orders = data["meta"]?.["orders"] ?? data["orders"] ?? []
|
|
89
|
+
state.structure = {}
|
|
90
|
+
state.skel = {}
|
|
91
|
+
} else {
|
|
92
|
+
// Entity: payload lives in `data` (v1 `values` is lifted to `data` by
|
|
93
|
+
// normalizeEnvelope; accept `values` too for a raw body).
|
|
94
|
+
const values = data["data"] !== undefined ? data["data"] : data["values"]
|
|
95
|
+
if (updateStructure && data["structure"] != null) {
|
|
96
|
+
// Der `values`-Prop belegt ein frisch GERENDERTES Formular vor. Die
|
|
97
|
+
// Antwort auf ein Absenden spiegelt, was gesendet wurde — darüber darf
|
|
98
|
+
// er nicht erneut schreiben.
|
|
99
|
+
initForm(values, data["structure"], presetValues ? state.values : null)
|
|
100
|
+
// Neue Struktur = Server-Flags: visibleIf/readonlyIf/requiredIf/evaluate
|
|
101
|
+
// gleich wieder anwenden, nicht erst bei der nächsten Eingabe.
|
|
102
|
+
logics()
|
|
103
|
+
} else if (values !== undefined) {
|
|
104
|
+
state.skel = values
|
|
105
|
+
}
|
|
106
|
+
state.list = null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
state.errors = data["errors"]
|
|
110
|
+
state.actionparams = data["params"]
|
|
111
|
+
state.actionname = data["action"]
|
|
112
|
+
|
|
113
|
+
// Envelope-v2 multi-step fields. Absent on classic v1 bodies → reset to null
|
|
114
|
+
// (harmless for single-step forms). `follow` becomes the POST target for the
|
|
115
|
+
// *current* step; on a POST response it is the *next* step's URL.
|
|
116
|
+
state.step = data["step"] ?? null
|
|
117
|
+
state.stepStatus = data["step_status"] ?? null
|
|
118
|
+
state.steps = data["steps"] ?? null
|
|
119
|
+
state.status = data["status"] ?? null
|
|
120
|
+
if (data["follow"] !== undefined) {
|
|
121
|
+
state.postUrl = data["follow"]
|
|
122
|
+
}
|
|
123
|
+
state.loading = false
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// GET-render a step of a follow-driven multi-step action. Unlike fetchData
|
|
127
|
+
// (which POSTs — a submit), this renders the step's *initial* form. `url` is
|
|
128
|
+
// the envelope `follow` / `steps[key].url`; omit it to build the entry URL
|
|
129
|
+
// from props.
|
|
130
|
+
async function loadStep(url = null) {
|
|
131
|
+
state.loading = true
|
|
132
|
+
state.failed = null
|
|
133
|
+
const requestSpec = buildRequestSpec(url)
|
|
134
|
+
const request = createControllerFetch({ endpoint: requestSpec.endpoint, method: "GET" })
|
|
135
|
+
const fetchResult = request(
|
|
136
|
+
requestSpec.controller,
|
|
137
|
+
"/",
|
|
138
|
+
{},
|
|
139
|
+
{
|
|
140
|
+
renderer: requestSpec.renderer,
|
|
141
|
+
version: props.version,
|
|
142
|
+
query: requestSpec.query,
|
|
143
|
+
immediate: false,
|
|
144
|
+
messages: props.useMessageStore !== false,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
try {
|
|
148
|
+
await fetchResult.execute()
|
|
149
|
+
applyResponseState(fetchResult.data.value, { updateStructure: true, presetValues: true })
|
|
150
|
+
// Ein v1-Schritt bringt kein Ziel mit (kein `follow`, kein next_url, kein
|
|
151
|
+
// params.action_url) — dort wird der Schritt an DIESELBE URL
|
|
152
|
+
// zurückgeschickt, aus der er gerendert wurde. Ohne das stünde postUrl
|
|
153
|
+
// nach dem Rendern auf null und der Absenden-Knopf liefe ins Leere.
|
|
154
|
+
if (!state.postUrl && url) {
|
|
155
|
+
state.postUrl = url
|
|
156
|
+
}
|
|
157
|
+
return fetchResult
|
|
158
|
+
} catch (error) {
|
|
159
|
+
state.failed = error
|
|
160
|
+
state.loading = false
|
|
161
|
+
throw error
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Back one step in a multi-step flow. The target is the previous step's
|
|
166
|
+
// backend-provided endpoint URL (`steps[prevKey].url`) — no client-side
|
|
167
|
+
// history. GET-rendering it prefills from the server session. No-op on the
|
|
168
|
+
// first step or when the previous step carries no url.
|
|
169
|
+
async function back() {
|
|
170
|
+
if (!state.steps || !state.step) return
|
|
171
|
+
const keys = Object.keys(state.steps)
|
|
172
|
+
const idx = keys.indexOf(state.step)
|
|
173
|
+
if (idx <= 0) return
|
|
174
|
+
const prev = state.steps[keys[idx - 1]]
|
|
175
|
+
const url = prev && (prev.url || prev.endpoint)
|
|
176
|
+
if (!url) return
|
|
177
|
+
return loadStep(url)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function toFormData() {
|
|
181
|
+
let formdata = []
|
|
182
|
+
|
|
183
|
+
function handleEntry(result, currentFieldName, bone, val) {
|
|
184
|
+
if (bone["type"].startsWith("record")) {
|
|
185
|
+
let struct = normalizeStructure(bone["using"])
|
|
186
|
+
for (const [_fieldname, _bone] of Object.entries(struct)) {
|
|
187
|
+
result = result.concat(boneToForm(`${currentFieldName}.${_fieldname}`, _bone, val?.[_fieldname]))
|
|
188
|
+
}
|
|
189
|
+
} else if (val === Object(val) && bone["using"]) {
|
|
190
|
+
//recusive call for nested data
|
|
191
|
+
if (val["dest"]?.["key"]) {
|
|
192
|
+
let struct = normalizeStructure(bone["using"])
|
|
193
|
+
for (const [_fieldname, _bone] of Object.entries(struct)) {
|
|
194
|
+
result = result.concat(boneToForm(`${currentFieldName}.${_fieldname}`, _bone, val["rel"]?.[_fieldname]))
|
|
195
|
+
}
|
|
196
|
+
result.push({ [`${currentFieldName}.key`]: val["dest"]["key"] })
|
|
197
|
+
} else {
|
|
198
|
+
result.push({ [`${currentFieldName}`]: null })
|
|
199
|
+
}
|
|
200
|
+
} else if (bone["type"].startsWith("spatial") && val) {
|
|
201
|
+
//spatialbones
|
|
202
|
+
result.push({ [currentFieldName + ".lat"]: val[0] })
|
|
203
|
+
result.push({ [currentFieldName + ".lng"]: val[1] })
|
|
204
|
+
} else if (bone["type"].startsWith("raw.json") && val) {
|
|
205
|
+
result.push({ [currentFieldName]: JSON.stringify(val) })
|
|
206
|
+
} else if (val === Object(val)) {
|
|
207
|
+
//normal relations
|
|
208
|
+
result.push({ [currentFieldName]: val["dest"]?.["key"] || null })
|
|
209
|
+
} else {
|
|
210
|
+
//everything else
|
|
211
|
+
result.push({ [currentFieldName]: val })
|
|
212
|
+
}
|
|
213
|
+
return result
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function boneToForm(fieldname, bone, value) {
|
|
217
|
+
let result = []
|
|
218
|
+
//only record and relational bones get indexed fields
|
|
219
|
+
let indexBone = bone["type"].startsWith("record")
|
|
220
|
+
let languages = bone["languages"] || ["none"]
|
|
221
|
+
let languageValue = value
|
|
222
|
+
for (const lang of languages) {
|
|
223
|
+
let currentFieldName = fieldname
|
|
224
|
+
if (lang !== "none") {
|
|
225
|
+
currentFieldName += `.${lang}` //append lang
|
|
226
|
+
if (languageValue) value = languageValue[lang]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (bone["multiple"]) {
|
|
230
|
+
if (!value) value = []
|
|
231
|
+
for (const [idx, val] of value.entries()) {
|
|
232
|
+
let currentFieldnameMultiple = currentFieldName
|
|
233
|
+
|
|
234
|
+
if (indexBone || val?.["rel"] || (bone["using"] && val?.["rel"] !== null)) {
|
|
235
|
+
currentFieldnameMultiple = `${currentFieldName}.${idx}` //append idx
|
|
236
|
+
}
|
|
237
|
+
result = handleEntry(result, currentFieldnameMultiple, bone, val)
|
|
238
|
+
}
|
|
239
|
+
if (value.length === 0) {
|
|
240
|
+
result.push({ [currentFieldName]: null }) //send empty multiple fields
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
result = handleEntry(result, currentFieldName, bone, value)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return result
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const [fieldname, bone] of Object.entries(state.structure)) {
|
|
250
|
+
if (props.sendReadOnly) {
|
|
251
|
+
formdata.push(boneToForm(fieldname, bone, state.skel[fieldname]))
|
|
252
|
+
} else if (!state.structure[fieldname]["readonly"] || bone.type === "key") {
|
|
253
|
+
formdata.push(boneToForm(fieldname, bone, state.skel[fieldname]))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
formdata = formdata.flat(10)
|
|
258
|
+
return formdata
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function sendData(alternativUrl = null, additionalData = null, headers = null, removeKeyFromDataset = true) {
|
|
262
|
+
state.loading = true
|
|
263
|
+
// No <form> element in list mode — nothing to validate.
|
|
264
|
+
let isValid = state.viformelement ? state.viformelement.reportValidity() : true
|
|
265
|
+
if (!isValid) {
|
|
266
|
+
state.loading = false
|
|
267
|
+
return new Promise((resolve, reject) => reject("Form is not valid"))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const requestSpec = buildRequestSpec(alternativUrl)
|
|
271
|
+
const request = createControllerFetch({
|
|
272
|
+
endpoint: requestSpec.endpoint,
|
|
273
|
+
method: "POST",
|
|
274
|
+
secure: props.secure,
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const formData = new FormData()
|
|
278
|
+
for (const bone of toFormData()) {
|
|
279
|
+
for (const [k, v] of Object.entries(bone)) {
|
|
280
|
+
let val = v
|
|
281
|
+
if ([undefined, null].includes(v)) {
|
|
282
|
+
val = ""
|
|
283
|
+
}
|
|
284
|
+
formData.append(k, val)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let data = {}
|
|
289
|
+
for (const key of formData.keys()) {
|
|
290
|
+
if (key === "key" && removeKeyFromDataset) continue
|
|
291
|
+
data[[key]] = formData.getAll(key)
|
|
292
|
+
}
|
|
293
|
+
if (additionalData) {
|
|
294
|
+
data = { ...data, ...additionalData } //inject data like contexts
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const fetchResult = request(requestSpec.controller, "/", data, {
|
|
298
|
+
renderer: requestSpec.renderer,
|
|
299
|
+
version: props.version,
|
|
300
|
+
query: requestSpec.query,
|
|
301
|
+
headers,
|
|
302
|
+
immediate: false,
|
|
303
|
+
messages: props.useMessageStore !== false,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
return fetchResult
|
|
307
|
+
.execute()
|
|
308
|
+
.then(async () => {
|
|
309
|
+
// Apply the (normalized v2) envelope — updates skel / errors / action
|
|
310
|
+
// and, for multi-step flows, step / steps / status / follow. Re-init the
|
|
311
|
+
// form when the response carries a structure (e.g. the next step's form).
|
|
312
|
+
const responseData = fetchResult.data.value
|
|
313
|
+
applyResponseState(responseData, { updateStructure: responseData?.["structure"] != null })
|
|
314
|
+
return fetchResult
|
|
315
|
+
})
|
|
316
|
+
.catch(async (error) => {
|
|
317
|
+
// A rejected/failed submit may still carry a form body (errors +
|
|
318
|
+
// structure) on the error response — re-apply it so the form comes back
|
|
319
|
+
// populated. Otherwise just surface the failure.
|
|
320
|
+
const body = error?.response ? await _envelopeFromResponse(error.response).catch(() => null) : null
|
|
321
|
+
if (body) {
|
|
322
|
+
applyResponseState(body, { updateStructure: true })
|
|
323
|
+
} else {
|
|
324
|
+
state.loading = false
|
|
325
|
+
}
|
|
326
|
+
state.failed = error
|
|
327
|
+
throw error
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function fetchData(alternativUrl = null, additionalData = null, headers = null) {
|
|
332
|
+
//fetch data
|
|
333
|
+
state.loading = true
|
|
334
|
+
const requestSpec = buildRequestSpec(alternativUrl)
|
|
335
|
+
const request = createControllerFetch({
|
|
336
|
+
endpoint: requestSpec.endpoint,
|
|
337
|
+
method: "POST",
|
|
338
|
+
secure: props.secure,
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
let data = {}
|
|
342
|
+
if (additionalData) {
|
|
343
|
+
data = { ...data, ...additionalData } //inject data like contexts
|
|
344
|
+
}
|
|
345
|
+
state.failed = null
|
|
346
|
+
const fetchResult = request(requestSpec.controller, "/", data, {
|
|
347
|
+
renderer: requestSpec.renderer,
|
|
348
|
+
version: props.version,
|
|
349
|
+
query: requestSpec.query,
|
|
350
|
+
headers,
|
|
351
|
+
immediate: false,
|
|
352
|
+
messages: props.useMessageStore !== false,
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
return fetchResult
|
|
356
|
+
.execute()
|
|
357
|
+
.then(async () => {
|
|
358
|
+
// Canonical v2 envelope: entity payload is in `data`, bone definitions
|
|
359
|
+
// in `structure` (both present for v1 too after normalizeEnvelope).
|
|
360
|
+
let responseData = fetchResult.data.value
|
|
361
|
+
initForm(responseData["data"], responseData["structure"], state.values)
|
|
362
|
+
|
|
363
|
+
state.errors = responseData["errors"]
|
|
364
|
+
state.actionparams = responseData["params"]
|
|
365
|
+
state.actionname = responseData["action"]
|
|
366
|
+
state.loading = false
|
|
367
|
+
return fetchResult
|
|
368
|
+
})
|
|
369
|
+
.catch(async (error) => {
|
|
370
|
+
state.failed = error
|
|
371
|
+
throw error
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function reload() {
|
|
376
|
+
state.loading = true
|
|
377
|
+
if (props.multistep && props.module && props.action) {
|
|
378
|
+
// Follow-driven multi-step: GET-render the entry step. The submit chain
|
|
379
|
+
// (POST → follow → GET next) is driven by ViForm via loadStep/sendData.
|
|
380
|
+
loadStep()
|
|
381
|
+
.then(() => {
|
|
382
|
+
state.loading = false
|
|
383
|
+
})
|
|
384
|
+
.catch(() => {
|
|
385
|
+
state.loading = false
|
|
386
|
+
})
|
|
387
|
+
} else if (props.structure) {
|
|
388
|
+
initForm(props.skel, props.structure, state.values)
|
|
389
|
+
state.loading = false
|
|
390
|
+
} else if (props.module && props.action) {
|
|
391
|
+
fetchData(props.fetchUrl, props.params)
|
|
392
|
+
.then(async (resp) => {
|
|
393
|
+
state.loading = false
|
|
394
|
+
})
|
|
395
|
+
.catch(async (error) => {
|
|
396
|
+
state.loading = false
|
|
397
|
+
})
|
|
398
|
+
} else {
|
|
399
|
+
console.log(props)
|
|
400
|
+
console.error("Error while building Form: you need atleast module and action or structure parameters")
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function updateCategories() {
|
|
405
|
+
if (!state.structure) {
|
|
406
|
+
state.structure = {}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let catname = state.categoryDefaultname
|
|
410
|
+
if (!!catname && state.categoryDefaultname.includes("$(")) {
|
|
411
|
+
catname = Utils.stripHtml(Utils.formatString(catname, state.skel, "Allgemein"))
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let categories = { default: { name: catname, bones: [], visible: false, open: true } }
|
|
415
|
+
|
|
416
|
+
for (const [boneName, bone] of Object.entries(state.structure)) {
|
|
417
|
+
if (props.bones.length > 0) {
|
|
418
|
+
if (!props.bones.includes(boneName)) {
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let category = "default"
|
|
424
|
+
let boneStructure = state.structure[boneName]
|
|
425
|
+
|
|
426
|
+
if (bone?.params?.category) {
|
|
427
|
+
category = bone.params.category.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (Object.keys(categories).includes(category)) {
|
|
431
|
+
categories[category]["bones"].push({
|
|
432
|
+
name: boneName,
|
|
433
|
+
})
|
|
434
|
+
} else {
|
|
435
|
+
categories[category] = {
|
|
436
|
+
name: bone.params.category,
|
|
437
|
+
bones: [
|
|
438
|
+
{
|
|
439
|
+
name: boneName,
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (boneStructure["visible"] === true) {
|
|
445
|
+
categories[category]["visible"] = true
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (
|
|
449
|
+
(props.collapsedCategories &&
|
|
450
|
+
props.collapsedCategories.map((x) => x.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase()).includes(category)) ||
|
|
451
|
+
category === "system" ||
|
|
452
|
+
category === "internal" ||
|
|
453
|
+
props.collapsedCategories?.[0] === "*"
|
|
454
|
+
) {
|
|
455
|
+
categories[category]["open"] = false
|
|
456
|
+
} else {
|
|
457
|
+
categories[category]["open"] = true
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let sortedCategories = {}
|
|
462
|
+
Object.keys(categories)
|
|
463
|
+
.sort()
|
|
464
|
+
.forEach(function (key) {
|
|
465
|
+
sortedCategories[key] = categories[key]
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
return sortedCategories
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function updateSkel(data) {
|
|
472
|
+
const { name, lang, value, index, valid } = data
|
|
473
|
+
state.valids[name] = valid
|
|
474
|
+
|
|
475
|
+
let skelvalue = state.skel[name]
|
|
476
|
+
if (value === undefined) return false
|
|
477
|
+
if (state.readonly) return false
|
|
478
|
+
|
|
479
|
+
if (lang) {
|
|
480
|
+
if (!skelvalue) {
|
|
481
|
+
skelvalue = {}
|
|
482
|
+
}
|
|
483
|
+
if (Object.keys(skelvalue).includes(lang) && index !== null) {
|
|
484
|
+
if (!skelvalue[lang]) {
|
|
485
|
+
skelvalue[lang] = []
|
|
486
|
+
}
|
|
487
|
+
skelvalue[lang][index] = value
|
|
488
|
+
} else {
|
|
489
|
+
skelvalue[lang] = value
|
|
490
|
+
}
|
|
491
|
+
} else if (index !== null) {
|
|
492
|
+
if (!skelvalue) {
|
|
493
|
+
skelvalue = []
|
|
494
|
+
}
|
|
495
|
+
skelvalue[index] = value
|
|
496
|
+
} else {
|
|
497
|
+
skelvalue = value
|
|
498
|
+
}
|
|
499
|
+
state.skel[name] = skelvalue
|
|
500
|
+
logics() //postprocess all bones if needed
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function _logics(structure, skel) {
|
|
504
|
+
for (const [boneName, bone] of Object.entries(structure)) {
|
|
505
|
+
if (bone?.["params"]?.["evaluate"]) {
|
|
506
|
+
let ex = new Logics(bone?.["params"]?.["evaluate"])
|
|
507
|
+
state.skel[boneName] = ex.run(skel).toString() //rule produces, valid results? multilang, multiple etc?
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (bone?.["params"]?.["visibleIf"]) {
|
|
511
|
+
try {
|
|
512
|
+
let ex = new Logics(bone?.["params"]?.["visibleIf"])
|
|
513
|
+
bone["visible"] = ex.run(skel).toBool()
|
|
514
|
+
} catch (error) {
|
|
515
|
+
//console.log(bone?.["params"]?.["visibleIf"])
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (bone?.["params"]?.["readonlyIf"]) {
|
|
520
|
+
let ex = new Logics(bone?.["params"]?.["readonlyIf"])
|
|
521
|
+
bone["readonly"] = ex.run(skel).toBool()
|
|
522
|
+
}
|
|
523
|
+
if (bone?.["params"]?.["requiredIf"]) {
|
|
524
|
+
try {
|
|
525
|
+
let ex = new Logics(bone?.["params"]?.["requiredIf"])
|
|
526
|
+
bone["required"] = ex.run(skel).toBool()
|
|
527
|
+
} catch (error) {
|
|
528
|
+
//console.log(bone?.["params"]?.["requiredIf"])
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (bone?.["using"]) {
|
|
532
|
+
_logics(normalizeStructure(bone["using"]), skel)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function logics() {
|
|
538
|
+
let skel = { ...state.skel, _skel: state.skel }
|
|
539
|
+
|
|
540
|
+
if (props.internal) {
|
|
541
|
+
// injet mainform with _skel
|
|
542
|
+
skel = { ...skel, _skel: props.internal.skel }
|
|
543
|
+
}
|
|
544
|
+
_logics(state.structure, skel)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function initForm(skel, structure, values = {}) {
|
|
548
|
+
let skeldata = skel || {}
|
|
549
|
+
let formvalues = {}
|
|
550
|
+
if (values) {
|
|
551
|
+
formvalues = values
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (structure !== undefined) {
|
|
555
|
+
//props are refs to a js Object, removing reactivativ is not enought, we musst create copy of that object.
|
|
556
|
+
//each form has its own structure and mutating dont change the structure for other forms, this is needed for multiple records with logics
|
|
557
|
+
let struct = {}
|
|
558
|
+
for (const [k, v] of Object.entries(normalizeStructure(structure))) {
|
|
559
|
+
struct[k] = toRaw(v)
|
|
560
|
+
}
|
|
561
|
+
state.structure = structuredClone(struct)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
state.skel = { ...skeldata, ...formvalues }
|
|
565
|
+
// Stand festhalten, wie er hereinkam: `state.skel` ist die Arbeitskopie und
|
|
566
|
+
// wird beim Tippen verändert. resetForm() stellt daraus wieder her, ohne
|
|
567
|
+
// erneut zu laden ("Änderungen verwerfen").
|
|
568
|
+
// markRaw: reine Lesedaten, kein reaktiver Proxy.
|
|
569
|
+
state.pristine = markRaw(rawCopy(state.skel))
|
|
570
|
+
|
|
571
|
+
state.categories = updateCategories()
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Änderungen verwerfen: die Werte des letzten Server-Stands wieder einsetzen,
|
|
576
|
+
* OHNE neuen Request (dafür ist reload() da). Struktur und Kategorien bleiben.
|
|
577
|
+
*/
|
|
578
|
+
function resetForm() {
|
|
579
|
+
if (state.pristine === undefined || state.pristine === null) return
|
|
580
|
+
initForm(rawCopy(state.pristine), undefined, state.values)
|
|
581
|
+
state.errors = []
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
fetchData,
|
|
586
|
+
sendData,
|
|
587
|
+
loadStep,
|
|
588
|
+
resetForm,
|
|
589
|
+
back,
|
|
590
|
+
buildRequestUrl: () => {
|
|
591
|
+
const requestSpec = buildRequestSpec()
|
|
592
|
+
return `/${requestSpec.renderer}/${requestSpec.controller.join("/")}/${requestSpec.endpoint}`
|
|
593
|
+
},
|
|
594
|
+
updateCategories,
|
|
595
|
+
updateSkel,
|
|
596
|
+
normalizeStructure,
|
|
597
|
+
initForm,
|
|
598
|
+
logics,
|
|
599
|
+
reload,
|
|
600
|
+
}
|
|
601
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M176 48L64 48c-8.8 0-16 7.2-16 16l0 384c0 8.8 7.2 16 16 16l256 0c8.8 0 16-7.2 16-16l0-240-88 0c-39.8 0-72-32.2-72-72l0-88zM316.1 160L224 67.9 224 136c0 13.3 10.7 24 24 24l68.1 0zM0 64C0 28.7 28.7 0 64 0L197.5 0c17 0 33.3 6.7 45.3 18.7L365.3 141.3c12 12 18.7 28.3 18.7 45.3L384 448c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M470.6 374.6l96-96c12.5-12.5 12.5-32.8 0-45.3l-96-96c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l41.4 41.4-357.5 0 41.4-41.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-96 96c-6 6-9.4 14.1-9.4 22.6s3.4 16.6 9.4 22.6l96 96c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-41.4-41.4 357.5 0-41.4 41.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M192 96c0-53 43-96 96-96s96 43 96 96l0 3.6c0 15.7-12.7 28.4-28.4 28.4l-135.1 0c-15.7 0-28.4-12.7-28.4-28.4l0-3.6zm345.6 12.8c10.6 14.1 7.7 34.2-6.4 44.8l-97.8 73.3c5.3 8.9 9.3 18.7 11.8 29.1l98.8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-96 0 0 32c0 2.6-.1 5.3-.2 7.9l83.4 62.5c14.1 10.6 17 30.7 6.4 44.8s-30.7 17-44.8 6.4l-63.1-47.3c-23.2 44.2-66.5 76.2-117.7 83.9L312 280c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 230.2c-51.2-7.7-94.5-39.7-117.7-83.9L83.2 473.6c-14.1 10.6-34.2 7.7-44.8-6.4s-7.7-34.2 6.4-44.8l83.4-62.5c-.1-2.6-.2-5.2-.2-7.9l0-32-96 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l98.8 0c2.5-10.4 6.5-20.2 11.8-29.1L44.8 153.6c-14.1-10.6-17-30.7-6.4-44.8s30.7-17 44.8-6.4L192 184c12.3-5.1 25.8-8 40-8l112 0c14.2 0 27.7 2.8 40 8l108.8-81.6c14.1-10.6 34.2-7.7 44.8 6.4z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M434.8 70.1c14.3 10.4 17.5 30.4 7.1 44.7l-256 352c-5.5 7.6-14 12.3-23.4 13.1s-18.5-2.7-25.1-9.3l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l101.5 101.5 234-321.7c10.4-14.3 30.4-17.5 44.7-7.1z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zM224 160a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm-8 64l48 0c13.3 0 24 10.7 24 24l0 88 8 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l24 0 0-64-24 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M160 64c0-17.7-14.3-32-32-32S96 46.3 96 64l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96zM32 320c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM352 64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 320c-17.7 0-32 14.3-32 32l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-96 0z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M256 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 210.7-41.4-41.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l96 96c12.5 12.5 32.8 12.5 45.3 0l96-96c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 242.7 256 32zM64 320c-35.3 0-64 28.7-64 64l0 32c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-32c0-35.3-28.7-64-64-64l-46.9 0-56.6 56.6c-31.2 31.2-81.9 31.2-113.1 0L110.9 320 64 320zm304 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M128 40c0-22.1-17.9-40-40-40L40 0C17.9 0 0 17.9 0 40L0 88c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zm0 192c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zM0 424l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40zM320 40c0-22.1-17.9-40-40-40L232 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zM192 232l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40zM320 424c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M256 64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 160-160 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l160 0 0 160c0 17.7 14.3 32 32 32s32-14.3 32-32l0-160 160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-160 0 0-160z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M64 160c0-53 43-96 96-96s96 43 96 96c0 42.7-27.9 78.9-66.5 91.4-28.4 9.2-61.5 35.3-61.5 76.6l0 24c0 17.7 14.3 32 32 32s32-14.3 32-32l0-24c0-1.7 .6-4.1 3.5-7.3 3-3.3 7.9-6.5 13.7-8.4 64.3-20.7 110.8-81 110.8-152.3 0-88.4-71.6-160-160-160S0 71.6 0 160c0 17.7 14.3 32 32 32s32-14.3 32-32zm96 352c22.1 0 40-17.9 40-40s-17.9-40-40-40-40 17.9-40 40 17.9 40 40 40z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M256 0c14.7 0 28.2 8.1 35.2 21l216 400c6.7 12.4 6.4 27.4-.8 39.5S486.1 480 472 480L40 480c-14.1 0-27.2-7.4-34.4-19.5s-7.5-27.1-.8-39.5l216-400c7-12.9 20.5-21 35.2-21zm0 352a32 32 0 1 0 0 64 32 32 0 1 0 0-64zm0-192c-18.2 0-32.7 15.5-31.4 33.7l7.4 104c.9 12.5 11.4 22.3 23.9 22.3 12.6 0 23-9.7 23.9-22.3l7.4-104c1.3-18.2-13.1-33.7-31.4-33.7z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M256 109.3L256 320c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-210.7-41.4 41.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l96-96c12.5-12.5 32.8-12.5 45.3 0l96 96c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 109.3zM224 400c44.2 0 80-35.8 80-80l80 0c35.3 0 64 28.7 64 64l0 32c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64l0-32c0-35.3 28.7-64 64-64l80 0c0 44.2 35.8 80 80 80zm144 24a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M55.1 73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L147.2 256 9.9 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192.5 301.3 329.9 438.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.8 256 375.1 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192.5 210.7 55.1 73.4z"/></svg>
|