@jskit-ai/users-web 0.1.10 → 0.1.12
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/package.descriptor.mjs +7 -3
- package/package.json +2 -2
- package/src/client/components/MembersAdminClientElement.vue +2 -7
- package/src/client/components/UsersWorkspaceSelector.vue +12 -1
- package/src/client/components/WorkspaceMembersClientElement.vue +7 -12
- package/src/client/components/WorkspaceSettingsClientElement.vue +103 -23
- package/src/client/composables/modelStateHelpers.js +42 -1
- package/src/client/composables/useAddEditCore.js +9 -30
- package/src/client/composables/useViewCore.js +10 -31
- package/src/client/lib/bootstrap.js +3 -0
- package/src/client/lib/menuIcons.js +1 -4
- package/src/client/lib/theme.js +165 -1
- package/src/client/runtime/bootstrapPlacementRouteGuards.js +10 -10
- package/src/client/runtime/bootstrapPlacementRuntime.js +58 -0
- package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +4 -14
- package/src/client/support/realtimeWorkspace.js +10 -1
- package/test/bootstrapPlacementRuntime.test.js +78 -1
- package/test/theme.test.js +73 -14
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/users-web",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.12",
|
|
5
5
|
description: "Users web module: workspace selector shell element plus workspace/profile/members UI elements.",
|
|
6
6
|
dependsOn: [
|
|
7
7
|
"@jskit-ai/http-runtime",
|
|
@@ -234,7 +234,7 @@ export default Object.freeze({
|
|
|
234
234
|
mutations: {
|
|
235
235
|
dependencies: {
|
|
236
236
|
runtime: {
|
|
237
|
-
"@tanstack/vue-query": "
|
|
237
|
+
"@tanstack/vue-query": "5.92.12",
|
|
238
238
|
"@mdi/js": "^7.4.47",
|
|
239
239
|
"@uppy/compressor": "^3.1.0",
|
|
240
240
|
"@uppy/core": "^5.2.0",
|
|
@@ -251,7 +251,11 @@ export default Object.freeze({
|
|
|
251
251
|
dev: {}
|
|
252
252
|
},
|
|
253
253
|
packageJson: {
|
|
254
|
-
scripts: {
|
|
254
|
+
scripts: {
|
|
255
|
+
"server:account": "SERVER_SURFACE=account node ./bin/server.js",
|
|
256
|
+
"dev:account": "VITE_SURFACE=account vite",
|
|
257
|
+
"build:account": "VITE_SURFACE=account vite build"
|
|
258
|
+
}
|
|
255
259
|
},
|
|
256
260
|
procfile: {},
|
|
257
261
|
files: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"./client/support/realtimeWorkspace": "./src/client/support/realtimeWorkspace.js"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@tanstack/vue-query": "
|
|
22
|
+
"@tanstack/vue-query": "5.92.12",
|
|
23
23
|
"@mdi/js": "^7.4.47",
|
|
24
24
|
"@jskit-ai/users-core": "0.1.10",
|
|
25
25
|
"@jskit-ai/realtime": "0.1.8",
|
|
@@ -167,6 +167,7 @@
|
|
|
167
167
|
|
|
168
168
|
<script setup>
|
|
169
169
|
import { computed, toRefs, unref } from "vue";
|
|
170
|
+
import { formatDateTime as formatKernelDateTime } from "@jskit-ai/kernel/shared/support";
|
|
170
171
|
import { requireBoolean, requireFunction, requireRecord } from "../support/contractGuards.js";
|
|
171
172
|
|
|
172
173
|
const props = defineProps({
|
|
@@ -327,13 +328,7 @@ function formatDateTime(value) {
|
|
|
327
328
|
if (typeof options.value.formatDateTime === "function") {
|
|
328
329
|
return options.value.formatDateTime(value);
|
|
329
330
|
}
|
|
330
|
-
|
|
331
|
-
const date = new Date(value);
|
|
332
|
-
if (Number.isNaN(date.getTime())) {
|
|
333
|
-
return "unknown";
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return date.toLocaleString();
|
|
331
|
+
return formatKernelDateTime(value);
|
|
337
332
|
}
|
|
338
333
|
|
|
339
334
|
function showOwnerChip(member) {
|
|
@@ -191,6 +191,17 @@ const activeWorkspaceLabel = computed(() => {
|
|
|
191
191
|
return "Workspace";
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
function workspaceAvatarStyle(workspace) {
|
|
195
|
+
const color = String(workspace?.color || "").trim();
|
|
196
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(color)) {
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
backgroundColor: color
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
194
205
|
</script>
|
|
195
206
|
|
|
196
207
|
<template>
|
|
@@ -218,7 +229,7 @@ const activeWorkspaceLabel = computed(() => {
|
|
|
218
229
|
@click="navigateToWorkspace(workspace.slug)"
|
|
219
230
|
>
|
|
220
231
|
<template #prepend>
|
|
221
|
-
<v-avatar size="24" color="primary" variant="tonal">
|
|
232
|
+
<v-avatar size="24" color="primary" variant="tonal" :style="workspaceAvatarStyle(workspace)">
|
|
222
233
|
<span class="text-caption">{{ String(workspace.name || "W").slice(0, 1).toUpperCase() }}</span>
|
|
223
234
|
</v-avatar>
|
|
224
235
|
</template>
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
<script setup>
|
|
22
22
|
import { computed, reactive, ref, watch } from "vue";
|
|
23
|
+
import { formatDateTime } from "@jskit-ai/kernel/shared/support";
|
|
23
24
|
import MembersAdminClientElement from "./MembersAdminClientElement.vue";
|
|
24
25
|
import { useCommand } from "../composables/useCommand.js";
|
|
25
26
|
import { useList } from "../composables/useList.js";
|
|
@@ -29,7 +30,7 @@ import { useAccess } from "../composables/useAccess.js";
|
|
|
29
30
|
import { useUiFeedback } from "../composables/useUiFeedback.js";
|
|
30
31
|
import { useWorkspaceRouteContext } from "../composables/useWorkspaceRouteContext.js";
|
|
31
32
|
import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
|
|
32
|
-
import {
|
|
33
|
+
import { createWorkspaceRealtimeMatcher } from "../support/realtimeWorkspace.js";
|
|
33
34
|
import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
|
|
34
35
|
import {
|
|
35
36
|
WORKSPACE_SETTINGS_CHANGED_EVENT,
|
|
@@ -53,11 +54,7 @@ const options = reactive({
|
|
|
53
54
|
inviteRoleOptions: [],
|
|
54
55
|
memberRoleOptions: [],
|
|
55
56
|
formatDateTime(value) {
|
|
56
|
-
|
|
57
|
-
if (Number.isNaN(parsedDate.getTime())) {
|
|
58
|
-
return "unknown";
|
|
59
|
-
}
|
|
60
|
-
return parsedDate.toLocaleString();
|
|
57
|
+
return formatDateTime(value);
|
|
61
58
|
}
|
|
62
59
|
});
|
|
63
60
|
|
|
@@ -106,9 +103,7 @@ const access = useAccess({
|
|
|
106
103
|
placementSource: "users-web.workspace-members-view"
|
|
107
104
|
});
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
return matchesCurrentWorkspaceEvent(payload, workspaceSlugFromRoute.value);
|
|
111
|
-
}
|
|
106
|
+
const matchesWorkspaceRealtime = createWorkspaceRealtimeMatcher(workspaceSlugFromRoute);
|
|
112
107
|
|
|
113
108
|
const canViewMembers = computed(() => {
|
|
114
109
|
return access.canAny(["workspace.members.view", "workspace.members.manage"]);
|
|
@@ -280,7 +275,7 @@ const workspaceSettingsView = useView({
|
|
|
280
275
|
viewPermissions: ["workspace.members.invite"],
|
|
281
276
|
realtime: {
|
|
282
277
|
event: WORKSPACE_SETTINGS_CHANGED_EVENT,
|
|
283
|
-
matches:
|
|
278
|
+
matches: matchesWorkspaceRealtime
|
|
284
279
|
},
|
|
285
280
|
fallbackLoadError: "Unable to load workspace settings."
|
|
286
281
|
});
|
|
@@ -301,7 +296,7 @@ const workspaceMembersList = useList({
|
|
|
301
296
|
viewPermissions: ["workspace.members.view", "workspace.members.manage"],
|
|
302
297
|
realtime: {
|
|
303
298
|
event: WORKSPACE_MEMBERS_CHANGED_EVENT,
|
|
304
|
-
matches:
|
|
299
|
+
matches: matchesWorkspaceRealtime
|
|
305
300
|
},
|
|
306
301
|
selectItems: (payload) => normalizeMembers(payload?.members),
|
|
307
302
|
fallbackLoadError: "Unable to load workspace members."
|
|
@@ -315,7 +310,7 @@ const workspaceInvitesList = useList({
|
|
|
315
310
|
viewPermissions: ["workspace.members.view", "workspace.members.manage"],
|
|
316
311
|
realtime: {
|
|
317
312
|
event: WORKSPACE_INVITES_CHANGED_EVENT,
|
|
318
|
-
matches:
|
|
313
|
+
matches: matchesWorkspaceRealtime
|
|
319
314
|
},
|
|
320
315
|
selectItems: (payload) => normalizeInvites(payload?.invites),
|
|
321
316
|
fallbackLoadError: "Unable to load workspace invites."
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<v-form @submit.prevent="addEdit.submit" novalidate>
|
|
24
24
|
<v-progress-linear v-if="addEdit.isRefetching" indeterminate class="mb-4" />
|
|
25
25
|
<v-row>
|
|
26
|
-
<v-col cols="12" md="
|
|
26
|
+
<v-col cols="12" md="6">
|
|
27
27
|
<v-text-field
|
|
28
28
|
v-model="workspaceForm.name"
|
|
29
29
|
label="Workspace name"
|
|
@@ -34,19 +34,7 @@
|
|
|
34
34
|
/>
|
|
35
35
|
</v-col>
|
|
36
36
|
|
|
37
|
-
<v-col cols="12" md="
|
|
38
|
-
<v-text-field
|
|
39
|
-
v-model="workspaceForm.color"
|
|
40
|
-
label="Workspace color"
|
|
41
|
-
type="color"
|
|
42
|
-
variant="outlined"
|
|
43
|
-
density="comfortable"
|
|
44
|
-
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
45
|
-
:error-messages="addEdit.fieldErrors.color ? [addEdit.fieldErrors.color] : []"
|
|
46
|
-
/>
|
|
47
|
-
</v-col>
|
|
48
|
-
|
|
49
|
-
<v-col cols="12" md="5">
|
|
37
|
+
<v-col cols="12" md="6">
|
|
50
38
|
<v-text-field
|
|
51
39
|
v-model="workspaceForm.avatarUrl"
|
|
52
40
|
label="Workspace avatar URL"
|
|
@@ -60,6 +48,61 @@
|
|
|
60
48
|
/>
|
|
61
49
|
</v-col>
|
|
62
50
|
|
|
51
|
+
<v-col cols="12">
|
|
52
|
+
<div class="text-subtitle-2 mb-2">Theme colors</div>
|
|
53
|
+
<v-row dense>
|
|
54
|
+
<v-col cols="12" md="3">
|
|
55
|
+
<v-text-field
|
|
56
|
+
v-model="workspaceForm.color"
|
|
57
|
+
label="Primary"
|
|
58
|
+
type="color"
|
|
59
|
+
variant="outlined"
|
|
60
|
+
density="comfortable"
|
|
61
|
+
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
62
|
+
:error-messages="addEdit.fieldErrors.color ? [addEdit.fieldErrors.color] : []"
|
|
63
|
+
/>
|
|
64
|
+
</v-col>
|
|
65
|
+
|
|
66
|
+
<v-col cols="12" md="3">
|
|
67
|
+
<v-text-field
|
|
68
|
+
v-model="workspaceForm.secondaryColor"
|
|
69
|
+
label="Secondary"
|
|
70
|
+
type="color"
|
|
71
|
+
variant="outlined"
|
|
72
|
+
density="comfortable"
|
|
73
|
+
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
74
|
+
:error-messages="addEdit.fieldErrors.secondaryColor ? [addEdit.fieldErrors.secondaryColor] : []"
|
|
75
|
+
/>
|
|
76
|
+
</v-col>
|
|
77
|
+
|
|
78
|
+
<v-col cols="12" md="3">
|
|
79
|
+
<v-text-field
|
|
80
|
+
v-model="workspaceForm.surfaceColor"
|
|
81
|
+
label="Surface"
|
|
82
|
+
type="color"
|
|
83
|
+
variant="outlined"
|
|
84
|
+
density="comfortable"
|
|
85
|
+
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
86
|
+
:error-messages="addEdit.fieldErrors.surfaceColor ? [addEdit.fieldErrors.surfaceColor] : []"
|
|
87
|
+
/>
|
|
88
|
+
</v-col>
|
|
89
|
+
|
|
90
|
+
<v-col cols="12" md="3">
|
|
91
|
+
<v-text-field
|
|
92
|
+
v-model="workspaceForm.surfaceVariantColor"
|
|
93
|
+
label="Surface variant"
|
|
94
|
+
type="color"
|
|
95
|
+
variant="outlined"
|
|
96
|
+
density="comfortable"
|
|
97
|
+
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
98
|
+
:error-messages="
|
|
99
|
+
addEdit.fieldErrors.surfaceVariantColor ? [addEdit.fieldErrors.surfaceVariantColor] : []
|
|
100
|
+
"
|
|
101
|
+
/>
|
|
102
|
+
</v-col>
|
|
103
|
+
</v-row>
|
|
104
|
+
</v-col>
|
|
105
|
+
|
|
63
106
|
<v-col cols="12" md="6" class="d-flex align-center">
|
|
64
107
|
<v-switch
|
|
65
108
|
v-model="workspaceForm.invitesEnabled"
|
|
@@ -96,20 +139,29 @@ import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validato
|
|
|
96
139
|
import { workspaceSettingsResource } from "@jskit-ai/users-core/shared/resources/workspaceSettingsResource";
|
|
97
140
|
import { WORKSPACE_SETTINGS_CHANGED_EVENT } from "@jskit-ai/users-core/shared/events/usersEvents";
|
|
98
141
|
import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
|
|
142
|
+
import {
|
|
143
|
+
DEFAULT_WORKSPACE_COLOR,
|
|
144
|
+
resolveWorkspaceThemePalette
|
|
145
|
+
} from "@jskit-ai/users-core/shared/settings";
|
|
99
146
|
import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
100
147
|
import { useAddEdit } from "../composables/useAddEdit.js";
|
|
101
148
|
import { useBootstrapQuery } from "../composables/useBootstrapQuery.js";
|
|
102
149
|
import { useWorkspaceRouteContext } from "../composables/useWorkspaceRouteContext.js";
|
|
103
150
|
import { findWorkspaceBySlug, normalizeWorkspaceList } from "../lib/bootstrap.js";
|
|
104
151
|
import { arePermissionListsEqual, normalizePermissionList } from "../lib/permissions.js";
|
|
105
|
-
import {
|
|
152
|
+
import { createWorkspaceRealtimeMatcher } from "../support/realtimeWorkspace.js";
|
|
106
153
|
import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
|
|
107
154
|
|
|
108
|
-
const
|
|
155
|
+
const DEFAULT_WORKSPACE_THEME = resolveWorkspaceThemePalette({
|
|
156
|
+
color: DEFAULT_WORKSPACE_COLOR
|
|
157
|
+
});
|
|
109
158
|
|
|
110
159
|
const workspaceForm = reactive({
|
|
111
160
|
name: "",
|
|
112
161
|
color: DEFAULT_WORKSPACE_COLOR,
|
|
162
|
+
secondaryColor: DEFAULT_WORKSPACE_THEME.secondaryColor,
|
|
163
|
+
surfaceColor: DEFAULT_WORKSPACE_THEME.surfaceColor,
|
|
164
|
+
surfaceVariantColor: DEFAULT_WORKSPACE_THEME.surfaceVariantColor,
|
|
113
165
|
avatarUrl: "",
|
|
114
166
|
invitesEnabled: false,
|
|
115
167
|
invitesAvailable: false
|
|
@@ -138,6 +190,22 @@ function toWorkspaceListSnapshot(list = []) {
|
|
|
138
190
|
return JSON.stringify(normalizeWorkspaceList(list));
|
|
139
191
|
}
|
|
140
192
|
|
|
193
|
+
function toWorkspaceSettingsSnapshot(settings = null) {
|
|
194
|
+
if (!settings || typeof settings !== "object") {
|
|
195
|
+
return "";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const normalized = resolveWorkspaceThemePalette(settings);
|
|
199
|
+
return JSON.stringify({
|
|
200
|
+
color: normalized.color,
|
|
201
|
+
secondaryColor: normalized.secondaryColor,
|
|
202
|
+
surfaceColor: normalized.surfaceColor,
|
|
203
|
+
surfaceVariantColor: normalized.surfaceVariantColor,
|
|
204
|
+
invitesEnabled: settings.invitesEnabled !== false,
|
|
205
|
+
invitesAvailable: settings.invitesAvailable !== false
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
141
209
|
function applyShellWorkspaceContext(payload = {}) {
|
|
142
210
|
const availableWorkspaces = normalizeWorkspaceList(payload?.workspaces);
|
|
143
211
|
const currentWorkspace = findWorkspaceBySlug(
|
|
@@ -154,14 +222,21 @@ function applyShellWorkspaceContext(payload = {}) {
|
|
|
154
222
|
const sameWorkspace = toWorkspaceEntrySnapshot(currentContext.workspace) === toWorkspaceEntrySnapshot(currentWorkspace);
|
|
155
223
|
const sameWorkspaces =
|
|
156
224
|
toWorkspaceListSnapshot(currentContext.workspaces) === toWorkspaceListSnapshot(availableWorkspaces);
|
|
225
|
+
const sameWorkspaceSettings =
|
|
226
|
+
toWorkspaceSettingsSnapshot(currentContext.workspaceSettings) ===
|
|
227
|
+
toWorkspaceSettingsSnapshot(payload?.workspaceSettings);
|
|
157
228
|
|
|
158
|
-
if (samePermissions && sameWorkspace && sameWorkspaces) {
|
|
229
|
+
if (samePermissions && sameWorkspace && sameWorkspaces && sameWorkspaceSettings) {
|
|
159
230
|
return;
|
|
160
231
|
}
|
|
161
232
|
|
|
162
233
|
mergePlacementContext(
|
|
163
234
|
{
|
|
164
235
|
workspace: currentWorkspace,
|
|
236
|
+
workspaceSettings:
|
|
237
|
+
payload?.workspaceSettings && typeof payload.workspaceSettings === "object"
|
|
238
|
+
? payload.workspaceSettings
|
|
239
|
+
: null,
|
|
165
240
|
workspaces: availableWorkspaces,
|
|
166
241
|
permissions
|
|
167
242
|
},
|
|
@@ -180,9 +255,7 @@ watch(
|
|
|
180
255
|
{ immediate: true }
|
|
181
256
|
);
|
|
182
257
|
|
|
183
|
-
|
|
184
|
-
return matchesCurrentWorkspaceEvent(payload, routeContext.workspaceSlugFromRoute.value);
|
|
185
|
-
}
|
|
258
|
+
const matchesWorkspaceRealtime = createWorkspaceRealtimeMatcher(routeContext.workspaceSlugFromRoute);
|
|
186
259
|
|
|
187
260
|
const addEdit = useAddEdit({
|
|
188
261
|
ownershipFilter: OWNERSHIP_WORKSPACE,
|
|
@@ -194,10 +267,10 @@ const addEdit = useAddEdit({
|
|
|
194
267
|
savePermissions: ["workspace.settings.update"],
|
|
195
268
|
placementSource: "users-web.workspace-settings-view",
|
|
196
269
|
fallbackLoadError: "Unable to load workspace settings.",
|
|
197
|
-
fieldErrorKeys: ["name", "avatarUrl", "color"],
|
|
270
|
+
fieldErrorKeys: ["name", "avatarUrl", "color", "secondaryColor", "surfaceColor", "surfaceVariantColor"],
|
|
198
271
|
realtime: {
|
|
199
272
|
event: WORKSPACE_SETTINGS_CHANGED_EVENT,
|
|
200
|
-
matches:
|
|
273
|
+
matches: matchesWorkspaceRealtime
|
|
201
274
|
},
|
|
202
275
|
model: workspaceForm,
|
|
203
276
|
parseInput: (rawPayload) =>
|
|
@@ -208,9 +281,13 @@ const addEdit = useAddEdit({
|
|
|
208
281
|
}),
|
|
209
282
|
mapLoadedToModel: (model, payload = {}) => {
|
|
210
283
|
const settings = payload?.settings && typeof payload.settings === "object" ? payload.settings : {};
|
|
284
|
+
const normalizedTheme = resolveWorkspaceThemePalette(settings);
|
|
211
285
|
|
|
212
286
|
model.name = String(settings.name || "");
|
|
213
|
-
model.color =
|
|
287
|
+
model.color = normalizedTheme.color;
|
|
288
|
+
model.secondaryColor = normalizedTheme.secondaryColor;
|
|
289
|
+
model.surfaceColor = normalizedTheme.surfaceColor;
|
|
290
|
+
model.surfaceVariantColor = normalizedTheme.surfaceVariantColor;
|
|
214
291
|
model.avatarUrl = String(settings.avatarUrl || "");
|
|
215
292
|
model.invitesEnabled = settings.invitesEnabled !== false;
|
|
216
293
|
model.invitesAvailable = settings.invitesAvailable !== false;
|
|
@@ -218,6 +295,9 @@ const addEdit = useAddEdit({
|
|
|
218
295
|
buildRawPayload: (model) => ({
|
|
219
296
|
name: model.name,
|
|
220
297
|
color: model.color,
|
|
298
|
+
secondaryColor: model.secondaryColor,
|
|
299
|
+
surfaceColor: model.surfaceColor,
|
|
300
|
+
surfaceVariantColor: model.surfaceVariantColor,
|
|
221
301
|
avatarUrl: model.avatarUrl,
|
|
222
302
|
invitesEnabled: model.invitesEnabled
|
|
223
303
|
}),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { watch } from "vue";
|
|
2
|
+
|
|
1
3
|
function isObjectLike(value) {
|
|
2
4
|
return value !== null && typeof value === "object";
|
|
3
5
|
}
|
|
@@ -43,7 +45,46 @@ function restoreModelSnapshot(model, snapshot) {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function watchResourceModelState({
|
|
49
|
+
resource,
|
|
50
|
+
model,
|
|
51
|
+
mapLoadedToModel,
|
|
52
|
+
resolveMapContext = null
|
|
53
|
+
} = {}) {
|
|
54
|
+
const modelSnapshot = captureModelSnapshot(model);
|
|
55
|
+
|
|
56
|
+
watch(
|
|
57
|
+
() => resource?.query?.isPending?.value,
|
|
58
|
+
(isPending) => {
|
|
59
|
+
if (!isPending || !modelSnapshot) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
restoreModelSnapshot(model, modelSnapshot);
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
immediate: true
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
watch(
|
|
71
|
+
() => resource?.data?.value,
|
|
72
|
+
(payload) => {
|
|
73
|
+
if (!payload || typeof mapLoadedToModel !== "function") {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const context = typeof resolveMapContext === "function" ? resolveMapContext() : {};
|
|
78
|
+
mapLoadedToModel(model, payload, context);
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
immediate: true
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
46
86
|
export {
|
|
47
87
|
captureModelSnapshot,
|
|
48
|
-
restoreModelSnapshot
|
|
88
|
+
restoreModelSnapshot,
|
|
89
|
+
watchResourceModelState
|
|
49
90
|
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { watch } from "vue";
|
|
2
1
|
import { useQueryClient } from "@tanstack/vue-query";
|
|
3
2
|
import { resolveFieldErrors } from "@jskit-ai/http-runtime/client";
|
|
4
3
|
import { validateOperationInput } from "./operationValidationHelpers.js";
|
|
5
|
-
import {
|
|
4
|
+
import { watchResourceModelState } from "./modelStateHelpers.js";
|
|
6
5
|
|
|
7
6
|
function useAddEditCore({
|
|
8
7
|
model,
|
|
@@ -19,37 +18,17 @@ function useAddEditCore({
|
|
|
19
18
|
messages = {}
|
|
20
19
|
} = {}) {
|
|
21
20
|
const queryClient = useQueryClient();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
(
|
|
27
|
-
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
restoreModelSnapshot(model, modelSnapshot);
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
immediate: true
|
|
35
|
-
}
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
watch(
|
|
39
|
-
() => resource?.data?.value,
|
|
40
|
-
(payload) => {
|
|
41
|
-
if (!payload || typeof mapLoadedToModel !== "function") {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
mapLoadedToModel(model, payload, {
|
|
21
|
+
watchResourceModelState({
|
|
22
|
+
resource,
|
|
23
|
+
model,
|
|
24
|
+
mapLoadedToModel,
|
|
25
|
+
resolveMapContext() {
|
|
26
|
+
return {
|
|
45
27
|
queryClient,
|
|
46
28
|
resource
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
immediate: true
|
|
29
|
+
};
|
|
51
30
|
}
|
|
52
|
-
);
|
|
31
|
+
});
|
|
53
32
|
|
|
54
33
|
const saving = resource?.isSaving;
|
|
55
34
|
const fieldErrors = fieldBag?.errors;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { computed
|
|
2
|
-
import {
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { watchResourceModelState } from "./modelStateHelpers.js";
|
|
3
3
|
|
|
4
4
|
function normalizeStatusList(value) {
|
|
5
5
|
if (Array.isArray(value)) {
|
|
@@ -18,37 +18,16 @@ function useViewCore({
|
|
|
18
18
|
notFoundMessage = "Record not found."
|
|
19
19
|
} = {}) {
|
|
20
20
|
const statusList = normalizeStatusList(notFoundStatuses);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
(
|
|
26
|
-
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
restoreModelSnapshot(model, modelSnapshot);
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
immediate: true
|
|
34
|
-
}
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
watch(
|
|
38
|
-
() => resource?.data?.value,
|
|
39
|
-
(payload) => {
|
|
40
|
-
if (!payload || typeof mapLoadedToModel !== "function") {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
mapLoadedToModel(model, payload, {
|
|
21
|
+
watchResourceModelState({
|
|
22
|
+
resource,
|
|
23
|
+
model,
|
|
24
|
+
mapLoadedToModel,
|
|
25
|
+
resolveMapContext() {
|
|
26
|
+
return {
|
|
45
27
|
resource
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
immediate: true
|
|
28
|
+
};
|
|
50
29
|
}
|
|
51
|
-
);
|
|
30
|
+
});
|
|
52
31
|
|
|
53
32
|
const data = resource?.data;
|
|
54
33
|
const record = computed(() => (model !== undefined ? model : data?.value));
|
|
@@ -26,6 +26,9 @@ function normalizeWorkspaceEntry(entry) {
|
|
|
26
26
|
slug,
|
|
27
27
|
name: String(entry.name || slug).trim() || slug,
|
|
28
28
|
color: String(entry.color || "").trim(),
|
|
29
|
+
secondaryColor: String(entry.secondaryColor || "").trim(),
|
|
30
|
+
surfaceColor: String(entry.surfaceColor || "").trim(),
|
|
31
|
+
surfaceVariantColor: String(entry.surfaceVariantColor || "").trim(),
|
|
29
32
|
avatarUrl: String(entry.avatarUrl || "").trim(),
|
|
30
33
|
roleId: String(entry.roleId || "member").trim().toLowerCase() || "member",
|
|
31
34
|
isAccessible: entry.isAccessible !== false
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
mdiViewListOutline
|
|
17
17
|
} from "@mdi/js";
|
|
18
18
|
import { isExternalLinkTarget, splitPathQueryHash } from "@jskit-ai/kernel/shared/support/linkPath";
|
|
19
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
19
20
|
import { normalizePathname as normalizeKernelPathname } from "@jskit-ai/kernel/shared/surface/paths";
|
|
20
21
|
|
|
21
22
|
const SURFACE_SWITCH_ICON_BY_ID = Object.freeze({
|
|
@@ -25,10 +26,6 @@ const SURFACE_SWITCH_ICON_BY_ID = Object.freeze({
|
|
|
25
26
|
console: mdiConsoleNetworkOutline
|
|
26
27
|
});
|
|
27
28
|
|
|
28
|
-
function normalizeText(value) {
|
|
29
|
-
return String(value || "").trim();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
29
|
function normalizePathname(value) {
|
|
33
30
|
const normalizedValue = normalizeText(value);
|
|
34
31
|
if (!normalizedValue) {
|
package/src/client/lib/theme.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { ThemeSymbol } from "vuetify/lib/composables/theme.js";
|
|
2
|
+
import { resolveWorkspaceThemePalette } from "@jskit-ai/users-core/shared/settings";
|
|
2
3
|
|
|
3
4
|
const THEME_PREFERENCE_LIGHT = "light";
|
|
4
5
|
const THEME_PREFERENCE_DARK = "dark";
|
|
5
6
|
const THEME_PREFERENCE_SYSTEM = "system";
|
|
7
|
+
const HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/;
|
|
8
|
+
const WORKSPACE_THEME_NAME_LIGHT = "workspace-light";
|
|
9
|
+
const WORKSPACE_THEME_NAME_DARK = "workspace-dark";
|
|
6
10
|
|
|
7
11
|
function normalizeThemePreference(value) {
|
|
8
12
|
const normalized = String(value || "").trim().toLowerCase();
|
|
@@ -89,11 +93,171 @@ function setVuetifyThemeName(themeController, themeName) {
|
|
|
89
93
|
return true;
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
function normalizeHexColor(value = "") {
|
|
97
|
+
const normalized = String(value || "").trim();
|
|
98
|
+
if (!HEX_COLOR_PATTERN.test(normalized)) {
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
return normalized.toUpperCase();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hexColorToRgb(value = "") {
|
|
105
|
+
const normalized = normalizeHexColor(value);
|
|
106
|
+
if (!normalized) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const red = Number.parseInt(normalized.slice(1, 3), 16);
|
|
111
|
+
const green = Number.parseInt(normalized.slice(3, 5), 16);
|
|
112
|
+
const blue = Number.parseInt(normalized.slice(5, 7), 16);
|
|
113
|
+
return `${red},${green},${blue}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolveVuetifyThemeDefinitions(themeController) {
|
|
117
|
+
if (!themeController || typeof themeController !== "object") {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const themes = themeController.themes?.value;
|
|
121
|
+
if (!themes || typeof themes !== "object") {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return themes;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeThemeColors(colors) {
|
|
128
|
+
const source = colors && typeof colors === "object" ? colors : {};
|
|
129
|
+
const normalized = {};
|
|
130
|
+
for (const [key, value] of Object.entries(source)) {
|
|
131
|
+
normalized[String(key)] = String(value);
|
|
132
|
+
}
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeWorkspaceBaseThemeName(themeName = "") {
|
|
137
|
+
const normalized = String(themeName || "").trim().toLowerCase();
|
|
138
|
+
if (normalized === WORKSPACE_THEME_NAME_LIGHT) {
|
|
139
|
+
return THEME_PREFERENCE_LIGHT;
|
|
140
|
+
}
|
|
141
|
+
if (normalized === WORKSPACE_THEME_NAME_DARK) {
|
|
142
|
+
return THEME_PREFERENCE_DARK;
|
|
143
|
+
}
|
|
144
|
+
if (normalized === THEME_PREFERENCE_DARK) {
|
|
145
|
+
return THEME_PREFERENCE_DARK;
|
|
146
|
+
}
|
|
147
|
+
return THEME_PREFERENCE_LIGHT;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function areThemeColorsEqual(leftColors = {}, rightColors = {}) {
|
|
151
|
+
const leftEntries = Object.entries(leftColors);
|
|
152
|
+
const rightEntries = Object.entries(rightColors);
|
|
153
|
+
if (leftEntries.length !== rightEntries.length) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const [key, value] of leftEntries) {
|
|
158
|
+
if (!Object.hasOwn(rightColors, key)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (String(rightColors[key]) !== String(value)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function composeWorkspaceThemeDefinition(baseThemeDefinition, palette) {
|
|
169
|
+
const baseTheme = baseThemeDefinition && typeof baseThemeDefinition === "object" ? baseThemeDefinition : {};
|
|
170
|
+
const baseColors = normalizeThemeColors(baseTheme.colors);
|
|
171
|
+
return {
|
|
172
|
+
...baseTheme,
|
|
173
|
+
colors: {
|
|
174
|
+
...baseColors,
|
|
175
|
+
primary: palette.color,
|
|
176
|
+
secondary: palette.secondaryColor,
|
|
177
|
+
surface: palette.surfaceColor,
|
|
178
|
+
"surface-variant": palette.surfaceVariantColor,
|
|
179
|
+
background: palette.backgroundColor
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function upsertThemeDefinition(themeDefinitions, themeName, nextDefinition) {
|
|
185
|
+
const currentDefinition =
|
|
186
|
+
themeDefinitions[themeName] && typeof themeDefinitions[themeName] === "object" ? themeDefinitions[themeName] : null;
|
|
187
|
+
const currentColors = normalizeThemeColors(currentDefinition?.colors);
|
|
188
|
+
const nextColors = normalizeThemeColors(nextDefinition?.colors);
|
|
189
|
+
const sameDarkFlag = Boolean(currentDefinition?.dark) === Boolean(nextDefinition?.dark);
|
|
190
|
+
if (sameDarkFlag && areThemeColorsEqual(currentColors, nextColors)) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
themeDefinitions[themeName] = nextDefinition;
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function setVuetifyPrimaryColorOverride(themeController, themeInput = null) {
|
|
198
|
+
if (
|
|
199
|
+
!themeController ||
|
|
200
|
+
typeof themeController !== "object" ||
|
|
201
|
+
!themeController.global ||
|
|
202
|
+
!themeController.global.name
|
|
203
|
+
) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const themeDefinitions = resolveVuetifyThemeDefinitions(themeController);
|
|
208
|
+
if (!themeDefinitions) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const currentThemeName = String(themeController.global.name.value || "").trim();
|
|
213
|
+
const normalizedBaseThemeName = normalizeWorkspaceBaseThemeName(currentThemeName);
|
|
214
|
+
const normalizedThemeName =
|
|
215
|
+
normalizedBaseThemeName === THEME_PREFERENCE_DARK ? THEME_PREFERENCE_DARK : THEME_PREFERENCE_LIGHT;
|
|
216
|
+
const source = themeInput && typeof themeInput === "object" ? themeInput : null;
|
|
217
|
+
|
|
218
|
+
if (!source) {
|
|
219
|
+
if (currentThemeName === normalizedThemeName) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
themeController.global.name.value = normalizedThemeName;
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const baseLightTheme =
|
|
227
|
+
themeDefinitions[THEME_PREFERENCE_LIGHT] && typeof themeDefinitions[THEME_PREFERENCE_LIGHT] === "object"
|
|
228
|
+
? themeDefinitions[THEME_PREFERENCE_LIGHT]
|
|
229
|
+
: null;
|
|
230
|
+
const baseDarkTheme =
|
|
231
|
+
themeDefinitions[THEME_PREFERENCE_DARK] && typeof themeDefinitions[THEME_PREFERENCE_DARK] === "object"
|
|
232
|
+
? themeDefinitions[THEME_PREFERENCE_DARK]
|
|
233
|
+
: null;
|
|
234
|
+
if (!baseLightTheme || !baseDarkTheme) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const palette = resolveWorkspaceThemePalette(source);
|
|
239
|
+
const nextLightTheme = composeWorkspaceThemeDefinition(baseLightTheme, palette);
|
|
240
|
+
const nextDarkTheme = composeWorkspaceThemeDefinition(baseDarkTheme, palette);
|
|
241
|
+
const nextThemeName =
|
|
242
|
+
normalizedThemeName === THEME_PREFERENCE_DARK ? WORKSPACE_THEME_NAME_DARK : WORKSPACE_THEME_NAME_LIGHT;
|
|
243
|
+
|
|
244
|
+
let changed = false;
|
|
245
|
+
changed = upsertThemeDefinition(themeDefinitions, WORKSPACE_THEME_NAME_LIGHT, nextLightTheme) || changed;
|
|
246
|
+
changed = upsertThemeDefinition(themeDefinitions, WORKSPACE_THEME_NAME_DARK, nextDarkTheme) || changed;
|
|
247
|
+
if (themeController.global.name.value !== nextThemeName) {
|
|
248
|
+
themeController.global.name.value = nextThemeName;
|
|
249
|
+
changed = true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return changed;
|
|
253
|
+
}
|
|
254
|
+
|
|
92
255
|
export {
|
|
256
|
+
hexColorToRgb,
|
|
93
257
|
normalizeThemePreference,
|
|
94
258
|
resolveThemeNameForPreference,
|
|
95
259
|
resolveBootstrapThemeName,
|
|
96
260
|
resolveVuetifyThemeController,
|
|
261
|
+
setVuetifyPrimaryColorOverride,
|
|
97
262
|
setVuetifyThemeName
|
|
98
263
|
};
|
|
99
|
-
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveSurfaceDefinitionFromPlacementContext,
|
|
3
3
|
resolveSurfaceIdFromPlacementPathname,
|
|
4
|
-
resolveSurfaceRootPathFromPlacementContext
|
|
4
|
+
resolveSurfaceRootPathFromPlacementContext,
|
|
5
|
+
resolveRuntimePathname
|
|
5
6
|
} from "@jskit-ai/shell-web/client/placement";
|
|
6
7
|
import {
|
|
7
8
|
extractWorkspaceSlugFromSurfacePathname,
|
|
@@ -17,7 +18,6 @@ import {
|
|
|
17
18
|
} from "./bootstrapPlacementRuntimeConstants.js";
|
|
18
19
|
import {
|
|
19
20
|
isGuardDenied,
|
|
20
|
-
normalizeGuardPathname,
|
|
21
21
|
normalizeSearch,
|
|
22
22
|
normalizeWorkspaceBootstrapStatus,
|
|
23
23
|
normalizeWorkspaceSlugKey,
|
|
@@ -36,7 +36,7 @@ function createBootstrapPlacementRouteGuards({
|
|
|
36
36
|
|
|
37
37
|
function resolveNormalizedSurfaceState(pathname = "/", search = "") {
|
|
38
38
|
const context = placementRuntime.getContext();
|
|
39
|
-
const normalizedPathname =
|
|
39
|
+
const normalizedPathname = resolveRuntimePathname(pathname);
|
|
40
40
|
const normalizedSearch = normalizeSearch(search);
|
|
41
41
|
const surfaceId = String(resolveSurfaceIdFromPlacementPathname(context, normalizedPathname) || "")
|
|
42
42
|
.trim()
|
|
@@ -77,7 +77,7 @@ function createBootstrapPlacementRouteGuards({
|
|
|
77
77
|
search: surfaceState.normalizedSearch,
|
|
78
78
|
surfaceId: surfaceState.surfaceId,
|
|
79
79
|
workspaceSlug,
|
|
80
|
-
workspaceRootPath:
|
|
80
|
+
workspaceRootPath: resolveRuntimePathname(
|
|
81
81
|
resolveSurfaceWorkspacePathFromPlacementContext(surfaceState.context, surfaceState.surfaceId, workspaceSlug, "/")
|
|
82
82
|
),
|
|
83
83
|
workspaceBootstrapStatus: String(getWorkspaceBootstrapStatus(workspaceSlug) || "")
|
|
@@ -126,12 +126,12 @@ function createBootstrapPlacementRouteGuards({
|
|
|
126
126
|
if (!fallbackWorkspaceSlug) {
|
|
127
127
|
return "/";
|
|
128
128
|
}
|
|
129
|
-
return
|
|
129
|
+
return resolveRuntimePathname(
|
|
130
130
|
resolveSurfaceWorkspacePathFromPlacementContext(context, fallbackSurfaceId, fallbackWorkspaceSlug, "/")
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
const fallbackPath =
|
|
134
|
+
const fallbackPath = resolveRuntimePathname(resolveSurfaceRootPathFromPlacementContext(context, fallbackSurfaceId));
|
|
135
135
|
if (fallbackPath.includes(":")) {
|
|
136
136
|
return "/";
|
|
137
137
|
}
|
|
@@ -214,7 +214,7 @@ function createBootstrapPlacementRouteGuards({
|
|
|
214
214
|
|
|
215
215
|
const currentRoute = router.currentRoute?.value || {};
|
|
216
216
|
const currentFullPath = String(currentRoute.fullPath || "").trim();
|
|
217
|
-
const currentPath =
|
|
217
|
+
const currentPath = resolveRuntimePathname(currentRoute.path || "/");
|
|
218
218
|
const currentComparablePath = currentFullPath || currentPath;
|
|
219
219
|
if (currentComparablePath === normalizedTargetPath) {
|
|
220
220
|
return;
|
|
@@ -233,7 +233,7 @@ function createBootstrapPlacementRouteGuards({
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
const currentRoute = router.currentRoute?.value || {};
|
|
236
|
-
const currentPath =
|
|
236
|
+
const currentPath = resolveRuntimePathname(currentRoute.path || "/");
|
|
237
237
|
const currentSearch = resolveSearchFromFullPath(currentRoute.fullPath || "");
|
|
238
238
|
const workspaceState = resolveWorkspaceRouteState(currentPath, currentSearch);
|
|
239
239
|
if (!workspaceState || workspaceState.workspaceSlug !== normalizedWorkspaceSlug) {
|
|
@@ -260,7 +260,7 @@ function createBootstrapPlacementRouteGuards({
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
const currentRoute = router.currentRoute?.value || {};
|
|
263
|
-
const currentPath =
|
|
263
|
+
const currentPath = resolveRuntimePathname(currentRoute.path || "/");
|
|
264
264
|
const currentSearch = resolveSearchFromFullPath(currentRoute.fullPath || "");
|
|
265
265
|
const surfaceDecision = resolveSurfaceAccessGuardDecision(currentPath, currentSearch, {
|
|
266
266
|
allowOnUnknown: false
|
|
@@ -299,7 +299,7 @@ function createBootstrapPlacementRouteGuards({
|
|
|
299
299
|
return baseOutcome;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
const pathname =
|
|
302
|
+
const pathname = resolveRuntimePathname(context?.location?.pathname || context?.to?.path || "/");
|
|
303
303
|
const search = normalizeSearch(context?.location?.search || resolveSearchFromFullPath(context?.to?.fullPath || ""));
|
|
304
304
|
const workspaceDecision = resolveWorkspaceGuardDecision(pathname, search);
|
|
305
305
|
if (workspaceDecision) {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { normalizePermissionList } from "../lib/permissions.js";
|
|
14
14
|
import {
|
|
15
15
|
resolveBootstrapThemeName,
|
|
16
|
+
setVuetifyPrimaryColorOverride,
|
|
16
17
|
resolveVuetifyThemeController,
|
|
17
18
|
setVuetifyThemeName
|
|
18
19
|
} from "../lib/theme.js";
|
|
@@ -132,6 +133,10 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
132
133
|
function writePlacementContext(payload = {}, state = {}, source = BOOTSTRAP_PLACEMENT_SOURCE) {
|
|
133
134
|
const availableWorkspaces = normalizeWorkspaceList(payload?.workspaces);
|
|
134
135
|
const currentWorkspace = findWorkspaceBySlug(availableWorkspaces, state.workspaceSlug);
|
|
136
|
+
const workspaceSettings =
|
|
137
|
+
payload?.workspaceSettings && typeof payload.workspaceSettings === "object"
|
|
138
|
+
? payload.workspaceSettings
|
|
139
|
+
: null;
|
|
135
140
|
const permissions = normalizePermissionList(payload?.permissions);
|
|
136
141
|
const user = resolvePlacementUserFromBootstrapPayload(payload, state.context?.user);
|
|
137
142
|
const workspaceInvitesEnabled = payload?.app?.features?.workspaceInvites === true;
|
|
@@ -140,6 +145,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
140
145
|
placementRuntime.setContext(
|
|
141
146
|
{
|
|
142
147
|
workspace: currentWorkspace,
|
|
148
|
+
workspaceSettings,
|
|
143
149
|
workspaces: availableWorkspaces,
|
|
144
150
|
permissions,
|
|
145
151
|
user,
|
|
@@ -152,12 +158,14 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
152
158
|
}
|
|
153
159
|
);
|
|
154
160
|
routeGuards.enforceSurfaceAccessForCurrentRoute();
|
|
161
|
+
applyWorkspaceColorFromPlacementContext("write");
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
function clearPlacementContext(source = BOOTSTRAP_PLACEMENT_SOURCE) {
|
|
158
165
|
placementRuntime.setContext(
|
|
159
166
|
{
|
|
160
167
|
workspace: null,
|
|
168
|
+
workspaceSettings: null,
|
|
161
169
|
workspaces: [],
|
|
162
170
|
permissions: [],
|
|
163
171
|
user: null,
|
|
@@ -170,6 +178,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
170
178
|
}
|
|
171
179
|
);
|
|
172
180
|
routeGuards.enforceSurfaceAccessForCurrentRoute();
|
|
181
|
+
applyWorkspaceColorFromPlacementContext("clear");
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
function getVuetifyThemeController() {
|
|
@@ -204,6 +213,52 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
204
213
|
}
|
|
205
214
|
}
|
|
206
215
|
|
|
216
|
+
function resolveWorkspaceThemeForCurrentRoute() {
|
|
217
|
+
const routeState = resolveRouteState(placementRuntime, router);
|
|
218
|
+
if (!routeState.workspaceSlug) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const context = placementRuntime.getContext();
|
|
223
|
+
const workspaceSettings =
|
|
224
|
+
context?.workspaceSettings && typeof context.workspaceSettings === "object"
|
|
225
|
+
? context.workspaceSettings
|
|
226
|
+
: null;
|
|
227
|
+
if (workspaceSettings) {
|
|
228
|
+
return workspaceSettings;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const workspace = context?.workspace && typeof context.workspace === "object" ? context.workspace : null;
|
|
232
|
+
if (!workspace) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const workspaceSlug = normalizeWorkspaceSlugKey(workspace.slug);
|
|
236
|
+
if (!workspaceSlug || workspaceSlug !== normalizeWorkspaceSlugKey(routeState.workspaceSlug)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return workspace;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function applyWorkspaceColorFromPlacementContext(reason = "manual") {
|
|
244
|
+
const themeController = getVuetifyThemeController();
|
|
245
|
+
if (!themeController) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const workspaceTheme = resolveWorkspaceThemeForCurrentRoute();
|
|
250
|
+
setVuetifyPrimaryColorOverride(themeController, workspaceTheme);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
runtimeLogger.warn(
|
|
253
|
+
{
|
|
254
|
+
reason,
|
|
255
|
+
error: String(error?.message || error || "unknown error")
|
|
256
|
+
},
|
|
257
|
+
"users-web bootstrap workspace color apply failed."
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
207
262
|
async function refresh(reason = "manual") {
|
|
208
263
|
if (shutdownRequested) {
|
|
209
264
|
return;
|
|
@@ -220,6 +275,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
220
275
|
|
|
221
276
|
writePlacementContext(payload, stateAtStart, source);
|
|
222
277
|
applyThemeFromBootstrapPayload(payload, reason);
|
|
278
|
+
applyWorkspaceColorFromPlacementContext(reason);
|
|
223
279
|
if (stateAtStart.workspaceSlug) {
|
|
224
280
|
const sessionAuthenticated = payload?.session?.authenticated === true;
|
|
225
281
|
if (!sessionAuthenticated) {
|
|
@@ -321,6 +377,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
321
377
|
}
|
|
322
378
|
}, "init");
|
|
323
379
|
}
|
|
380
|
+
applyWorkspaceColorFromPlacementContext("init");
|
|
324
381
|
|
|
325
382
|
if (typeof placementRuntime.subscribe === "function") {
|
|
326
383
|
const unsubscribePlacement = placementRuntime.subscribe((event = {}) => {
|
|
@@ -329,6 +386,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
329
386
|
}
|
|
330
387
|
|
|
331
388
|
const nextContext = placementRuntime.getContext();
|
|
389
|
+
applyWorkspaceColorFromPlacementContext("context");
|
|
332
390
|
const nextSignature = resolveAuthSignature(nextContext);
|
|
333
391
|
if (nextSignature === authSignature) {
|
|
334
392
|
return;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createProviderLogger as createSharedProviderLogger
|
|
3
|
+
} from "@jskit-ai/kernel/shared/support/providerLogger";
|
|
1
4
|
import {
|
|
2
5
|
resolveRuntimePathname,
|
|
3
6
|
resolveSurfaceIdFromPlacementPathname
|
|
@@ -12,15 +15,7 @@ import {
|
|
|
12
15
|
import { WORKSPACE_BOOTSTRAP_STATUSES } from "./bootstrapPlacementRuntimeConstants.js";
|
|
13
16
|
|
|
14
17
|
function createProviderLogger(app) {
|
|
15
|
-
return
|
|
16
|
-
warn: (...args) => {
|
|
17
|
-
if (app && typeof app.warn === "function") {
|
|
18
|
-
app.warn(...args);
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
console.warn(...args);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
18
|
+
return createSharedProviderLogger(app);
|
|
24
19
|
}
|
|
25
20
|
|
|
26
21
|
function resolveRouteState(placementRuntime, router) {
|
|
@@ -60,10 +55,6 @@ function resolveSearchFromFullPath(fullPath = "") {
|
|
|
60
55
|
return normalizeSearch(search);
|
|
61
56
|
}
|
|
62
57
|
|
|
63
|
-
function normalizeGuardPathname(pathname = "/") {
|
|
64
|
-
return resolveRuntimePathname(pathname);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
58
|
function isGuardDenied(outcome) {
|
|
68
59
|
if (outcome === false) {
|
|
69
60
|
return true;
|
|
@@ -146,7 +137,6 @@ export {
|
|
|
146
137
|
createProviderLogger,
|
|
147
138
|
fetchBootstrapPayload,
|
|
148
139
|
isGuardDenied,
|
|
149
|
-
normalizeGuardPathname,
|
|
150
140
|
normalizeSearch,
|
|
151
141
|
normalizeWorkspaceBootstrapStatus,
|
|
152
142
|
normalizeWorkspaceSlugKey,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { unref } from "vue";
|
|
2
|
+
|
|
1
3
|
function matchesCurrentWorkspaceEvent(payload = {}, workspaceSlug = "") {
|
|
2
4
|
const payloadWorkspaceSlug = String(payload?.workspaceSlug || "").trim();
|
|
3
5
|
if (!payloadWorkspaceSlug) {
|
|
@@ -7,6 +9,13 @@ function matchesCurrentWorkspaceEvent(payload = {}, workspaceSlug = "") {
|
|
|
7
9
|
return payloadWorkspaceSlug === String(workspaceSlug || "").trim();
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
function createWorkspaceRealtimeMatcher(workspaceSlugRef) {
|
|
13
|
+
return function matchesWorkspaceRealtimeEvent({ payload = {} } = {}) {
|
|
14
|
+
return matchesCurrentWorkspaceEvent(payload, unref(workspaceSlugRef));
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
export {
|
|
11
|
-
matchesCurrentWorkspaceEvent
|
|
19
|
+
matchesCurrentWorkspaceEvent,
|
|
20
|
+
createWorkspaceRealtimeMatcher
|
|
12
21
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { WEB_PLACEMENT_RUNTIME_CLIENT_TOKEN } from "@jskit-ai/shell-web/client/placement";
|
|
8
8
|
import { REALTIME_SOCKET_CLIENT_TOKEN } from "@jskit-ai/realtime/client/tokens";
|
|
9
9
|
import { USERS_BOOTSTRAP_CHANGED_EVENT } from "@jskit-ai/users-core/shared/events/usersEvents";
|
|
10
|
+
import { resolveWorkspaceThemePalette } from "@jskit-ai/users-core/shared/settings";
|
|
10
11
|
import { ThemeSymbol } from "vuetify/lib/composables/theme.js";
|
|
11
12
|
import {
|
|
12
13
|
WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN,
|
|
@@ -199,6 +200,30 @@ function createVuetifyThemeController(initial = "light") {
|
|
|
199
200
|
name: {
|
|
200
201
|
value: initial
|
|
201
202
|
}
|
|
203
|
+
},
|
|
204
|
+
themes: {
|
|
205
|
+
value: {
|
|
206
|
+
light: {
|
|
207
|
+
dark: false,
|
|
208
|
+
colors: {
|
|
209
|
+
primary: "#0f6b54",
|
|
210
|
+
secondary: "#3f5150",
|
|
211
|
+
background: "#eef3ee",
|
|
212
|
+
surface: "#f7fbf6",
|
|
213
|
+
"surface-variant": "#dfe8df"
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
dark: {
|
|
217
|
+
dark: true,
|
|
218
|
+
colors: {
|
|
219
|
+
primary: "#6fd0b5",
|
|
220
|
+
secondary: "#9db2af",
|
|
221
|
+
background: "#0f1715",
|
|
222
|
+
surface: "#16211e",
|
|
223
|
+
"surface-variant": "#253430"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
202
227
|
}
|
|
203
228
|
};
|
|
204
229
|
}
|
|
@@ -515,10 +540,62 @@ test("bootstrap placement runtime reapplies theme when bootstrap payload changes
|
|
|
515
540
|
});
|
|
516
541
|
|
|
517
542
|
await runtime.initialize();
|
|
518
|
-
assert.equal(themeController.global.name.value, "dark");
|
|
543
|
+
assert.equal(themeController.global.name.value, "workspace-dark");
|
|
519
544
|
|
|
520
545
|
socket.emit(USERS_BOOTSTRAP_CHANGED_EVENT, {});
|
|
521
546
|
await flushTasks();
|
|
547
|
+
assert.equal(themeController.global.name.value, "workspace-light");
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test("bootstrap placement runtime applies workspace palette via Vuetify workspace themes and clears it off workspace routes", async () => {
|
|
551
|
+
const placementRuntime = createPlacementRuntimeStub();
|
|
552
|
+
const router = createRouterStub("/w/acme/dashboard");
|
|
553
|
+
const themeController = createVuetifyThemeController("light");
|
|
554
|
+
const runtime = createBootstrapPlacementRuntime({
|
|
555
|
+
app: createAppStub({
|
|
556
|
+
[WEB_PLACEMENT_RUNTIME_CLIENT_TOKEN]: placementRuntime,
|
|
557
|
+
[CLIENT_MODULE_ROUTER_TOKEN]: router,
|
|
558
|
+
[CLIENT_MODULE_VUE_APP_TOKEN]: createVueAppWithThemeController(themeController)
|
|
559
|
+
}),
|
|
560
|
+
fetchBootstrap: async (workspaceSlug) => {
|
|
561
|
+
return {
|
|
562
|
+
session: {
|
|
563
|
+
authenticated: true,
|
|
564
|
+
userId: 1
|
|
565
|
+
},
|
|
566
|
+
workspaces: [
|
|
567
|
+
{
|
|
568
|
+
id: 1,
|
|
569
|
+
slug: "acme",
|
|
570
|
+
name: "Acme Workspace",
|
|
571
|
+
color: "#CC3344"
|
|
572
|
+
}
|
|
573
|
+
],
|
|
574
|
+
permissions: []
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
await runtime.initialize();
|
|
580
|
+
const palette = resolveWorkspaceThemePalette({
|
|
581
|
+
color: "#CC3344"
|
|
582
|
+
});
|
|
583
|
+
assert.equal(themeController.global.name.value, "workspace-light");
|
|
584
|
+
assert.equal(themeController.themes.value["workspace-light"].colors.primary, palette.color);
|
|
585
|
+
assert.equal(themeController.themes.value["workspace-light"].colors.secondary, palette.secondaryColor);
|
|
586
|
+
assert.equal(themeController.themes.value["workspace-light"].colors.surface, palette.surfaceColor);
|
|
587
|
+
assert.equal(
|
|
588
|
+
themeController.themes.value["workspace-light"].colors["surface-variant"],
|
|
589
|
+
palette.surfaceVariantColor
|
|
590
|
+
);
|
|
591
|
+
assert.equal(themeController.themes.value["workspace-light"].colors.background, palette.backgroundColor);
|
|
592
|
+
|
|
593
|
+
router.currentRoute.value.path = "/home";
|
|
594
|
+
router.currentRoute.value.fullPath = "/home";
|
|
595
|
+
router.emitAfterEach();
|
|
596
|
+
await flushTasks();
|
|
597
|
+
await flushTasks();
|
|
598
|
+
|
|
522
599
|
assert.equal(themeController.global.name.value, "light");
|
|
523
600
|
});
|
|
524
601
|
|
package/test/theme.test.js
CHANGED
|
@@ -1,14 +1,51 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { ThemeSymbol } from "vuetify/lib/composables/theme.js";
|
|
4
|
+
import { resolveWorkspaceThemePalette } from "@jskit-ai/users-core/shared/settings";
|
|
4
5
|
import {
|
|
6
|
+
hexColorToRgb,
|
|
5
7
|
normalizeThemePreference,
|
|
6
8
|
resolveThemeNameForPreference,
|
|
7
9
|
resolveBootstrapThemeName,
|
|
8
10
|
resolveVuetifyThemeController,
|
|
11
|
+
setVuetifyPrimaryColorOverride,
|
|
9
12
|
setVuetifyThemeName
|
|
10
13
|
} from "../src/client/lib/theme.js";
|
|
11
14
|
|
|
15
|
+
function createVuetifyThemeController(initialTheme = "light") {
|
|
16
|
+
return {
|
|
17
|
+
global: {
|
|
18
|
+
name: {
|
|
19
|
+
value: initialTheme
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
themes: {
|
|
23
|
+
value: {
|
|
24
|
+
light: {
|
|
25
|
+
dark: false,
|
|
26
|
+
colors: {
|
|
27
|
+
primary: "#0f6b54",
|
|
28
|
+
secondary: "#3f5150",
|
|
29
|
+
background: "#eef3ee",
|
|
30
|
+
surface: "#f7fbf6",
|
|
31
|
+
"surface-variant": "#dfe8df"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
dark: {
|
|
35
|
+
dark: true,
|
|
36
|
+
colors: {
|
|
37
|
+
primary: "#6fd0b5",
|
|
38
|
+
secondary: "#9db2af",
|
|
39
|
+
background: "#0f1715",
|
|
40
|
+
surface: "#16211e",
|
|
41
|
+
"surface-variant": "#253430"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
12
49
|
test("normalizeThemePreference accepts known preferences and falls back to system", () => {
|
|
13
50
|
assert.equal(normalizeThemePreference("light"), "light");
|
|
14
51
|
assert.equal(normalizeThemePreference(" DARK "), "dark");
|
|
@@ -60,13 +97,7 @@ test("resolveBootstrapThemeName uses authenticated user preference", () => {
|
|
|
60
97
|
});
|
|
61
98
|
|
|
62
99
|
test("resolveVuetifyThemeController reads theme controller from Vue app provides", () => {
|
|
63
|
-
const themeController =
|
|
64
|
-
global: {
|
|
65
|
-
name: {
|
|
66
|
-
value: "light"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
};
|
|
100
|
+
const themeController = createVuetifyThemeController("light");
|
|
70
101
|
const vueApp = {
|
|
71
102
|
_context: {
|
|
72
103
|
provides: {
|
|
@@ -80,16 +111,44 @@ test("resolveVuetifyThemeController reads theme controller from Vue app provides
|
|
|
80
111
|
});
|
|
81
112
|
|
|
82
113
|
test("setVuetifyThemeName updates only when the value changes", () => {
|
|
83
|
-
const themeController =
|
|
84
|
-
global: {
|
|
85
|
-
name: {
|
|
86
|
-
value: "light"
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
};
|
|
114
|
+
const themeController = createVuetifyThemeController("light");
|
|
90
115
|
|
|
91
116
|
assert.equal(setVuetifyThemeName(themeController, "light"), false);
|
|
92
117
|
assert.equal(setVuetifyThemeName(themeController, "dark"), true);
|
|
93
118
|
assert.equal(themeController.global.name.value, "dark");
|
|
94
119
|
});
|
|
95
120
|
|
|
121
|
+
test("hexColorToRgb returns Vuetify rgb tuple and rejects invalid values", () => {
|
|
122
|
+
assert.equal(hexColorToRgb("#0f6b54"), "15,107,84");
|
|
123
|
+
assert.equal(hexColorToRgb("#CC3344"), "204,51,68");
|
|
124
|
+
assert.equal(hexColorToRgb("invalid"), "");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("setVuetifyPrimaryColorOverride mutates workspace themes and restores base theme names", () => {
|
|
128
|
+
const themeController = createVuetifyThemeController("light");
|
|
129
|
+
const expectedPalette = resolveWorkspaceThemePalette({
|
|
130
|
+
color: "#CC3344"
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
assert.equal(setVuetifyPrimaryColorOverride(themeController, { color: "#CC3344" }), true);
|
|
134
|
+
assert.equal(themeController.global.name.value, "workspace-light");
|
|
135
|
+
assert.equal(themeController.themes.value["workspace-light"].colors.primary, expectedPalette.color);
|
|
136
|
+
assert.equal(themeController.themes.value["workspace-light"].colors.secondary, expectedPalette.secondaryColor);
|
|
137
|
+
assert.equal(themeController.themes.value["workspace-light"].colors.surface, expectedPalette.surfaceColor);
|
|
138
|
+
assert.equal(
|
|
139
|
+
themeController.themes.value["workspace-light"].colors["surface-variant"],
|
|
140
|
+
expectedPalette.surfaceVariantColor
|
|
141
|
+
);
|
|
142
|
+
assert.equal(themeController.themes.value["workspace-light"].colors.background, expectedPalette.backgroundColor);
|
|
143
|
+
|
|
144
|
+
assert.equal(setVuetifyPrimaryColorOverride(themeController, { color: "#CC3344" }), false);
|
|
145
|
+
|
|
146
|
+
assert.equal(setVuetifyThemeName(themeController, "dark"), true);
|
|
147
|
+
assert.equal(setVuetifyPrimaryColorOverride(themeController, { color: "#CC3344" }), true);
|
|
148
|
+
assert.equal(themeController.global.name.value, "workspace-dark");
|
|
149
|
+
assert.equal(themeController.themes.value["workspace-dark"].colors.primary, expectedPalette.color);
|
|
150
|
+
|
|
151
|
+
assert.equal(setVuetifyPrimaryColorOverride(themeController, null), true);
|
|
152
|
+
assert.equal(themeController.global.name.value, "dark");
|
|
153
|
+
assert.equal(setVuetifyPrimaryColorOverride(themeController, null), false);
|
|
154
|
+
});
|