@jskit-ai/users-web 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/package.descriptor.mjs +507 -0
  2. package/package.json +31 -0
  3. package/src/client/components/ConsoleSettingsClientElement.vue +24 -0
  4. package/src/client/components/MembersAdminClientElement.vue +404 -0
  5. package/src/client/components/ProfileClientElement.vue +242 -0
  6. package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +39 -0
  7. package/src/client/components/UsersShellMenuLinkItem.vue +140 -0
  8. package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +87 -0
  9. package/src/client/components/UsersWorkspaceMembersMenuItem.vue +36 -0
  10. package/src/client/components/UsersWorkspacePermissionMenuItem.vue +90 -0
  11. package/src/client/components/UsersWorkspaceSelector.vue +237 -0
  12. package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +39 -0
  13. package/src/client/components/UsersWorkspaceToolsWidget.vue +23 -0
  14. package/src/client/components/WorkspaceMembersClientElement.vue +663 -0
  15. package/src/client/components/WorkspaceSettingsClientElement.vue +230 -0
  16. package/src/client/components/WorkspacesClientElement.vue +514 -0
  17. package/src/client/composables/accountSettingsAvatarUploadRuntime.js +241 -0
  18. package/src/client/composables/accountSettingsInvitesRuntime.js +88 -0
  19. package/src/client/composables/accountSettingsRuntimeConstants.js +77 -0
  20. package/src/client/composables/accountSettingsRuntimeHelpers.js +75 -0
  21. package/src/client/composables/errorMessageHelpers.js +66 -0
  22. package/src/client/composables/internal/useOperationScope.js +144 -0
  23. package/src/client/composables/modelStateHelpers.js +49 -0
  24. package/src/client/composables/operationUiHelpers.js +121 -0
  25. package/src/client/composables/operationValidationHelpers.js +52 -0
  26. package/src/client/composables/refValueHelpers.js +19 -0
  27. package/src/client/composables/scopeHelpers.js +145 -0
  28. package/src/client/composables/useAccess.js +109 -0
  29. package/src/client/composables/useAccountSettingsRuntime.js +533 -0
  30. package/src/client/composables/useAddEdit.js +135 -0
  31. package/src/client/composables/useAddEditCore.js +137 -0
  32. package/src/client/composables/useBootstrapQuery.js +52 -0
  33. package/src/client/composables/useCommand.js +112 -0
  34. package/src/client/composables/useCommandCore.js +130 -0
  35. package/src/client/composables/useEndpointResource.js +104 -0
  36. package/src/client/composables/useFieldErrorBag.js +61 -0
  37. package/src/client/composables/useList.js +85 -0
  38. package/src/client/composables/useListCore.js +65 -0
  39. package/src/client/composables/usePagedCollection.js +125 -0
  40. package/src/client/composables/usePaths.js +108 -0
  41. package/src/client/composables/useRealtimeQueryInvalidation.js +105 -0
  42. package/src/client/composables/useScopeRuntime.js +107 -0
  43. package/src/client/composables/useSurfaceRouteContext.js +31 -0
  44. package/src/client/composables/useUiFeedback.js +96 -0
  45. package/src/client/composables/useView.js +89 -0
  46. package/src/client/composables/useViewCore.js +104 -0
  47. package/src/client/composables/useWorkspaceRouteContext.js +28 -0
  48. package/src/client/composables/useWorkspaceSurfaceId.js +43 -0
  49. package/src/client/index.js +7 -0
  50. package/src/client/lib/bootstrap.js +95 -0
  51. package/src/client/lib/httpClient.js +67 -0
  52. package/src/client/lib/menuIcons.js +192 -0
  53. package/src/client/lib/permissions.js +34 -0
  54. package/src/client/lib/profileSurfaceMenuLinks.js +142 -0
  55. package/src/client/lib/surfaceAccessPolicy.js +350 -0
  56. package/src/client/lib/theme.js +99 -0
  57. package/src/client/lib/workspaceLinkResolver.js +207 -0
  58. package/src/client/lib/workspaceSurfaceContext.js +82 -0
  59. package/src/client/lib/workspaceSurfacePaths.js +163 -0
  60. package/src/client/providers/UsersWebClientProvider.js +85 -0
  61. package/src/client/runtime/bootstrapPlacementRouteGuards.js +371 -0
  62. package/src/client/runtime/bootstrapPlacementRuntime.js +413 -0
  63. package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +32 -0
  64. package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +157 -0
  65. package/src/client/support/contractGuards.js +34 -0
  66. package/src/client/support/realtimeWorkspace.js +12 -0
  67. package/src/client/support/runtimeNormalization.js +27 -0
  68. package/src/client/support/workspaceQueryKeys.js +15 -0
  69. package/templates/packages/main/src/client/components/AccountPendingInvitesCue.vue +162 -0
  70. package/templates/src/components/WorkspaceNotFoundCard.vue +33 -0
  71. package/templates/src/components/account/settings/AccountSettingsClientElement.vue +153 -0
  72. package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +77 -0
  73. package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +55 -0
  74. package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +125 -0
  75. package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +94 -0
  76. package/templates/src/composables/useWorkspaceNotFoundState.js +48 -0
  77. package/templates/src/pages/account/index.vue +17 -0
  78. package/templates/src/pages/admin/members/index.vue +7 -0
  79. package/templates/src/pages/admin/workspace/settings/index.vue +16 -0
  80. package/templates/src/pages/console/settings/index.vue +16 -0
  81. package/templates/src/surfaces/admin/index.vue +29 -0
  82. package/templates/src/surfaces/admin/root.vue +20 -0
  83. package/templates/src/surfaces/app/index.vue +27 -0
  84. package/templates/src/surfaces/app/root.vue +20 -0
  85. package/test/bootstrap.test.js +38 -0
  86. package/test/bootstrapPlacementRuntime.test.js +991 -0
  87. package/test/errorMessageHelpers.test.js +28 -0
  88. package/test/exportsContract.test.js +39 -0
  89. package/test/menuIcons.test.js +33 -0
  90. package/test/permissions.test.js +35 -0
  91. package/test/profileSurfaceMenuLinks.test.js +207 -0
  92. package/test/refValueHelpers.test.js +14 -0
  93. package/test/scopeHelpers.test.js +57 -0
  94. package/test/surfaceAccessPolicy.test.js +129 -0
  95. package/test/theme.test.js +95 -0
  96. package/test/workspaceLinkResolver.test.js +61 -0
  97. package/test/workspaceSurfacePaths.test.js +39 -0
