viur-ui 0.0.1 → 0.0.2
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 +67 -41
- package/index.d.ts +18 -9
- package/index.js +6 -3
- package/package.json +3 -5
- package/src/components/Browser/Browser.vue +58 -58
- package/src/components/Browser/TreeNav.vue +32 -32
- package/src/components/Browser/ViurBrowser.d.vue.ts +21 -21
- package/src/components/Browser/ViurBrowser.vue +47 -44
- package/src/components/Browser/dragSession.ts +27 -27
- package/src/components/Browser/index.ts +3 -3
- package/src/components/Browser/types.ts +9 -9
- package/src/components/DataTable/ColumnFilterPanel.vue +7 -7
- package/src/components/DataTable/DataTable.vue +346 -346
- package/src/components/DataTable/ViurDataTable.d.vue.ts +26 -26
- package/src/components/DataTable/ViurDataTable.vue +51 -50
- package/src/components/DataTable/columns.ts +30 -30
- package/src/components/DataTable/index.ts +12 -12
- package/src/components/DataTable/rows.ts +71 -72
- package/src/components/DataTable/types.ts +55 -55
- package/src/components/DataTable/useColumnLayout.ts +28 -28
- package/src/components/DataTable/useGridNavigation.ts +15 -15
- package/src/components/DataTable/useVirtualRows.ts +10 -10
- package/src/components/DataTable/viurColumns.ts +69 -69
- package/src/forms/ViForm.d.vue.ts +32 -32
- package/src/forms/ViForm.vue +17 -16
- package/src/forms/ViFormCategory.vue +3 -1
- package/src/forms/bones/Bone.vue +125 -108
- package/src/forms/bones/BoneActions.vue +21 -21
- package/src/forms/bones/BoneLabel.vue +1 -1
- package/src/forms/bones/BoneMultipleWrapper.vue +3 -3
- package/src/forms/bones/BoneNestedWrapper.vue +3 -3
- package/src/forms/bones/BoneSingleEntry.vue +2 -2
- package/src/forms/bones/FilterBone.vue +3 -3
- package/src/forms/bones/actions/ActionDialog.vue +2 -2
- package/src/forms/bones/actions/Debug.vue +6 -6
- package/src/forms/bones/actions/Delete.vue +2 -2
- package/src/forms/bones/actions/DescribeImage.vue +4 -3
- package/src/forms/bones/actions/Download.vue +4 -4
- package/src/forms/bones/actions/Placeholder.vue +2 -2
- package/src/forms/bones/actions/index.js +72 -74
- package/src/forms/bones/bone.d.ts +19 -19
- package/src/forms/bones/uploadAdapter.js +3 -3
- package/src/forms/bones/utils.js +40 -41
- package/src/forms/bones/view/base_item.vue +1 -1
- package/src/forms/bones/view/boneLogic.js +1 -1
- package/src/forms/bones/widgets/booleanBone.vue +19 -19
- package/src/forms/bones/widgets/booleanBoneChoose.vue +2 -2
- package/src/forms/bones/widgets/booleanBoneSelect.vue +3 -3
- package/src/forms/bones/widgets/codeBone.vue +22 -22
- package/src/forms/bones/widgets/colorBone.vue +18 -18
- package/src/forms/bones/widgets/dateBone.vue +22 -22
- package/src/forms/bones/widgets/emailBone.vue +20 -20
- package/src/forms/bones/widgets/fileBone.vue +32 -31
- package/src/forms/bones/widgets/keyBone.vue +21 -21
- package/src/forms/bones/widgets/numericBone.vue +21 -21
- package/src/forms/bones/widgets/passwordBone.vue +34 -31
- package/src/forms/bones/widgets/phoneBone.vue +20 -20
- package/src/forms/bones/widgets/rawBone.vue +19 -19
- package/src/forms/bones/widgets/recordBone.vue +16 -16
- package/src/forms/bones/widgets/relationalBone.vue +26 -26
- package/src/forms/bones/widgets/selectBone.vue +23 -23
- package/src/forms/bones/widgets/selectBoneChoose.vue +2 -2
- package/src/forms/bones/widgets/spatialBone.vue +21 -21
- package/src/forms/bones/widgets/stringBone.vue +27 -27
- package/src/forms/bones/widgets/textBone.vue +28 -26
- package/src/forms/install.d.ts +1 -1
- package/src/forms/install.js +7 -0
- package/src/forms/layouts/LayloutList2Columns.vue +1 -1
- package/src/forms/layouts/LayoutCategory.vue +1 -1
- package/src/forms/layouts/LayoutList.vue +1 -1
- package/src/forms/utils.js +22 -22
- package/src/request/configStore.js +4 -4
- package/src/request/envelope.js +6 -6
- package/src/request/request.js +22 -22
- package/src/translations/de.js +31 -0
- package/src/translations/en.js +31 -0
- package/src/user/LoginScreen.d.vue.ts +24 -24
- package/src/user/LoginScreen.vue +6 -7
- package/src/user/store.d.ts +16 -16
- package/src/vite/safeHtml.js +73 -0
- package/src/vite/sanitizeVHtml.d.ts +4 -0
- package/src/vite/sanitizeVHtml.js +15 -0
- package/src/webawesomeCompat.css +0 -40
package/src/request/envelope.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
194
|
-
// next_url –
|
|
195
|
-
// params.action_url –
|
|
196
|
-
//
|
|
197
|
-
//
|
|
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
|
|
package/src/request/request.js
CHANGED
|
@@ -126,14 +126,14 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
*
|
|
129
|
+
* Fetch the structure of the list.
|
|
130
130
|
*
|
|
131
|
-
*
|
|
132
|
-
* •
|
|
131
|
+
* Two response shapes are in circulation:
|
|
132
|
+
* • current: `/<modul>/structure` renders like any other action, i.e.
|
|
133
133
|
* `{ action: "structure.view", values, structure }`
|
|
134
|
-
* •
|
|
135
|
-
*
|
|
136
|
-
* (`json.bone.structure.keytuples`) — normalizeStructure
|
|
134
|
+
* • historic (getStructure): `{ viewSkel: {…}, editSkel: {…} }`
|
|
135
|
+
* Additionally, the structure can be a PAIR LIST
|
|
136
|
+
* (`json.bone.structure.keytuples`) — normalizeStructure flattens that.
|
|
137
137
|
*/
|
|
138
138
|
async function _fetchStructure() {
|
|
139
139
|
const skeltype =
|
|
@@ -188,9 +188,9 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
|
|
|
188
188
|
await _fetchStructure()
|
|
189
189
|
}
|
|
190
190
|
const list = responseData.data ?? []
|
|
191
|
-
//
|
|
192
|
-
// (
|
|
193
|
-
// separate
|
|
191
|
+
// Some installations deliver the structure along with the list
|
|
192
|
+
// (compatibility flag `json.bone.structure.inlists`) — then the
|
|
193
|
+
// separate call above was in vain, but this one is more current.
|
|
194
194
|
if (responseData.structure) {
|
|
195
195
|
structure.value = normalizeStructure(responseData.structure)
|
|
196
196
|
}
|
|
@@ -218,8 +218,8 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
|
|
|
218
218
|
|
|
219
219
|
const { isFetching, isFinished, error, execute: _execute, abort } = useViurFetch(listUrl, { immediate }).json()
|
|
220
220
|
|
|
221
|
-
/**
|
|
222
|
-
*
|
|
221
|
+
/** Everything loaded: last response is in and no cursor left open. From here
|
|
222
|
+
* on, sorting/filtering may run locally on the dataset. */
|
|
223
223
|
const complete = computed(() => isFinished.value && !cursor.value)
|
|
224
224
|
|
|
225
225
|
const fetchResult = {
|
|
@@ -262,8 +262,8 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
|
|
|
262
262
|
return executeAndThrow(fetchResult, _execute)
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
/**
|
|
266
|
-
*
|
|
265
|
+
/** Load one more page; detects the ViUR end (empty page with the same
|
|
266
|
+
* cursor marker) and then clears the cursor. Returns true on progress. */
|
|
267
267
|
async function _advance() {
|
|
268
268
|
if (!cursor.value) return false
|
|
269
269
|
const before = data.value.length
|
|
@@ -279,8 +279,8 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
|
|
|
279
279
|
return true
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
/**
|
|
283
|
-
*
|
|
282
|
+
/** Load the next `pages` pages (default: one) — e.g. directly as the
|
|
283
|
+
* handler of the DataTable cursor bar: `@load-more="({ pages }) =>
|
|
284
284
|
* pages === 'all' ? list.nextAll() : list.next(pages)"`. */
|
|
285
285
|
async function next(pages = 1) {
|
|
286
286
|
let remaining = pages
|
|
@@ -289,10 +289,10 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
|
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
/**
|
|
292
|
+
/** Load all remaining pages ("load everything"). */
|
|
293
293
|
async function nextAll() {
|
|
294
294
|
while (await _advance()) {
|
|
295
|
-
/*
|
|
295
|
+
/* until the end */
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
|
|
@@ -352,7 +352,7 @@ export function useTreeFetch(
|
|
|
352
352
|
const isFinished = computed(() => nodes.isFinished.value && leaves.isFinished.value)
|
|
353
353
|
const error = computed(() => nodes.error.value ?? leaves.error.value)
|
|
354
354
|
const hasMore = computed(() => !!(nodes.cursor.value || leaves.cursor.value))
|
|
355
|
-
/**
|
|
355
|
+
/** Both lists fully loaded — sorting/filtering may run locally. */
|
|
356
356
|
const complete = computed(() => isFinished.value && !hasMore.value)
|
|
357
357
|
|
|
358
358
|
function _both(fn) {
|
|
@@ -363,9 +363,9 @@ export function useTreeFetch(
|
|
|
363
363
|
return _both((l) => l.execute())
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
//
|
|
367
|
-
// (
|
|
368
|
-
//
|
|
366
|
+
// Load one more page: nodes to the end first, then leaves. useListFetch.next()
|
|
367
|
+
// detects the ViUR end (empty page with the same cursor marker) itself and
|
|
368
|
+
// then clears its cursor — only progress counts here.
|
|
369
369
|
async function _nextOf(list) {
|
|
370
370
|
const before = list.data.value.length
|
|
371
371
|
await list.next()
|
|
@@ -377,7 +377,7 @@ export function useTreeFetch(
|
|
|
377
377
|
if (leaves.cursor.value) await _nextOf(leaves)
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
/**
|
|
380
|
+
/** Load all remaining pages of both lists ("load everything"). */
|
|
381
381
|
async function nextAll() {
|
|
382
382
|
while (nodes.cursor.value) {
|
|
383
383
|
if (!(await _nextOf(nodes))) break
|
package/src/translations/de.js
CHANGED
|
@@ -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: {
|
package/src/translations/en.js
CHANGED
|
@@ -12,6 +12,7 @@ export default {
|
|
|
12
12
|
list: "List",
|
|
13
13
|
addEmpty: "New entry",
|
|
14
14
|
no_entries: "No entries",
|
|
15
|
+
errors_summary: "Errors",
|
|
15
16
|
},
|
|
16
17
|
lang: "Language",
|
|
17
18
|
bones: {
|
|
@@ -27,6 +28,13 @@ export default {
|
|
|
27
28
|
phone: {
|
|
28
29
|
placeholder: "Phone/Fax",
|
|
29
30
|
},
|
|
31
|
+
password: {
|
|
32
|
+
mismatch: "The passwords do not match.",
|
|
33
|
+
empty: "The password is empty.",
|
|
34
|
+
},
|
|
35
|
+
text: {
|
|
36
|
+
openInNewWindow: "Open link in new window",
|
|
37
|
+
},
|
|
30
38
|
},
|
|
31
39
|
|
|
32
40
|
errors: {
|
|
@@ -77,6 +85,10 @@ export default {
|
|
|
77
85
|
collapseRow: "Collapse row",
|
|
78
86
|
expandRow: "Expand row",
|
|
79
87
|
noData: "No data",
|
|
88
|
+
noValues: "No values",
|
|
89
|
+
clear: "Clear",
|
|
90
|
+
filterEquals: "exactly …",
|
|
91
|
+
filterContains: "contains …",
|
|
80
92
|
noResults: "No matches",
|
|
81
93
|
filterFrom: "from",
|
|
82
94
|
filterTo: "to",
|
|
@@ -108,6 +120,7 @@ export default {
|
|
|
108
120
|
path: "Path",
|
|
109
121
|
navigation: "Navigation",
|
|
110
122
|
uploadHint: "Drop files here",
|
|
123
|
+
name: "Name",
|
|
111
124
|
},
|
|
112
125
|
form: {
|
|
113
126
|
next: "Next",
|
|
@@ -121,6 +134,7 @@ export default {
|
|
|
121
134
|
original: "Source language ({lang}):",
|
|
122
135
|
target: "Target language ({lang}):",
|
|
123
136
|
run: "Translate",
|
|
137
|
+
name: "Translate",
|
|
124
138
|
applyNext: "Apply and go to next empty language",
|
|
125
139
|
apply: "Apply target for {lang}",
|
|
126
140
|
},
|
|
@@ -129,6 +143,23 @@ export default {
|
|
|
129
143
|
instructions: "additional instructions",
|
|
130
144
|
run: "Generate description",
|
|
131
145
|
apply: "Apply description",
|
|
146
|
+
name: "Generate image description",
|
|
147
|
+
title: "Describe image",
|
|
148
|
+
},
|
|
149
|
+
debug: {
|
|
150
|
+
name: "Debug",
|
|
151
|
+
title: "Show bone debug information",
|
|
152
|
+
structure: "Bone structure",
|
|
153
|
+
value: "Bone value",
|
|
154
|
+
state: "Bone state",
|
|
155
|
+
context: "Context",
|
|
156
|
+
},
|
|
157
|
+
placeholder: {
|
|
158
|
+
name: "Placeholder",
|
|
159
|
+
title: "Placeholder (TODO: remove)",
|
|
160
|
+
},
|
|
161
|
+
delete: {
|
|
162
|
+
name: "Delete",
|
|
132
163
|
},
|
|
133
164
|
},
|
|
134
165
|
messages: {
|
|
@@ -1,52 +1,52 @@
|
|
|
1
1
|
import type { DefineComponent } from "vue"
|
|
2
2
|
|
|
3
|
-
/** Props
|
|
3
|
+
/** Props of LoginScreen.vue (see defineProps there). */
|
|
4
4
|
export interface LoginScreenProps {
|
|
5
|
-
/**
|
|
5
|
+
/** Heading of the card; replaceable via the `header` slot. */
|
|
6
6
|
title?: string
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Endpoint below the `user` module that starts the flow. Envelope v2
|
|
9
|
+
* runs the whole chain from "login". viur-core 3.9 (classic v1) answers
|
|
10
|
+
* "login" with the provider choice; the LoginScreen then drives the chain
|
|
11
|
+
* itself like vi-vue-utils (password, lost password, second factor).
|
|
12
12
|
*/
|
|
13
13
|
action?: string
|
|
14
|
-
/** Renderer
|
|
14
|
+
/** Renderer for the requests; default: VITE_DEFAULT_RENDERER or "json". */
|
|
15
15
|
renderer?: string | null
|
|
16
|
-
/**
|
|
16
|
+
/** Pin the protocol version; null → unversioned (server picks v2). */
|
|
17
17
|
version?: string | number | null
|
|
18
18
|
/**
|
|
19
|
-
* OAuth
|
|
20
|
-
*
|
|
21
|
-
* `false`
|
|
19
|
+
* OAuth client ID for "Sign in with Google". If omitted, the ID supplied by
|
|
20
|
+
* the server via `/vi/config` ("admin.user.google.clientID") is used;
|
|
21
|
+
* `false` always hides the button.
|
|
22
22
|
*/
|
|
23
23
|
googleClientId?: string | false | null
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
* ("admin.login.background"
|
|
27
|
-
*
|
|
25
|
+
* Background image of the app, in case the configuration doesn't set one
|
|
26
|
+
* ("admin.login.background" empty). As there: absolute URLs stay, "/…" is
|
|
27
|
+
* a path on the backend, anything else an asset of the app (under BASE_URL).
|
|
28
28
|
*/
|
|
29
29
|
backgroundImage?: string | null
|
|
30
|
-
/** Logo
|
|
30
|
+
/** Logo above the login, in case "admin.login.logo" is empty (resolved as above). */
|
|
31
31
|
logo?: string | null
|
|
32
|
-
/**
|
|
32
|
+
/** Show the progress bar of the multistep form. */
|
|
33
33
|
showSteps?: boolean
|
|
34
|
-
/**
|
|
34
|
+
/** On mount, check for an existing session and skip the login. */
|
|
35
35
|
autoStart?: boolean
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* Target after a successful login (also when `autoStart` finds an existing
|
|
38
|
+
* session). Goes through vue-router if the application has one,
|
|
39
|
+
* otherwise through `location`. If omitted, the view stays put and the
|
|
40
|
+
* caller decides in `@success`.
|
|
41
41
|
*/
|
|
42
42
|
redirect?: string
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/** Events as `on*` props so that `@success` / `@error` are typed in the template. */
|
|
46
46
|
export interface LoginScreenEmitProps {
|
|
47
|
-
/**
|
|
47
|
+
/** Logged in — with the values of the UserSkel. */
|
|
48
48
|
onSuccess?: (user: Record<string, any> | null) => void
|
|
49
|
-
/** Login
|
|
49
|
+
/** Login failed (form error, transport, "google"). */
|
|
50
50
|
onError?: (error: unknown) => void
|
|
51
51
|
}
|
|
52
52
|
|
package/src/user/LoginScreen.vue
CHANGED
|
@@ -138,8 +138,7 @@
|
|
|
138
138
|
@failed="onFailed"
|
|
139
139
|
>
|
|
140
140
|
<!-- Pass the form's own slots through so an app can supply its own
|
|
141
|
-
chrome (
|
|
142
|
-
replaces it here). Only forwarded when the consumer provides the
|
|
141
|
+
chrome (e.g. its own action bar). Only forwarded when the consumer provides the
|
|
143
142
|
slot, otherwise ViForm keeps its default. -->
|
|
144
143
|
<template
|
|
145
144
|
v-if="$slots.actions"
|
|
@@ -244,8 +243,8 @@ const emit = defineEmits(["success", "error"])
|
|
|
244
243
|
const store = useUserStore()
|
|
245
244
|
const configStore = useConfigStore()
|
|
246
245
|
const { t } = useI18n()
|
|
247
|
-
// Optional:
|
|
248
|
-
// location
|
|
246
|
+
// Optional: without an installed router this is undefined — navigation then
|
|
247
|
+
// happens via location.
|
|
249
248
|
const router = useRouter()
|
|
250
249
|
|
|
251
250
|
const state = reactive({
|
|
@@ -306,9 +305,9 @@ function onFailed(error) {
|
|
|
306
305
|
}
|
|
307
306
|
|
|
308
307
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
308
|
+
* Redirect after login if `redirect` is set. Runs AFTER the success emit so
|
|
309
|
+
* the caller can still take the user into its own state (access rights,
|
|
310
|
+
* persona visibility) before the target renders.
|
|
312
311
|
*/
|
|
313
312
|
async function goRedirect() {
|
|
314
313
|
if (!props.redirect) return
|
package/src/user/store.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Values of the UserSkel as the backend delivers them. Only the fields the
|
|
3
|
+
* store itself relies on are named — the rest stays open because the skel
|
|
4
|
+
* is extended per project.
|
|
5
5
|
*/
|
|
6
6
|
export interface UserSkelValues {
|
|
7
7
|
key?: string
|
|
@@ -16,43 +16,43 @@ export interface UserSkelValues {
|
|
|
16
16
|
export type UserStatus = "unknown" | "loading" | "authenticated" | "anonymous"
|
|
17
17
|
|
|
18
18
|
export interface UserStoreState {
|
|
19
|
-
/**
|
|
19
|
+
/** Logged-in user or null. */
|
|
20
20
|
user: UserSkelValues | null
|
|
21
21
|
status: UserStatus
|
|
22
|
-
/** Renderer
|
|
22
|
+
/** Renderer for the requests (default: VITE_DEFAULT_RENDERER or "json"). */
|
|
23
23
|
renderer: string
|
|
24
24
|
googleClientId: string | null
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* The store as returned by `useUserStore()`. The getters appear as values
|
|
29
|
+
* because Pinia unwraps the refs of a setup store on the store object.
|
|
30
30
|
*/
|
|
31
31
|
export interface UserStore {
|
|
32
32
|
state: UserStoreState
|
|
33
33
|
isLoggedIn: boolean
|
|
34
|
-
/**
|
|
34
|
+
/** Access rights of the user (empty if nobody is logged in). */
|
|
35
35
|
access: string[]
|
|
36
36
|
isRoot: boolean
|
|
37
37
|
isAdmin: boolean
|
|
38
|
-
/** Root
|
|
38
|
+
/** Root always counts as authorized. */
|
|
39
39
|
hasAccess: (right: string) => boolean
|
|
40
|
-
/**
|
|
40
|
+
/** Short form of the name for UIs, "-" without a user. */
|
|
41
41
|
shortName: string
|
|
42
42
|
configure: (options?: { renderer?: string; googleClientId?: string | null }) => void
|
|
43
|
-
/**
|
|
43
|
+
/** Load the session. Yields null if nobody is logged in (401). */
|
|
44
44
|
fetchSelf: () => Promise<UserSkelValues | null>
|
|
45
45
|
logout: () => Promise<void>
|
|
46
46
|
reset: () => void
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* Classic chain (viur-core 3.9): render a step via GET — an endpoint
|
|
49
|
+
* below `user` (default "login") or a server URL. Returns the
|
|
50
|
+
* normalized response; throws on a transport error.
|
|
51
51
|
*/
|
|
52
52
|
loadLoginStep: (target?: string) => Promise<Record<string, any>>
|
|
53
|
-
/**
|
|
53
|
+
/** Choose a primary provider without a widget of its own; `next_url` is its first step. */
|
|
54
54
|
selectAuthenticationProvider: (provider: string) => Promise<Record<string, any>>
|
|
55
|
-
/** Google
|
|
55
|
+
/** Log in with a Google identity token; returns the normalized response. */
|
|
56
56
|
loginWithGoogle: (token: string) => Promise<Record<string, any> | null>
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import DOMPurify from "dompurify"
|
|
2
|
+
|
|
3
|
+
// Central, safe HTML rendering for the app.
|
|
4
|
+
//
|
|
5
|
+
// Purpose: v-html renders raw HTML and is therefore a stored-XSS sink as soon as
|
|
6
|
+
// the value comes from the backend or user input. This file provides ONE sanitize
|
|
7
|
+
// function, used in two places:
|
|
8
|
+
//
|
|
9
|
+
// 1) window.__sanitizeHtml -> called by the compiled templates.
|
|
10
|
+
// The app's Vue Vite plugin can override the built-in v-html compiler
|
|
11
|
+
// transform (in the app's vite.config.js) so that EVERY v-html runs through
|
|
12
|
+
// it automatically. Existing `v-html` usages become safe without code changes.
|
|
13
|
+
// 2) the `v-safe-html` directive as an explicit, easy-to-spot alias.
|
|
14
|
+
//
|
|
15
|
+
// DOMPurify is a fail-closed allowlist: formatting tags (h1-h6, br, p, strong, em,
|
|
16
|
+
// ul/ol/li, a[href], table, ...) are kept and stay functional; <script>, event
|
|
17
|
+
// handlers (onerror/onclick/...) and javascript: URLs are removed.
|
|
18
|
+
//
|
|
19
|
+
// IMPORTANT: do NOT run values through helpers that decode HTML entities (e.g. a
|
|
20
|
+
// `cleanString()`) beforehand — that turns neutralized payloads live again. Plain
|
|
21
|
+
// text output needs no HTML at all -> use {{ text }} / v-text.
|
|
22
|
+
|
|
23
|
+
// Protect links that open in a new tab against reverse tabnabbing.
|
|
24
|
+
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
|
|
25
|
+
if (node.tagName === "A" && node.getAttribute("target") === "_blank") {
|
|
26
|
+
node.setAttribute("rel", "noopener noreferrer")
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }
|
|
31
|
+
|
|
32
|
+
export function sanitizeHtml(value) {
|
|
33
|
+
const html = value == null ? "" : String(value)
|
|
34
|
+
// DOMPurify returns its input UNCHANGED where it can't run (no DOM, e.g. SSR or
|
|
35
|
+
// some test environments). Stay fail-closed there: escape instead of passing through.
|
|
36
|
+
if (!DOMPurify.isSupported) return html.replace(/[&<>"']/g, (c) => ESCAPES[c])
|
|
37
|
+
// `target` is stripped by default; keep it so the hook above can secure new-tab links
|
|
38
|
+
return DOMPurify.sanitize(html, { ADD_ATTR: ["target"] })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// For the compiled templates (v-html compiler override in the app's vite.config.js).
|
|
42
|
+
// Set when this module is imported — i.e. before app.mount().
|
|
43
|
+
if (typeof window !== "undefined") {
|
|
44
|
+
window.__sanitizeHtml = sanitizeHtml
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function render(el, value) {
|
|
48
|
+
el.innerHTML = sanitizeHtml(value)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Directive object — also importable for local registration (`<script setup>`:
|
|
52
|
+
// `import { vSafeHtml } from ".../safeHtml.js"` makes `v-safe-html` available
|
|
53
|
+
// without a global plugin).
|
|
54
|
+
export const vSafeHtml = {
|
|
55
|
+
mounted(el, binding) {
|
|
56
|
+
render(el, binding.value)
|
|
57
|
+
},
|
|
58
|
+
updated(el, binding) {
|
|
59
|
+
if (binding.value !== binding.oldValue) {
|
|
60
|
+
render(el, binding.value)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default {
|
|
66
|
+
install: (app) => {
|
|
67
|
+
// Also available globally as an instance property (e.g. for render functions).
|
|
68
|
+
app.config.globalProperties.$sanitizeHtml = sanitizeHtml
|
|
69
|
+
|
|
70
|
+
// Explicit alias for v-html.
|
|
71
|
+
app.directive("safe-html", vSafeHtml)
|
|
72
|
+
},
|
|
73
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DOMDirectiveTransforms, createCompoundExpression } from "@vue/compiler-dom"
|
|
2
|
+
|
|
3
|
+
// Compile-time v-html override for the app's vite.config.js: every v-html is routed
|
|
4
|
+
// through window.__sanitizeHtml (DOMPurify, set by src/vite/safeHtml.js /
|
|
5
|
+
// installViurForms). Wraps Vue's own transform, so its dev warnings and child
|
|
6
|
+
// handling stay intact.
|
|
7
|
+
//
|
|
8
|
+
// vue({ template: { compilerOptions: { directiveTransforms: { html: sanitizeVHtml } } } })
|
|
9
|
+
export function sanitizeVHtml(dir, node, context) {
|
|
10
|
+
const result = DOMDirectiveTransforms.html(dir, node, context)
|
|
11
|
+
if (dir.exp) {
|
|
12
|
+
result.props[0].value = createCompoundExpression(["window.__sanitizeHtml(", dir.exp, ")"])
|
|
13
|
+
}
|
|
14
|
+
return result
|
|
15
|
+
}
|
package/src/webawesomeCompat.css
CHANGED
|
@@ -1,43 +1,3 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
--sl-spacing-x-small: var(--wa-space-xs);
|
|
3
|
-
--sl-spacing-small: var(--wa-space-s);
|
|
4
|
-
--sl-spacing-medium: var(--wa-space-m);
|
|
5
|
-
--sl-spacing-large: var(--wa-space-l);
|
|
6
|
-
|
|
7
|
-
--sl-border-radius-medium: var(--wa-border-radius-m);
|
|
8
|
-
--sl-input-border-radius-medium: var(--wa-form-control-border-radius);
|
|
9
|
-
--sl-input-height-medium: var(--wa-form-control-height);
|
|
10
|
-
--sl-input-font-size-medium: var(--wa-font-size-m);
|
|
11
|
-
|
|
12
|
-
--sl-color-neutral-0: var(--wa-color-surface-default);
|
|
13
|
-
--sl-color-neutral-50: var(--wa-color-neutral-95);
|
|
14
|
-
--sl-color-neutral-100: var(--wa-color-neutral-90);
|
|
15
|
-
--sl-color-neutral-200: var(--wa-color-neutral-fill-normal);
|
|
16
|
-
--sl-color-neutral-300: var(--wa-color-surface-border);
|
|
17
|
-
--sl-color-neutral-400: var(--wa-color-neutral-border-normal);
|
|
18
|
-
--sl-color-neutral-500: var(--wa-color-neutral-60);
|
|
19
|
-
--sl-color-neutral-600: var(--wa-color-neutral-50);
|
|
20
|
-
--sl-color-neutral-700: var(--wa-color-neutral-40);
|
|
21
|
-
--sl-color-neutral-800: var(--wa-color-neutral-20);
|
|
22
|
-
--sl-color-neutral-900: var(--wa-color-text-normal);
|
|
23
|
-
|
|
24
|
-
--sl-color-gray-200: var(--wa-color-neutral-95);
|
|
25
|
-
--sl-color-gray-400: var(--wa-color-neutral-60);
|
|
26
|
-
--sl-color-gray-500: var(--wa-color-neutral-border-normal);
|
|
27
|
-
|
|
28
|
-
--sl-color-primary-500: var(--wa-color-brand-50);
|
|
29
|
-
--sl-color-primary-600: var(--wa-color-brand-60);
|
|
30
|
-
--sl-color-success-500: var(--wa-color-success-50);
|
|
31
|
-
--sl-color-success-600: var(--wa-color-success-60);
|
|
32
|
-
--sl-color-danger-500: var(--wa-color-danger-50);
|
|
33
|
-
--sl-color-danger-700: var(--wa-color-danger-70);
|
|
34
|
-
|
|
35
|
-
--sl-color-info-50: var(--wa-color-brand-95);
|
|
36
|
-
--sl-color-info-300: var(--wa-color-brand-90);
|
|
37
|
-
--sl-color-info-500: var(--wa-color-brand-60);
|
|
38
|
-
--sl-color-info-900: var(--wa-color-brand-30);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
1
|
wa-input,
|
|
42
2
|
wa-textarea,
|
|
43
3
|
wa-select,
|