@jskit-ai/users-web 0.1.52 → 0.1.54
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 +14 -96
- package/package.json +16 -11
- package/src/client/account-settings/sections.js +74 -0
- package/src/client/composables/account-settings/accountSettingsRuntimeHelpers.js +2 -38
- package/src/client/composables/crud/crudLookupFieldRuntime.js +2 -2
- package/src/client/composables/internal/crudListParentTitleSupport.js +1 -1
- package/src/client/composables/internal/useOperationScope.js +12 -12
- package/src/client/composables/records/useAddEdit.js +2 -2
- package/src/client/composables/records/useList.js +3 -3
- package/src/client/composables/records/useView.js +2 -2
- package/src/client/composables/support/scopeHelpers.js +19 -19
- package/src/client/composables/useAccess.js +3 -3
- package/src/client/composables/useAccountSettingsRuntime.js +8 -156
- package/src/client/composables/useCommand.js +2 -2
- package/src/client/composables/useCrudListParentTitle.js +2 -2
- package/src/client/composables/usePaths.js +50 -38
- package/src/client/composables/useScopeRuntime.js +55 -27
- package/src/client/composables/useSurfaceRouteContext.js +1 -7
- package/src/client/index.js +0 -1
- package/src/client/lib/bootstrap.js +0 -63
- package/src/client/lib/httpClient.js +2 -59
- package/src/client/lib/theme.js +12 -189
- package/src/client/providers/UsersWebClientProvider.js +2 -25
- package/src/client/providers/bootUsersWebClientProvider.js +28 -0
- package/src/shared/toolsOutletContracts.js +1 -8
- package/templates/src/components/account/settings/AccountSettingsClientElement.vue +33 -21
- package/test/accountSettingsSections.test.js +79 -0
- package/test/exportsContract.test.js +2 -2
- package/test/scopeHelpers.test.js +6 -6
- package/test/settingsPlacementContract.test.js +4 -49
- package/test/theme.test.js +0 -56
- package/src/client/components/ConsoleSettingsClientElement.vue +0 -24
- package/src/client/components/MembersAdminClientElement.vue +0 -400
- package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +0 -39
- package/src/client/components/UsersWorkspaceMembersMenuItem.vue +0 -36
- package/src/client/components/UsersWorkspacePermissionMenuItem.vue +0 -90
- package/src/client/components/UsersWorkspaceSelector.vue +0 -248
- package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +0 -39
- package/src/client/components/UsersWorkspaceToolsWidget.vue +0 -12
- package/src/client/components/WorkspaceMembersClientElement.vue +0 -655
- package/src/client/components/WorkspaceProfileClientElement.vue +0 -116
- package/src/client/components/WorkspaceSettingsClientElement.vue +0 -102
- package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +0 -265
- package/src/client/components/WorkspacesClientElement.vue +0 -509
- package/src/client/composables/account-settings/accountSettingsInvitesRuntime.js +0 -88
- package/src/client/composables/useBootstrapQuery.js +0 -52
- package/src/client/composables/useWorkspaceRouteContext.js +0 -28
- package/src/client/composables/useWorkspaceSurfaceId.js +0 -43
- package/src/client/lib/menuIcons.js +0 -210
- package/src/client/lib/profileSurfaceMenuLinks.js +0 -142
- package/src/client/lib/surfaceAccessPolicy.js +0 -350
- package/src/client/lib/workspaceLinkResolver.js +0 -207
- package/src/client/lib/workspaceSurfaceContext.js +0 -82
- package/src/client/lib/workspaceSurfacePaths.js +0 -163
- package/src/client/providers/UsersWorkspacesClientProvider.js +0 -24
- package/src/client/runtime/bootstrapPlacementRouteGuards.js +0 -371
- package/src/client/runtime/bootstrapPlacementRuntime.js +0 -463
- package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +0 -28
- package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +0 -147
- package/src/client/support/menuLinkTarget.js +0 -93
- package/src/client/support/realtimeWorkspace.js +0 -21
- package/src/client/support/runtimeNormalization.js +0 -27
- package/src/client/support/workspaceQueryKeys.js +0 -15
- package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +0 -77
- package/templates/src/pages/console/settings/index.vue +0 -8
- package/templates/src/pages/console/settings.vue +0 -32
- package/test/bootstrapPlacementRuntime.test.js +0 -1095
- package/test/menuIcons.test.js +0 -35
- package/test/menuLinkTarget.test.js +0 -116
- package/test/profileSurfaceMenuLinks.test.js +0 -207
- package/test/surfaceAccessPolicy.test.js +0 -129
- package/test/workspaceLinkResolver.test.js +0 -61
- package/test/workspaceSurfacePaths.test.js +0 -39
|
@@ -1,67 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
isTransientQueryError,
|
|
4
|
-
transientQueryRetryDelay
|
|
5
|
-
} from "@jskit-ai/kernel/shared/support";
|
|
1
|
+
import { createTransientRetryHttpClient } from "@jskit-ai/http-runtime/client";
|
|
6
2
|
|
|
7
|
-
const
|
|
8
|
-
const MAX_TRANSIENT_HTTP_RETRIES = 2;
|
|
9
|
-
|
|
10
|
-
const baseUsersWebHttpClient = createHttpClient({
|
|
3
|
+
const usersWebHttpClient = createTransientRetryHttpClient({
|
|
11
4
|
credentials: "include",
|
|
12
5
|
csrf: {
|
|
13
6
|
sessionPath: "/api/session"
|
|
14
7
|
}
|
|
15
8
|
});
|
|
16
9
|
|
|
17
|
-
function sleep(delayMs) {
|
|
18
|
-
return new Promise((resolve) => {
|
|
19
|
-
setTimeout(resolve, delayMs);
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function shouldRetryTransientHttpFailure(error, method, attemptIndex) {
|
|
24
|
-
if (!SAFE_RETRY_METHODS.has(String(method || "GET").toUpperCase())) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
if (!isTransientQueryError(error)) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
return Number(attemptIndex) < MAX_TRANSIENT_HTTP_RETRIES;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function requestWithTransientRetry(executor, method) {
|
|
34
|
-
let attemptIndex = 0;
|
|
35
|
-
|
|
36
|
-
while (true) {
|
|
37
|
-
try {
|
|
38
|
-
return await executor();
|
|
39
|
-
} catch (error) {
|
|
40
|
-
if (!shouldRetryTransientHttpFailure(error, method, attemptIndex)) {
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
attemptIndex += 1;
|
|
44
|
-
await sleep(transientQueryRetryDelay(attemptIndex));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const usersWebHttpClient = Object.freeze({
|
|
50
|
-
...baseUsersWebHttpClient,
|
|
51
|
-
request(url, requestOptions = {}, state = null) {
|
|
52
|
-
const method = String(requestOptions?.method || "GET").toUpperCase();
|
|
53
|
-
return requestWithTransientRetry(
|
|
54
|
-
() => baseUsersWebHttpClient.request(url, requestOptions, state),
|
|
55
|
-
method
|
|
56
|
-
);
|
|
57
|
-
},
|
|
58
|
-
requestStream(url, requestOptions = {}, handlers = {}, state = null) {
|
|
59
|
-
const method = String(requestOptions?.method || "GET").toUpperCase();
|
|
60
|
-
return requestWithTransientRetry(
|
|
61
|
-
() => baseUsersWebHttpClient.requestStream(url, requestOptions, handlers, state),
|
|
62
|
-
method
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
10
|
export { usersWebHttpClient };
|
package/src/client/lib/theme.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { ThemeSymbol } from "vuetify/lib/composables/theme.js";
|
|
2
|
-
import { resolveWorkspaceThemePalette } from "@jskit-ai/users-core/shared/settings";
|
|
3
2
|
|
|
4
3
|
const THEME_PREFERENCE_LIGHT = "light";
|
|
5
4
|
const THEME_PREFERENCE_DARK = "dark";
|
|
6
5
|
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";
|
|
10
6
|
|
|
11
7
|
function normalizeThemePreference(value) {
|
|
12
8
|
const normalized = String(value || "").trim().toLowerCase();
|
|
@@ -22,8 +18,9 @@ function resolveSystemThemeName({ prefersDark } = {}) {
|
|
|
22
18
|
}
|
|
23
19
|
|
|
24
20
|
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
22
|
+
? THEME_PREFERENCE_DARK
|
|
23
|
+
: THEME_PREFERENCE_LIGHT;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
return THEME_PREFERENCE_LIGHT;
|
|
@@ -51,16 +48,11 @@ function resolveThemePreferenceStorage(options = {}) {
|
|
|
51
48
|
return null;
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
let storage = null;
|
|
55
51
|
try {
|
|
56
|
-
|
|
52
|
+
return window.localStorage;
|
|
57
53
|
} catch {
|
|
58
54
|
return null;
|
|
59
55
|
}
|
|
60
|
-
if (!storage || typeof storage !== "object") {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
return storage;
|
|
64
56
|
}
|
|
65
57
|
|
|
66
58
|
function readPersistedThemePreference(options = {}) {
|
|
@@ -70,8 +62,7 @@ function readPersistedThemePreference(options = {}) {
|
|
|
70
62
|
}
|
|
71
63
|
|
|
72
64
|
try {
|
|
73
|
-
|
|
74
|
-
return normalizeThemePreference(value);
|
|
65
|
+
return normalizeThemePreference(storage.getItem("jskit.themePreference"));
|
|
75
66
|
} catch {
|
|
76
67
|
return THEME_PREFERENCE_SYSTEM;
|
|
77
68
|
}
|
|
@@ -83,9 +74,8 @@ function persistThemePreference(themePreference, options = {}) {
|
|
|
83
74
|
return false;
|
|
84
75
|
}
|
|
85
76
|
|
|
86
|
-
const normalizedPreference = normalizeThemePreference(themePreference);
|
|
87
77
|
try {
|
|
88
|
-
storage.setItem("jskit.themePreference",
|
|
78
|
+
storage.setItem("jskit.themePreference", normalizeThemePreference(themePreference));
|
|
89
79
|
return true;
|
|
90
80
|
} catch {
|
|
91
81
|
return false;
|
|
@@ -128,12 +118,7 @@ function resolveVuetifyThemeController(vueApp) {
|
|
|
128
118
|
}
|
|
129
119
|
|
|
130
120
|
const themeController = provides[ThemeSymbol];
|
|
131
|
-
if (
|
|
132
|
-
!themeController ||
|
|
133
|
-
typeof themeController !== "object" ||
|
|
134
|
-
!themeController.global ||
|
|
135
|
-
!themeController.global.name
|
|
136
|
-
) {
|
|
121
|
+
if (!themeController || typeof themeController !== "object" || !themeController.global || !themeController.global.name) {
|
|
137
122
|
return null;
|
|
138
123
|
}
|
|
139
124
|
|
|
@@ -141,12 +126,7 @@ function resolveVuetifyThemeController(vueApp) {
|
|
|
141
126
|
}
|
|
142
127
|
|
|
143
128
|
function setVuetifyThemeName(themeController, themeName) {
|
|
144
|
-
if (
|
|
145
|
-
!themeController ||
|
|
146
|
-
typeof themeController !== "object" ||
|
|
147
|
-
!themeController.global ||
|
|
148
|
-
!themeController.global.name
|
|
149
|
-
) {
|
|
129
|
+
if (!themeController || typeof themeController !== "object" || !themeController.global || !themeController.global.name) {
|
|
150
130
|
return false;
|
|
151
131
|
}
|
|
152
132
|
|
|
@@ -154,171 +134,15 @@ function setVuetifyThemeName(themeController, themeName) {
|
|
|
154
134
|
if (themeController.global.name.value === normalizedThemeName) {
|
|
155
135
|
return false;
|
|
156
136
|
}
|
|
157
|
-
themeController.global.name.value = normalizedThemeName;
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function normalizeHexColor(value = "") {
|
|
162
|
-
const normalized = String(value || "").trim();
|
|
163
|
-
if (!HEX_COLOR_PATTERN.test(normalized)) {
|
|
164
|
-
return "";
|
|
165
|
-
}
|
|
166
|
-
return normalized.toUpperCase();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function hexColorToRgb(value = "") {
|
|
170
|
-
const normalized = normalizeHexColor(value);
|
|
171
|
-
if (!normalized) {
|
|
172
|
-
return "";
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const red = Number.parseInt(normalized.slice(1, 3), 16);
|
|
176
|
-
const green = Number.parseInt(normalized.slice(3, 5), 16);
|
|
177
|
-
const blue = Number.parseInt(normalized.slice(5, 7), 16);
|
|
178
|
-
return `${red},${green},${blue}`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function resolveVuetifyThemeDefinitions(themeController) {
|
|
182
|
-
if (!themeController || typeof themeController !== "object") {
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
const themes = themeController.themes?.value;
|
|
186
|
-
if (!themes || typeof themes !== "object") {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
return themes;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function normalizeThemeColors(colors) {
|
|
193
|
-
const source = colors && typeof colors === "object" ? colors : {};
|
|
194
|
-
const normalized = {};
|
|
195
|
-
for (const [key, value] of Object.entries(source)) {
|
|
196
|
-
normalized[String(key)] = String(value);
|
|
197
|
-
}
|
|
198
|
-
return normalized;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function normalizeWorkspaceBaseThemeName(themeName = "") {
|
|
202
|
-
const normalized = String(themeName || "").trim().toLowerCase();
|
|
203
|
-
if (normalized === WORKSPACE_THEME_NAME_LIGHT) {
|
|
204
|
-
return THEME_PREFERENCE_LIGHT;
|
|
205
|
-
}
|
|
206
|
-
if (normalized === WORKSPACE_THEME_NAME_DARK) {
|
|
207
|
-
return THEME_PREFERENCE_DARK;
|
|
208
|
-
}
|
|
209
|
-
if (normalized === THEME_PREFERENCE_DARK) {
|
|
210
|
-
return THEME_PREFERENCE_DARK;
|
|
211
|
-
}
|
|
212
|
-
return THEME_PREFERENCE_LIGHT;
|
|
213
|
-
}
|
|
214
137
|
|
|
215
|
-
|
|
216
|
-
const leftEntries = Object.entries(leftColors);
|
|
217
|
-
const rightEntries = Object.entries(rightColors);
|
|
218
|
-
if (leftEntries.length !== rightEntries.length) {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
for (const [key, value] of leftEntries) {
|
|
223
|
-
if (!Object.hasOwn(rightColors, key)) {
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
226
|
-
if (String(rightColors[key]) !== String(value)) {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function composeWorkspaceThemeDefinition(baseThemeDefinition, palette) {
|
|
234
|
-
const baseTheme = baseThemeDefinition && typeof baseThemeDefinition === "object" ? baseThemeDefinition : {};
|
|
235
|
-
const baseColors = normalizeThemeColors(baseTheme.colors);
|
|
236
|
-
return {
|
|
237
|
-
...baseTheme,
|
|
238
|
-
colors: {
|
|
239
|
-
...baseColors,
|
|
240
|
-
primary: palette.color,
|
|
241
|
-
secondary: palette.secondaryColor,
|
|
242
|
-
surface: palette.surfaceColor,
|
|
243
|
-
"surface-variant": palette.surfaceVariantColor
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function upsertThemeDefinition(themeDefinitions, themeName, nextDefinition) {
|
|
249
|
-
const currentDefinition =
|
|
250
|
-
themeDefinitions[themeName] && typeof themeDefinitions[themeName] === "object" ? themeDefinitions[themeName] : null;
|
|
251
|
-
const currentColors = normalizeThemeColors(currentDefinition?.colors);
|
|
252
|
-
const nextColors = normalizeThemeColors(nextDefinition?.colors);
|
|
253
|
-
const sameDarkFlag = Boolean(currentDefinition?.dark) === Boolean(nextDefinition?.dark);
|
|
254
|
-
if (sameDarkFlag && areThemeColorsEqual(currentColors, nextColors)) {
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
themeDefinitions[themeName] = nextDefinition;
|
|
138
|
+
themeController.global.name.value = normalizedThemeName;
|
|
258
139
|
return true;
|
|
259
140
|
}
|
|
260
141
|
|
|
261
|
-
function setVuetifyPrimaryColorOverride(themeController, themeInput = null) {
|
|
262
|
-
if (
|
|
263
|
-
!themeController ||
|
|
264
|
-
typeof themeController !== "object" ||
|
|
265
|
-
!themeController.global ||
|
|
266
|
-
!themeController.global.name
|
|
267
|
-
) {
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const themeDefinitions = resolveVuetifyThemeDefinitions(themeController);
|
|
272
|
-
if (!themeDefinitions) {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const currentThemeName = String(themeController.global.name.value || "").trim();
|
|
277
|
-
const normalizedBaseThemeName = normalizeWorkspaceBaseThemeName(currentThemeName);
|
|
278
|
-
const normalizedThemeName =
|
|
279
|
-
normalizedBaseThemeName === THEME_PREFERENCE_DARK ? THEME_PREFERENCE_DARK : THEME_PREFERENCE_LIGHT;
|
|
280
|
-
const source = themeInput && typeof themeInput === "object" ? themeInput : null;
|
|
281
|
-
|
|
282
|
-
if (!source) {
|
|
283
|
-
if (currentThemeName === normalizedThemeName) {
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
themeController.global.name.value = normalizedThemeName;
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const baseLightTheme =
|
|
291
|
-
themeDefinitions[THEME_PREFERENCE_LIGHT] && typeof themeDefinitions[THEME_PREFERENCE_LIGHT] === "object"
|
|
292
|
-
? themeDefinitions[THEME_PREFERENCE_LIGHT]
|
|
293
|
-
: null;
|
|
294
|
-
const baseDarkTheme =
|
|
295
|
-
themeDefinitions[THEME_PREFERENCE_DARK] && typeof themeDefinitions[THEME_PREFERENCE_DARK] === "object"
|
|
296
|
-
? themeDefinitions[THEME_PREFERENCE_DARK]
|
|
297
|
-
: null;
|
|
298
|
-
if (!baseLightTheme || !baseDarkTheme) {
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const lightPalette = resolveWorkspaceThemePalette(source, { mode: THEME_PREFERENCE_LIGHT });
|
|
303
|
-
const darkPalette = resolveWorkspaceThemePalette(source, { mode: THEME_PREFERENCE_DARK });
|
|
304
|
-
const nextLightTheme = composeWorkspaceThemeDefinition(baseLightTheme, lightPalette);
|
|
305
|
-
const nextDarkTheme = composeWorkspaceThemeDefinition(baseDarkTheme, darkPalette);
|
|
306
|
-
const nextThemeName =
|
|
307
|
-
normalizedThemeName === THEME_PREFERENCE_DARK ? WORKSPACE_THEME_NAME_DARK : WORKSPACE_THEME_NAME_LIGHT;
|
|
308
|
-
|
|
309
|
-
let changed = false;
|
|
310
|
-
changed = upsertThemeDefinition(themeDefinitions, WORKSPACE_THEME_NAME_LIGHT, nextLightTheme) || changed;
|
|
311
|
-
changed = upsertThemeDefinition(themeDefinitions, WORKSPACE_THEME_NAME_DARK, nextDarkTheme) || changed;
|
|
312
|
-
if (themeController.global.name.value !== nextThemeName) {
|
|
313
|
-
themeController.global.name.value = nextThemeName;
|
|
314
|
-
changed = true;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return changed;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
142
|
export {
|
|
321
|
-
|
|
143
|
+
THEME_PREFERENCE_DARK,
|
|
144
|
+
THEME_PREFERENCE_LIGHT,
|
|
145
|
+
THEME_PREFERENCE_SYSTEM,
|
|
322
146
|
normalizeThemePreference,
|
|
323
147
|
persistBootstrapThemePreference,
|
|
324
148
|
persistThemePreference,
|
|
@@ -327,6 +151,5 @@ export {
|
|
|
327
151
|
resolveThemeNameForPreference,
|
|
328
152
|
resolveBootstrapThemeName,
|
|
329
153
|
resolveVuetifyThemeController,
|
|
330
|
-
setVuetifyPrimaryColorOverride,
|
|
331
154
|
setVuetifyThemeName
|
|
332
155
|
};
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import UsersProfileSurfaceSwitchMenuItem from "../components/UsersProfileSurfaceSwitchMenuItem.vue";
|
|
2
1
|
import UsersHomeToolsWidget from "../components/UsersHomeToolsWidget.vue";
|
|
3
2
|
import ProfileClientElement from "../components/ProfileClientElement.vue";
|
|
4
|
-
import {
|
|
5
|
-
createBootstrapPlacementRuntime
|
|
6
|
-
} from "../runtime/bootstrapPlacementRuntime.js";
|
|
3
|
+
import { bootUsersWebClientProvider } from "./bootUsersWebClientProvider.js";
|
|
7
4
|
|
|
8
5
|
class UsersWebClientProvider {
|
|
9
6
|
static id = "users.web.client";
|
|
@@ -14,32 +11,12 @@ class UsersWebClientProvider {
|
|
|
14
11
|
throw new Error("UsersWebClientProvider requires application singleton().");
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
app.singleton("users.web.profile.menu.surface-switch-item", () => UsersProfileSurfaceSwitchMenuItem);
|
|
18
14
|
app.singleton("users.web.home.tools.widget", () => UsersHomeToolsWidget);
|
|
19
15
|
app.singleton("users.web.profile.element", () => ProfileClientElement);
|
|
20
|
-
app.singleton("users.web.bootstrap-placement.runtime", (scope) => createBootstrapPlacementRuntime({ app: scope }));
|
|
21
16
|
}
|
|
22
17
|
|
|
23
18
|
async boot(app) {
|
|
24
|
-
|
|
25
|
-
throw new Error("UsersWebClientProvider requires application make().");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const runtime = app.make("users.web.bootstrap-placement.runtime");
|
|
29
|
-
if (runtime && typeof runtime.initialize === "function") {
|
|
30
|
-
await runtime.initialize();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
shutdown(app) {
|
|
35
|
-
if (!app || typeof app.make !== "function") {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const runtime = app.make("users.web.bootstrap-placement.runtime");
|
|
40
|
-
if (runtime && typeof runtime.shutdown === "function") {
|
|
41
|
-
runtime.shutdown();
|
|
42
|
-
}
|
|
19
|
+
await bootUsersWebClientProvider(app);
|
|
43
20
|
}
|
|
44
21
|
}
|
|
45
22
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACCOUNT_SETTINGS_SECTIONS_INJECTION_KEY,
|
|
3
|
+
ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG,
|
|
4
|
+
resolveAccountSettingsSections
|
|
5
|
+
} from "../account-settings/sections.js";
|
|
6
|
+
|
|
7
|
+
async function bootUsersWebClientProvider(app) {
|
|
8
|
+
if (!app || typeof app.make !== "function") {
|
|
9
|
+
throw new Error("bootUsersWebClientProvider requires application make().");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const vueApp = app.make("jskit.client.vue.app");
|
|
13
|
+
if (!vueApp || typeof vueApp.provide !== "function") {
|
|
14
|
+
throw new Error("bootUsersWebClientProvider requires jskit.client.vue.app provide().");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const extensionSections =
|
|
18
|
+
typeof app.resolveTag === "function"
|
|
19
|
+
? app.resolveTag(ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG)
|
|
20
|
+
: [];
|
|
21
|
+
|
|
22
|
+
vueApp.provide(
|
|
23
|
+
ACCOUNT_SETTINGS_SECTIONS_INJECTION_KEY,
|
|
24
|
+
resolveAccountSettingsSections(extensionSections)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { bootUsersWebClientProvider };
|
|
@@ -6,14 +6,7 @@ const HOME_TOOLS_OUTLET = Object.freeze({
|
|
|
6
6
|
ariaLabel: "Home tools"
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
const WORKSPACE_TOOLS_OUTLET = Object.freeze({
|
|
10
|
-
target: "workspace-tools:primary-menu",
|
|
11
|
-
defaultLinkComponentToken: DEFAULT_TOOLS_LINK_COMPONENT_TOKEN,
|
|
12
|
-
ariaLabel: "Workspace tools"
|
|
13
|
-
});
|
|
14
|
-
|
|
15
9
|
export {
|
|
16
10
|
DEFAULT_TOOLS_LINK_COMPONENT_TOKEN,
|
|
17
|
-
HOME_TOOLS_OUTLET
|
|
18
|
-
WORKSPACE_TOOLS_OUTLET
|
|
11
|
+
HOME_TOOLS_OUTLET
|
|
19
12
|
};
|
|
@@ -3,27 +3,48 @@ import { computed } from "vue";
|
|
|
3
3
|
import { useRoute, useRouter } from "vue-router";
|
|
4
4
|
import { normalizeOneOf } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
5
|
import { useAccountSettingsRuntime } from "@jskit-ai/users-web/client/composables/useAccountSettingsRuntime";
|
|
6
|
+
import { useAccountSettingsSections } from "@jskit-ai/users-web/client/account-settings/sections";
|
|
6
7
|
import AccountSettingsProfileSection from "./AccountSettingsProfileSection.vue";
|
|
7
8
|
import AccountSettingsPreferencesSection from "./AccountSettingsPreferencesSection.vue";
|
|
8
9
|
import AccountSettingsNotificationsSection from "./AccountSettingsNotificationsSection.vue";
|
|
9
|
-
import AccountSettingsInvitesSection from "./AccountSettingsInvitesSection.vue";
|
|
10
10
|
|
|
11
11
|
const runtime = useAccountSettingsRuntime();
|
|
12
12
|
const route = useRoute();
|
|
13
13
|
const router = useRouter();
|
|
14
|
+
const extensionSections = useAccountSettingsSections();
|
|
14
15
|
|
|
15
16
|
const sections = computed(() => {
|
|
16
17
|
const nextSections = [
|
|
17
|
-
{ title: "Profile", value: "profile" },
|
|
18
|
-
{
|
|
19
|
-
|
|
18
|
+
{ title: "Profile", value: "profile", component: AccountSettingsProfileSection, usesSharedRuntime: true, order: 100 },
|
|
19
|
+
{
|
|
20
|
+
title: "Preferences",
|
|
21
|
+
value: "preferences",
|
|
22
|
+
component: AccountSettingsPreferencesSection,
|
|
23
|
+
usesSharedRuntime: true,
|
|
24
|
+
order: 200
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: "Notifications",
|
|
28
|
+
value: "notifications",
|
|
29
|
+
component: AccountSettingsNotificationsSection,
|
|
30
|
+
usesSharedRuntime: true,
|
|
31
|
+
order: 300
|
|
32
|
+
}
|
|
20
33
|
];
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
nextSections.push(
|
|
35
|
+
for (const entry of extensionSections) {
|
|
36
|
+
nextSections.push(entry);
|
|
24
37
|
}
|
|
25
38
|
|
|
26
|
-
return Object.freeze(
|
|
39
|
+
return Object.freeze(
|
|
40
|
+
nextSections.sort((left, right) => {
|
|
41
|
+
const orderDelta = Number(left.order || 0) - Number(right.order || 0);
|
|
42
|
+
if (orderDelta !== 0) {
|
|
43
|
+
return orderDelta;
|
|
44
|
+
}
|
|
45
|
+
return String(left.value || "").localeCompare(String(right.value || ""));
|
|
46
|
+
})
|
|
47
|
+
);
|
|
27
48
|
});
|
|
28
49
|
const sectionValues = computed(() => Object.freeze(sections.value.map((section) => section.value)));
|
|
29
50
|
|
|
@@ -105,20 +126,11 @@ const activeTab = computed({
|
|
|
105
126
|
|
|
106
127
|
<v-col cols="12" md="9" lg="10">
|
|
107
128
|
<v-window v-model="activeTab" :touch="false" class="settings-sections-window">
|
|
108
|
-
<v-window-item value="
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<AccountSettingsPreferencesSection :runtime="runtime" />
|
|
114
|
-
</v-window-item>
|
|
115
|
-
|
|
116
|
-
<v-window-item value="notifications">
|
|
117
|
-
<AccountSettingsNotificationsSection :runtime="runtime" />
|
|
118
|
-
</v-window-item>
|
|
119
|
-
|
|
120
|
-
<v-window-item value="invites">
|
|
121
|
-
<AccountSettingsInvitesSection :runtime="runtime" />
|
|
129
|
+
<v-window-item v-for="section in sections" :key="section.value" :value="section.value">
|
|
130
|
+
<component
|
|
131
|
+
:is="section.component"
|
|
132
|
+
v-bind="section.usesSharedRuntime ? { runtime } : undefined"
|
|
133
|
+
/>
|
|
122
134
|
</v-window-item>
|
|
123
135
|
</v-window>
|
|
124
136
|
</v-col>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG,
|
|
5
|
+
resolveAccountSettingsSections
|
|
6
|
+
} from "../src/client/account-settings/sections.js";
|
|
7
|
+
import { bootUsersWebClientProvider } from "../src/client/providers/bootUsersWebClientProvider.js";
|
|
8
|
+
|
|
9
|
+
test("resolveAccountSettingsSections normalizes, deduplicates, and sorts tagged account section entries", () => {
|
|
10
|
+
const SectionA = {};
|
|
11
|
+
const SectionB = {};
|
|
12
|
+
|
|
13
|
+
const resolved = resolveAccountSettingsSections([
|
|
14
|
+
{ value: "notifications", title: "Ignore duplicate", component: SectionA, order: 999 },
|
|
15
|
+
{ value: "invites", title: "Invites", component: SectionA, order: 400 },
|
|
16
|
+
{ value: "profile", title: "Broken missing component" },
|
|
17
|
+
{ value: "security", title: "Security", component: SectionB, order: 350 },
|
|
18
|
+
{ value: "invites", title: "Second duplicate", component: SectionB, order: 100 }
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
assert.deepEqual(
|
|
22
|
+
resolved.map((entry) => ({ value: entry.value, title: entry.title, order: entry.order })),
|
|
23
|
+
[
|
|
24
|
+
{ value: "security", title: "Security", order: 350 },
|
|
25
|
+
{ value: "invites", title: "Invites", order: 400 }
|
|
26
|
+
]
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("bootUsersWebClientProvider provides normalized account section extensions", async () => {
|
|
31
|
+
const SectionComponent = {};
|
|
32
|
+
const provided = new Map();
|
|
33
|
+
let resolvedTagName = "";
|
|
34
|
+
|
|
35
|
+
await bootUsersWebClientProvider({
|
|
36
|
+
make(token) {
|
|
37
|
+
if (token === "jskit.client.vue.app") {
|
|
38
|
+
return {
|
|
39
|
+
provide(key, value) {
|
|
40
|
+
provided.set(key, value);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`Unexpected token: ${token}`);
|
|
45
|
+
},
|
|
46
|
+
resolveTag(tagName) {
|
|
47
|
+
resolvedTagName = tagName;
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
value: "invites",
|
|
51
|
+
title: "Invites",
|
|
52
|
+
component: SectionComponent,
|
|
53
|
+
order: 400,
|
|
54
|
+
usesSharedRuntime: false
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
assert.equal(resolvedTagName, ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG);
|
|
61
|
+
const [providedSections] = [...provided.values()];
|
|
62
|
+
assert.equal(Array.isArray(providedSections), true);
|
|
63
|
+
assert.deepEqual(
|
|
64
|
+
providedSections.map((entry) => ({
|
|
65
|
+
value: entry.value,
|
|
66
|
+
title: entry.title,
|
|
67
|
+
order: entry.order,
|
|
68
|
+
usesSharedRuntime: entry.usesSharedRuntime
|
|
69
|
+
})),
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
value: "invites",
|
|
73
|
+
title: "Invites",
|
|
74
|
+
order: 400,
|
|
75
|
+
usesSharedRuntime: false
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
);
|
|
79
|
+
});
|
|
@@ -15,13 +15,13 @@ test("users-web exports are explicit and aligned with production/template usage"
|
|
|
15
15
|
packageId: "@jskit-ai/users-web",
|
|
16
16
|
requiredExports: [
|
|
17
17
|
"./client",
|
|
18
|
+
"./client/account-settings/sections",
|
|
18
19
|
"./client/composables/useAddEdit",
|
|
19
20
|
"./client/composables/useList",
|
|
20
21
|
"./client/composables/useView",
|
|
21
22
|
"./client/composables/useCrudAddEdit",
|
|
22
23
|
"./client/composables/useCrudList",
|
|
23
|
-
"./client/composables/useCrudView"
|
|
24
|
-
"./client/support/menuLinkTarget"
|
|
24
|
+
"./client/composables/useCrudView"
|
|
25
25
|
]
|
|
26
26
|
});
|
|
27
27
|
|
|
@@ -2,7 +2,7 @@ import test from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { computed } from "vue";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
isScopedOwnershipFilter,
|
|
6
6
|
normalizeOwnershipFilter,
|
|
7
7
|
resolveApiSuffix,
|
|
8
8
|
resolveResourceMessages
|
|
@@ -49,9 +49,9 @@ test("normalizeOwnershipFilter accepts users visibility levels", () => {
|
|
|
49
49
|
assert.equal(normalizeOwnershipFilter("workspace_user"), "workspace_user");
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
test("
|
|
53
|
-
assert.equal(
|
|
54
|
-
assert.equal(
|
|
55
|
-
assert.equal(
|
|
56
|
-
assert.equal(
|
|
52
|
+
test("isScopedOwnershipFilter only matches scoped ownership levels", () => {
|
|
53
|
+
assert.equal(isScopedOwnershipFilter("workspace"), true);
|
|
54
|
+
assert.equal(isScopedOwnershipFilter("workspace_user"), true);
|
|
55
|
+
assert.equal(isScopedOwnershipFilter("public"), false);
|
|
56
|
+
assert.equal(isScopedOwnershipFilter("user"), false);
|
|
57
57
|
});
|