@@ -0,0 +1,533 @@
1
+ import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
2
+ import { useQueryClient } from "@tanstack/vue-query";
3
+ import { useTheme } from "vuetify";
4
+ import { useRoute, useRouter } from "vue-router";
5
+ import {
6
+ useWebPlacementContext,
7
+ resolveSurfaceNavigationTargetFromPlacementContext
8
+ } from "@jskit-ai/shell-web/client/placement";
9
+ import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
10
+ import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
11
+ import { userProfileResource } from "@jskit-ai/users-core/shared/resources/userProfileResource";
12
+ import { userSettingsResource } from "@jskit-ai/users-core/shared/resources/userSettingsResource";
13
+ import { USERS_ROUTE_VISIBILITY_PUBLIC } from "@jskit-ai/users-core/shared/support/usersVisibility";
14
+ import {
15
+ resolveThemeNameForPreference,
16
+ setVuetifyThemeName
17
+ } from "../lib/theme.js";
18
+ import {
19
+ useWorkspaceSurfaceId
20
+ } from "./useWorkspaceSurfaceId.js";
21
+ import { useAddEdit } from "./useAddEdit.js";
22
+ import { useCommand } from "./useCommand.js";
23
+ import { useView } from "./useView.js";
24
+ import { usePaths } from "./usePaths.js";
25
+ import {
26
+ ACCOUNT_SETTINGS_CHANGED_EVENT,
27
+ WORKSPACE_PENDING_INVITATIONS_CHANGED_EVENT
28
+ } from "@jskit-ai/users-core/shared/events/usersEvents";
29
+ import { resolveAccountSettingsPathFromPlacementContext } from "../lib/workspaceSurfacePaths.js";
30
+ import {
31
+ ACCOUNT_SETTINGS_DEFAULTS,
32
+ AVATAR_DEFAULT_SIZE,
33
+ AVATAR_SIZE_OPTIONS,
34
+ CURRENCY_OPTIONS,
35
+ DATE_FORMAT_OPTIONS,
36
+ LOCALE_OPTIONS,
37
+ NUMBER_FORMAT_OPTIONS,
38
+ THEME_OPTIONS,
39
+ TIME_ZONE_OPTIONS
40
+ } from "./accountSettingsRuntimeConstants.js";
41
+ import {
42
+ normalizeAvatarSize,
43
+ normalizePendingInvite,
44
+ normalizeReturnToPath,
45
+ normalizeSettingsPayload,
46
+ resolveAllowedReturnToOrigins
47
+ } from "./accountSettingsRuntimeHelpers.js";
48
+ import { createAccountSettingsAvatarUploadRuntime } from "./accountSettingsAvatarUploadRuntime.js";
49
+ import { createAccountSettingsInvitesRuntime } from "./accountSettingsInvitesRuntime.js";
50
+
51
+ function useAccountSettingsRuntime() {
52
+ const route = useRoute();
53
+ const router = useRouter();
54
+ const { context: placementContext } = useWebPlacementContext();
55
+ const paths = usePaths();
56
+ const queryClient = useQueryClient();
57
+ const errorRuntime = useShellWebErrorRuntime();
58
+ const vuetifyTheme = useTheme();
59
+
60
+ const accountSettingsQueryKey = ["users-web", "settings", "account"];
61
+ const pendingInvitesQueryKey = ["users-web", "settings", "pending-invites"];
62
+ const sessionQueryKey = Object.freeze(["users-web", "session", "csrf"]);
63
+ const OWNERSHIP_PUBLIC = USERS_ROUTE_VISIBILITY_PUBLIC;
64
+
65
+ const accountSettingsPath = computed(() => resolveAccountSettingsPathFromPlacementContext(placementContext.value));
66
+ const allowedReturnToOrigins = computed(() => resolveAllowedReturnToOrigins(placementContext.value));
67
+ const backTarget = computed(() =>
68
+ normalizeReturnToPath(route?.query?.returnTo, {
69
+ fallback: "/",
70
+ accountSettingsPath: accountSettingsPath.value,
71
+ allowedOrigins: allowedReturnToOrigins.value
72
+ })
73
+ );
74
+ const backNavigationTarget = computed(() =>
75
+ resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
76
+ path: backTarget.value
77
+ })
78
+ );
79
+
80
+ const profileForm = reactive({
81
+ displayName: "",
82
+ email: ""
83
+ });
84
+
85
+ const preferencesForm = reactive({
86
+ theme: ACCOUNT_SETTINGS_DEFAULTS.preferences.theme,
87
+ locale: ACCOUNT_SETTINGS_DEFAULTS.preferences.locale,
88
+ timeZone: ACCOUNT_SETTINGS_DEFAULTS.preferences.timeZone,
89
+ dateFormat: ACCOUNT_SETTINGS_DEFAULTS.preferences.dateFormat,
90
+ numberFormat: ACCOUNT_SETTINGS_DEFAULTS.preferences.numberFormat,
91
+ currencyCode: ACCOUNT_SETTINGS_DEFAULTS.preferences.currencyCode,
92
+ avatarSize: ACCOUNT_SETTINGS_DEFAULTS.preferences.avatarSize
93
+ });
94
+
95
+ const notificationsForm = reactive({
96
+ productUpdates: ACCOUNT_SETTINGS_DEFAULTS.notifications.productUpdates,
97
+ accountActivity: ACCOUNT_SETTINGS_DEFAULTS.notifications.accountActivity,
98
+ securityAlerts: ACCOUNT_SETTINGS_DEFAULTS.notifications.securityAlerts
99
+ });
100
+
101
+ const profileAvatar = reactive({
102
+ uploadedUrl: null,
103
+ gravatarUrl: "",
104
+ effectiveUrl: "",
105
+ hasUploadedAvatar: false,
106
+ size: ACCOUNT_SETTINGS_DEFAULTS.preferences.avatarSize,
107
+ version: null
108
+ });
109
+
110
+ const pendingInvitesModel = reactive({
111
+ pendingInvites: [],
112
+ workspaceInvitesEnabled: false
113
+ });
114
+
115
+ const selectedAvatarFileName = ref("");
116
+ const inviteAction = ref({
117
+ token: "",
118
+ decision: ""
119
+ });
120
+ const redeemInviteModel = reactive({
121
+ token: "",
122
+ decision: ""
123
+ });
124
+
125
+ const profileInitials = computed(() => {
126
+ const source = String(profileForm.displayName || profileForm.email || "U").trim();
127
+ return source.slice(0, 2).toUpperCase() || "U";
128
+ });
129
+
130
+ function reportAccountFeedback({
131
+ message,
132
+ severity = "error",
133
+ channel = "banner",
134
+ dedupeKey = ""
135
+ } = {}) {
136
+ const normalizedMessage = String(message || "").trim();
137
+ if (!normalizedMessage) {
138
+ return;
139
+ }
140
+
141
+ errorRuntime.report({
142
+ source: "users-web.account-settings-runtime",
143
+ message: normalizedMessage,
144
+ severity,
145
+ channel,
146
+ dedupeKey: dedupeKey || `users-web.account-settings-runtime:${severity}:${normalizedMessage}`,
147
+ dedupeWindowMs: 3000
148
+ });
149
+ }
150
+
151
+ function applyThemePreference(themePreference) {
152
+ const themeName = resolveThemeNameForPreference(themePreference);
153
+ setVuetifyThemeName(vuetifyTheme, themeName);
154
+ }
155
+
156
+ function applyAvatarData(avatar) {
157
+ const nextAvatar = avatar && typeof avatar === "object" ? avatar : {};
158
+
159
+ profileAvatar.uploadedUrl = nextAvatar.uploadedUrl ? String(nextAvatar.uploadedUrl) : null;
160
+ profileAvatar.gravatarUrl = String(nextAvatar.gravatarUrl || "");
161
+ profileAvatar.effectiveUrl = String(nextAvatar.effectiveUrl || profileAvatar.gravatarUrl || "");
162
+ profileAvatar.hasUploadedAvatar = Boolean(nextAvatar.hasUploadedAvatar);
163
+ profileAvatar.size = normalizeAvatarSize(nextAvatar.size || preferencesForm.avatarSize || AVATAR_DEFAULT_SIZE);
164
+ profileAvatar.version = nextAvatar.version == null ? null : String(nextAvatar.version);
165
+ }
166
+
167
+ function applySettingsData(payload) {
168
+ const data = normalizeSettingsPayload(payload);
169
+
170
+ profileForm.displayName = String(data.profile?.displayName || "");
171
+ profileForm.email = String(data.profile?.email || "");
172
+ applyAvatarData(data.profile?.avatar);
173
+
174
+ preferencesForm.theme = String(data.preferences?.theme || ACCOUNT_SETTINGS_DEFAULTS.preferences.theme);
175
+ preferencesForm.locale = String(data.preferences?.locale || ACCOUNT_SETTINGS_DEFAULTS.preferences.locale);
176
+ preferencesForm.timeZone = String(data.preferences?.timeZone || ACCOUNT_SETTINGS_DEFAULTS.preferences.timeZone);
177
+ preferencesForm.dateFormat = String(data.preferences?.dateFormat || ACCOUNT_SETTINGS_DEFAULTS.preferences.dateFormat);
178
+ preferencesForm.numberFormat = String(data.preferences?.numberFormat || ACCOUNT_SETTINGS_DEFAULTS.preferences.numberFormat);
179
+ preferencesForm.currencyCode = String(data.preferences?.currencyCode || ACCOUNT_SETTINGS_DEFAULTS.preferences.currencyCode);
180
+ preferencesForm.avatarSize = normalizeAvatarSize(data.preferences?.avatarSize || ACCOUNT_SETTINGS_DEFAULTS.preferences.avatarSize);
181
+
182
+ notificationsForm.productUpdates = Boolean(data.notifications?.productUpdates);
183
+ notificationsForm.accountActivity = Boolean(data.notifications?.accountActivity);
184
+ notificationsForm.securityAlerts = ACCOUNT_SETTINGS_DEFAULTS.notifications.securityAlerts;
185
+
186
+ applyThemePreference(preferencesForm.theme);
187
+ }
188
+
189
+ const avatarUploadRuntime = createAccountSettingsAvatarUploadRuntime({
190
+ queryClient,
191
+ sessionQueryKey,
192
+ accountSettingsQueryKey,
193
+ selectedAvatarFileName,
194
+ applySettingsData,
195
+ reportAccountFeedback
196
+ });
197
+
198
+ const mapAccountSettingsPayload = (_model, payload = {}) => {
199
+ applySettingsData(payload);
200
+ };
201
+
202
+ const settingsView = useView({
203
+ ownershipFilter: OWNERSHIP_PUBLIC,
204
+ apiSuffix: "/settings",
205
+ queryKeyFactory: () => accountSettingsQueryKey,
206
+ realtime: {
207
+ event: ACCOUNT_SETTINGS_CHANGED_EVENT
208
+ },
209
+ fallbackLoadError: "Unable to load settings.",
210
+ mapLoadedToModel: mapAccountSettingsPayload
211
+ });
212
+
213
+ const pendingInvitesView = useView({
214
+ ownershipFilter: OWNERSHIP_PUBLIC,
215
+ apiSuffix: "/bootstrap",
216
+ queryKeyFactory: () => pendingInvitesQueryKey,
217
+ realtime: {
218
+ event: WORKSPACE_PENDING_INVITATIONS_CHANGED_EVENT
219
+ },
220
+ fallbackLoadError: "Unable to load invitations.",
221
+ model: pendingInvitesModel,
222
+ mapLoadedToModel: (model, payload = {}) => {
223
+ model.workspaceInvitesEnabled = payload?.app?.features?.workspaceInvites === true;
224
+ model.pendingInvites = model.workspaceInvitesEnabled
225
+ ? (Array.isArray(payload?.pendingInvites) ? payload.pendingInvites : [])
226
+ .map(normalizePendingInvite)
227
+ .filter(Boolean)
228
+ : [];
229
+ }
230
+ });
231
+
232
+ const redeemInviteCommand = useCommand({
233
+ ownershipFilter: OWNERSHIP_PUBLIC,
234
+ apiSuffix: "/workspace/invitations/redeem",
235
+ writeMethod: "POST",
236
+ fallbackRunError: "Unable to respond to invitation.",
237
+ suppressSuccessMessage: true,
238
+ model: redeemInviteModel,
239
+ buildRawPayload: (model) => ({
240
+ token: String(model.token || "").trim(),
241
+ decision: String(model.decision || "").trim().toLowerCase()
242
+ }),
243
+ messages: {
244
+ error: "Unable to respond to invitation."
245
+ }
246
+ });
247
+
248
+ const profileAddEdit = useAddEdit({
249
+ ownershipFilter: OWNERSHIP_PUBLIC,
250
+ resource: userProfileResource,
251
+ apiSuffix: "/settings/profile",
252
+ queryKeyFactory: () => accountSettingsQueryKey,
253
+ readEnabled: false,
254
+ writeMethod: "PATCH",
255
+ fallbackSaveError: "Unable to update profile.",
256
+ fieldErrorKeys: ["displayName"],
257
+ model: profileForm,
258
+ mapLoadedToModel: mapAccountSettingsPayload,
259
+ parseInput: (rawPayload) =>
260
+ validateOperationSection({
261
+ operation: userProfileResource.operations.patch,
262
+ section: "bodyValidator",
263
+ value: rawPayload
264
+ }),
265
+ buildRawPayload: (model) => ({
266
+ displayName: String(model.displayName || "").trim()
267
+ }),
268
+ messages: {
269
+ saveSuccess: "Profile updated.",
270
+ saveError: "Unable to update profile."
271
+ }
272
+ });
273
+
274
+ const avatarDeleteCommand = useCommand({
275
+ ownershipFilter: OWNERSHIP_PUBLIC,
276
+ apiSuffix: "/settings/profile/avatar",
277
+ writeMethod: "DELETE",
278
+ fallbackRunError: "Unable to remove avatar.",
279
+ model: profileForm,
280
+ onRunSuccess: (payload, { queryClient: commandQueryClient }) => {
281
+ applySettingsData(payload);
282
+ commandQueryClient.setQueryData(accountSettingsQueryKey, payload);
283
+ },
284
+ messages: {
285
+ success: "Avatar removed.",
286
+ error: "Unable to remove avatar."
287
+ }
288
+ });
289
+
290
+ const preferencesAddEdit = useAddEdit({
291
+ ownershipFilter: OWNERSHIP_PUBLIC,
292
+ resource: userSettingsResource,
293
+ apiSuffix: "/settings/preferences",
294
+ queryKeyFactory: () => accountSettingsQueryKey,
295
+ readEnabled: false,
296
+ writeMethod: "PATCH",
297
+ fallbackSaveError: "Unable to update preferences.",
298
+ fieldErrorKeys: ["theme", "locale", "timeZone", "dateFormat", "numberFormat", "currencyCode", "avatarSize"],
299
+ model: preferencesForm,
300
+ mapLoadedToModel: mapAccountSettingsPayload,
301
+ parseInput: (rawPayload) =>
302
+ validateOperationSection({
303
+ operation: userSettingsResource.operations.preferencesUpdate,
304
+ section: "bodyValidator",
305
+ value: rawPayload
306
+ }),
307
+ buildRawPayload: (model) => ({
308
+ theme: model.theme,
309
+ locale: model.locale,
310
+ timeZone: model.timeZone,
311
+ dateFormat: model.dateFormat,
312
+ numberFormat: model.numberFormat,
313
+ currencyCode: model.currencyCode,
314
+ avatarSize: Number(model.avatarSize)
315
+ }),
316
+ messages: {
317
+ saveSuccess: "Preferences updated.",
318
+ saveError: "Unable to update preferences."
319
+ }
320
+ });
321
+
322
+ const notificationsAddEdit = useAddEdit({
323
+ ownershipFilter: OWNERSHIP_PUBLIC,
324
+ resource: userSettingsResource,
325
+ apiSuffix: "/settings/notifications",
326
+ queryKeyFactory: () => accountSettingsQueryKey,
327
+ readEnabled: false,
328
+ writeMethod: "PATCH",
329
+ fallbackSaveError: "Unable to update notifications.",
330
+ model: notificationsForm,
331
+ mapLoadedToModel: mapAccountSettingsPayload,
332
+ parseInput: (rawPayload) =>
333
+ validateOperationSection({
334
+ operation: userSettingsResource.operations.notificationsUpdate,
335
+ section: "bodyValidator",
336
+ value: rawPayload
337
+ }),
338
+ buildRawPayload: (model) => ({
339
+ productUpdates: Boolean(model.productUpdates),
340
+ accountActivity: Boolean(model.accountActivity),
341
+ securityAlerts: true
342
+ }),
343
+ messages: {
344
+ saveSuccess: "Notification settings updated.",
345
+ saveError: "Unable to update notifications."
346
+ }
347
+ });
348
+
349
+ const loadingSettings = computed(() => Boolean(settingsView.isLoading.value));
350
+ const refreshingSettings = computed(() => Boolean(settingsView.isRefetching.value));
351
+ const invitesAvailable = computed(() => pendingInvitesModel.workspaceInvitesEnabled === true);
352
+ const loadingInvites = computed(() => Boolean(pendingInvitesView.isLoading.value));
353
+ const refreshingInvites = computed(() => Boolean(pendingInvitesView.isRefetching.value));
354
+ const pendingInvites = computed(() => {
355
+ return Array.isArray(pendingInvitesModel.pendingInvites) ? pendingInvitesModel.pendingInvites : [];
356
+ });
357
+ const isResolvingInvite = computed(() => Boolean(redeemInviteCommand.isRunning.value));
358
+
359
+ const { workspaceSurfaceId } = useWorkspaceSurfaceId({
360
+ route,
361
+ placementContext
362
+ });
363
+
364
+ function workspaceHomePath(workspaceSlug) {
365
+ const normalizedSlug = String(workspaceSlug || "").trim();
366
+ if (!normalizedSlug || !workspaceSurfaceId.value) {
367
+ return "";
368
+ }
369
+
370
+ return paths.page("/", {
371
+ surface: workspaceSurfaceId.value,
372
+ workspaceSlug: normalizedSlug,
373
+ mode: "workspace"
374
+ });
375
+ }
376
+
377
+ async function submitProfile() {
378
+ await profileAddEdit.submit();
379
+ }
380
+
381
+ async function openWorkspace(workspaceSlug) {
382
+ const targetPath = workspaceHomePath(workspaceSlug);
383
+ if (!targetPath) {
384
+ reportAccountFeedback({
385
+ message: "Workspace surface is not configured.",
386
+ severity: "error",
387
+ channel: "banner",
388
+ dedupeKey: "users-web.account-settings-runtime:workspace-surface-missing"
389
+ });
390
+ return;
391
+ }
392
+
393
+ try {
394
+ const navigationTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
395
+ path: targetPath,
396
+ surfaceId: workspaceSurfaceId.value
397
+ });
398
+ if (navigationTarget.sameOrigin) {
399
+ await router.push(navigationTarget.href);
400
+ } else if (typeof window === "object" && window?.location && typeof window.location.assign === "function") {
401
+ window.location.assign(navigationTarget.href);
402
+ } else {
403
+ throw new Error("Cross-origin navigation is unavailable in this environment.");
404
+ }
405
+ } catch (error) {
406
+ reportAccountFeedback({
407
+ message: String(error?.message || "Unable to open workspace."),
408
+ severity: "error",
409
+ channel: "banner",
410
+ dedupeKey: `users-web.account-settings-runtime:open-workspace:${String(workspaceSlug || "").trim()}`
411
+ });
412
+ }
413
+ }
414
+
415
+ const invitesRuntime = createAccountSettingsInvitesRuntime({
416
+ invitesAvailable,
417
+ isResolvingInvite,
418
+ inviteAction,
419
+ redeemInviteModel,
420
+ redeemInviteCommand,
421
+ pendingInvites,
422
+ pendingInvitesModel,
423
+ pendingInvitesView,
424
+ openWorkspace,
425
+ reportAccountFeedback
426
+ });
427
+
428
+ function acceptInvite(invite) {
429
+ return invitesRuntime.accept(invite);
430
+ }
431
+
432
+ function refuseInvite(invite) {
433
+ return invitesRuntime.refuse(invite);
434
+ }
435
+
436
+ function openAvatarEditor() {
437
+ avatarUploadRuntime.openEditor();
438
+ }
439
+
440
+ async function submitAvatarDelete() {
441
+ try {
442
+ await avatarDeleteCommand.run();
443
+ } catch {
444
+ // Error feedback is already handled in useCommand.
445
+ }
446
+ }
447
+
448
+ async function submitPreferences() {
449
+ await preferencesAddEdit.submit();
450
+ }
451
+
452
+ async function submitNotifications() {
453
+ await notificationsAddEdit.submit();
454
+ }
455
+
456
+ watch(
457
+ () => preferencesForm.avatarSize,
458
+ (nextSize) => {
459
+ profileAvatar.size = normalizeAvatarSize(nextSize || AVATAR_DEFAULT_SIZE);
460
+ },
461
+ { immediate: true }
462
+ );
463
+
464
+ onMounted(() => {
465
+ avatarUploadRuntime.setup();
466
+ });
467
+
468
+ onBeforeUnmount(() => {
469
+ avatarUploadRuntime.destroy();
470
+ });
471
+
472
+ const profile = Object.freeze({
473
+ form: profileForm,
474
+ avatar: profileAvatar,
475
+ initials: profileInitials,
476
+ selectedAvatarFileName,
477
+ fieldErrors: profileAddEdit.fieldErrors,
478
+ isSaving: profileAddEdit.isSaving,
479
+ isDeletingAvatar: avatarDeleteCommand.isRunning,
480
+ isRefreshing: refreshingSettings,
481
+ submit: submitProfile,
482
+ openAvatarEditor,
483
+ removeAvatar: submitAvatarDelete
484
+ });
485
+
486
+ const preferences = Object.freeze({
487
+ form: preferencesForm,
488
+ fieldErrors: preferencesAddEdit.fieldErrors,
489
+ isSaving: preferencesAddEdit.isSaving,
490
+ isRefreshing: refreshingSettings,
491
+ options: Object.freeze({
492
+ theme: THEME_OPTIONS,
493
+ locale: LOCALE_OPTIONS,
494
+ timeZone: TIME_ZONE_OPTIONS,
495
+ dateFormat: DATE_FORMAT_OPTIONS,
496
+ numberFormat: NUMBER_FORMAT_OPTIONS,
497
+ currency: CURRENCY_OPTIONS,
498
+ avatarSize: AVATAR_SIZE_OPTIONS
499
+ }),
500
+ submit: submitPreferences
501
+ });
502
+
503
+ const notifications = Object.freeze({
504
+ form: notificationsForm,
505
+ isSaving: notificationsAddEdit.isSaving,
506
+ isRefreshing: refreshingSettings,
507
+ submit: submitNotifications
508
+ });
509
+
510
+ const invites = Object.freeze({
511
+ isAvailable: invitesAvailable,
512
+ items: pendingInvites,
513
+ isLoading: loadingInvites,
514
+ isRefetching: refreshingInvites,
515
+ isResolving: isResolvingInvite,
516
+ action: inviteAction,
517
+ accept: acceptInvite,
518
+ refuse: refuseInvite
519
+ });
520
+
521
+ return Object.freeze({
522
+ backTarget,
523
+ backNavigationTarget,
524
+ loadingSettings,
525
+ refreshingSettings,
526
+ profile,
527
+ preferences,
528
+ notifications,
529
+ invites
530
+ });
531
+ }
532
+
533
+ export { useAccountSettingsRuntime };
@@ -0,0 +1,135 @@
1
+ import { computed, proxyRefs } from "vue";
2
+ import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
3
+ import { useAddEditCore } from "./useAddEditCore.js";
4
+ import { useEndpointResource } from "./useEndpointResource.js";
5
+ import { useOperationScope } from "./internal/useOperationScope.js";
6
+ import { useUiFeedback } from "./useUiFeedback.js";
7
+ import { useFieldErrorBag } from "./useFieldErrorBag.js";
8
+ import {
9
+ setupRouteChangeCleanup,
10
+ setupOperationErrorReporting
11
+ } from "./operationUiHelpers.js";
12
+ import {
13
+ resolveResourceMessages
14
+ } from "./scopeHelpers.js";
15
+
16
+ function useAddEdit({
17
+ ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
18
+ surfaceId = "",
19
+ access = "auto",
20
+ resource = null,
21
+ apiSuffix = "",
22
+ queryKeyFactory = null,
23
+ viewPermissions = [],
24
+ savePermissions = [],
25
+ readMethod = "GET",
26
+ readEnabled = true,
27
+ writeMethod = "PATCH",
28
+ placementSource = "users-web.add-edit",
29
+ fallbackLoadError = "Unable to load resource.",
30
+ fallbackSaveError = "Unable to save resource.",
31
+ fieldErrorKeys = [],
32
+ clearOnRouteChange = true,
33
+ model,
34
+ parseInput,
35
+ mapLoadedToModel,
36
+ buildRawPayload,
37
+ buildSavePayload,
38
+ onSaveSuccess,
39
+ messages = {},
40
+ realtime = null
41
+ } = {}) {
42
+ const operationScope = useOperationScope({
43
+ ownershipFilter,
44
+ surfaceId,
45
+ access,
46
+ placementSource,
47
+ apiSuffix,
48
+ model,
49
+ readEnabled,
50
+ queryKeyFactory,
51
+ permissionSets: {
52
+ view: viewPermissions,
53
+ save: savePermissions
54
+ },
55
+ realtime
56
+ });
57
+ const routeContext = operationScope.routeContext;
58
+ const effectiveMessages = {
59
+ ...resolveResourceMessages(resource, {
60
+ validation: "Fix invalid values and try again.",
61
+ saveSuccess: "Saved.",
62
+ saveError: "Unable to save."
63
+ }),
64
+ ...(messages && typeof messages === "object" ? messages : {})
65
+ };
66
+
67
+ const canView = operationScope.permissionGate("view");
68
+ const canSave = operationScope.permissionGate("save");
69
+
70
+ const endpointResource = useEndpointResource({
71
+ queryKey: operationScope.queryKey,
72
+ path: operationScope.apiPath,
73
+ enabled: operationScope.queryCanRun(canView),
74
+ readMethod,
75
+ writeMethod,
76
+ fallbackLoadError,
77
+ fallbackSaveError: String(fallbackSaveError || effectiveMessages.saveError || "Unable to save resource.")
78
+ });
79
+
80
+ const feedback = useUiFeedback({
81
+ source: `${placementSource}.feedback`
82
+ });
83
+ const fieldBag = useFieldErrorBag(fieldErrorKeys);
84
+
85
+ const addEdit = useAddEditCore({
86
+ model,
87
+ resource: endpointResource,
88
+ queryKey: operationScope.queryKey,
89
+ canSave,
90
+ fieldBag,
91
+ feedback,
92
+ parseInput,
93
+ mapLoadedToModel,
94
+ buildRawPayload,
95
+ buildSavePayload,
96
+ onSaveSuccess,
97
+ messages: effectiveMessages
98
+ });
99
+
100
+ setupRouteChangeCleanup({
101
+ enabled: clearOnRouteChange,
102
+ route: routeContext.route,
103
+ feedback,
104
+ fieldBag
105
+ });
106
+
107
+ const isInitialLoading = operationScope.isLoading(endpointResource.isInitialLoading);
108
+ const isFetching = operationScope.isLoading(endpointResource.isFetching);
109
+ const isRefetching = computed(() => Boolean(isFetching.value && !isInitialLoading.value));
110
+ const loadError = operationScope.loadError(endpointResource.loadError);
111
+ const isLoading = operationScope.isLoading(endpointResource.isLoading);
112
+ setupOperationErrorReporting({
113
+ source: `${placementSource}.load`,
114
+ loadError
115
+ });
116
+
117
+ return proxyRefs({
118
+ canView,
119
+ canSave,
120
+ loadError,
121
+ isInitialLoading,
122
+ isFetching,
123
+ isRefetching,
124
+ isLoading,
125
+ isSaving: addEdit.saving,
126
+ fieldErrors: addEdit.fieldErrors,
127
+ message: addEdit.message,
128
+ messageType: addEdit.messageType,
129
+ submit: addEdit.submit,
130
+ refresh: endpointResource.reload,
131
+ resource: endpointResource
132
+ });
133
+ }
134
+
135
+ export { useAddEdit };