@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,241 @@
1
+ import Uppy from "@uppy/core";
2
+ import Dashboard from "@uppy/dashboard";
3
+ import ImageEditor from "@uppy/image-editor";
4
+ import Compressor from "@uppy/compressor";
5
+ import XHRUpload from "@uppy/xhr-upload";
6
+ import "@uppy/core/css/style.min.css";
7
+ import "@uppy/dashboard/css/style.min.css";
8
+ import "@uppy/image-editor/css/style.min.css";
9
+ import { resolveFieldErrors } from "@jskit-ai/http-runtime/client";
10
+ import { usersWebHttpClient } from "../lib/httpClient.js";
11
+ import {
12
+ AVATAR_ALLOWED_MIME_TYPES,
13
+ AVATAR_MAX_UPLOAD_BYTES
14
+ } from "./accountSettingsRuntimeConstants.js";
15
+
16
+ function parseUploadResponse(xhr) {
17
+ if (!xhr.responseText) {
18
+ return {};
19
+ }
20
+
21
+ try {
22
+ return JSON.parse(xhr.responseText);
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+
28
+ function stopImageEditor(uppy) {
29
+ const imageEditor = uppy.getPlugin("ImageEditor");
30
+ if (imageEditor && typeof imageEditor.stop === "function") {
31
+ imageEditor.stop();
32
+ }
33
+ }
34
+
35
+ function createAccountSettingsAvatarUploadRuntime({
36
+ queryClient,
37
+ sessionQueryKey,
38
+ accountSettingsQueryKey,
39
+ selectedAvatarFileName,
40
+ applySettingsData,
41
+ reportAccountFeedback
42
+ } = {}) {
43
+ let avatarUppy = null;
44
+
45
+ async function resolveCsrfToken() {
46
+ const sessionPayload = await queryClient.fetchQuery({
47
+ queryKey: sessionQueryKey,
48
+ queryFn: () =>
49
+ usersWebHttpClient.request("/api/session", {
50
+ method: "GET"
51
+ }),
52
+ staleTime: 60_000
53
+ });
54
+
55
+ const csrfToken = String(sessionPayload?.csrfToken || "");
56
+ if (!csrfToken) {
57
+ throw new Error("Unable to prepare secure avatar upload request.");
58
+ }
59
+
60
+ return csrfToken;
61
+ }
62
+
63
+ function setup() {
64
+ if (typeof window === "undefined") {
65
+ return;
66
+ }
67
+
68
+ if (avatarUppy) {
69
+ return;
70
+ }
71
+
72
+ const uppy = new Uppy({
73
+ autoProceed: false,
74
+ restrictions: {
75
+ maxNumberOfFiles: 1,
76
+ allowedFileTypes: [...AVATAR_ALLOWED_MIME_TYPES],
77
+ maxFileSize: AVATAR_MAX_UPLOAD_BYTES
78
+ }
79
+ });
80
+
81
+ uppy.use(Dashboard, {
82
+ inline: false,
83
+ closeAfterFinish: false,
84
+ showProgressDetails: true,
85
+ proudlyDisplayPoweredByUppy: false,
86
+ hideUploadButton: false,
87
+ doneButtonHandler: () => {
88
+ const dashboard = uppy.getPlugin("Dashboard");
89
+ if (dashboard && typeof dashboard.closeModal === "function") {
90
+ dashboard.closeModal();
91
+ }
92
+ },
93
+ note: `Accepted: ${AVATAR_ALLOWED_MIME_TYPES.join(", ")}, max ${Math.floor(AVATAR_MAX_UPLOAD_BYTES / (1024 * 1024))}MB`
94
+ });
95
+
96
+ uppy.use(ImageEditor, {
97
+ quality: 0.9
98
+ });
99
+
100
+ uppy.use(Compressor, {
101
+ quality: 0.84,
102
+ limit: 1
103
+ });
104
+
105
+ uppy.use(XHRUpload, {
106
+ endpoint: "/api/settings/profile/avatar",
107
+ method: "POST",
108
+ formData: true,
109
+ fieldName: "avatar",
110
+ withCredentials: true,
111
+ onBeforeRequest: async (xhr) => {
112
+ const csrfToken = await resolveCsrfToken();
113
+ xhr.setRequestHeader("csrf-token", csrfToken);
114
+ },
115
+ getResponseData: parseUploadResponse
116
+ });
117
+
118
+ uppy.on("file-added", (file) => {
119
+ selectedAvatarFileName.value = String(file?.name || "");
120
+ });
121
+
122
+ uppy.on("file-removed", () => {
123
+ selectedAvatarFileName.value = "";
124
+ });
125
+
126
+ uppy.on("file-editor:complete", (file) => {
127
+ selectedAvatarFileName.value = String(file?.name || selectedAvatarFileName.value || "");
128
+ stopImageEditor(uppy);
129
+ });
130
+
131
+ uppy.on("file-editor:cancel", () => {
132
+ stopImageEditor(uppy);
133
+ });
134
+
135
+ uppy.on("dashboard:modal-closed", () => {
136
+ stopImageEditor(uppy);
137
+ });
138
+
139
+ uppy.on("upload-success", (_file, response) => {
140
+ const data = response?.body;
141
+ if (!data || typeof data !== "object") {
142
+ reportAccountFeedback({
143
+ message: "Avatar uploaded, but the response payload was invalid.",
144
+ severity: "error",
145
+ channel: "banner",
146
+ dedupeKey: "users-web.account-settings-runtime:avatar-upload-invalid-response"
147
+ });
148
+ return;
149
+ }
150
+
151
+ applySettingsData(data);
152
+ queryClient.setQueryData(accountSettingsQueryKey, data);
153
+
154
+ const dashboard = uppy.getPlugin("Dashboard");
155
+ if (dashboard && typeof dashboard.closeModal === "function") {
156
+ dashboard.closeModal();
157
+ }
158
+
159
+ reportAccountFeedback({
160
+ message: "Avatar uploaded.",
161
+ severity: "success",
162
+ channel: "snackbar",
163
+ dedupeKey: "users-web.account-settings-runtime:avatar-uploaded"
164
+ });
165
+ selectedAvatarFileName.value = "";
166
+ });
167
+
168
+ uppy.on("upload-error", (_file, error, response) => {
169
+ const body = response?.body && typeof response.body === "object" ? response.body : {};
170
+ const fieldErrors = resolveFieldErrors(body);
171
+
172
+ reportAccountFeedback({
173
+ message: String(fieldErrors.avatar || body?.error || error?.message || "Unable to upload avatar."),
174
+ severity: "error",
175
+ channel: "banner",
176
+ dedupeKey: "users-web.account-settings-runtime:avatar-upload-error"
177
+ });
178
+ });
179
+
180
+ uppy.on("restriction-failed", (_file, error) => {
181
+ reportAccountFeedback({
182
+ message: String(error?.message || "Selected avatar file does not meet upload restrictions."),
183
+ severity: "error",
184
+ channel: "banner",
185
+ dedupeKey: "users-web.account-settings-runtime:avatar-upload-restriction"
186
+ });
187
+ });
188
+
189
+ uppy.on("complete", (result) => {
190
+ const successfulCount = Array.isArray(result?.successful) ? result.successful.length : 0;
191
+ if (successfulCount <= 0) {
192
+ return;
193
+ }
194
+
195
+ try {
196
+ uppy.clear();
197
+ } catch {
198
+ // Upload succeeded; ignore clear timing issues.
199
+ }
200
+ });
201
+
202
+ avatarUppy = uppy;
203
+ }
204
+
205
+ function openEditor() {
206
+ setup();
207
+
208
+ const uppy = avatarUppy;
209
+ if (!uppy) {
210
+ reportAccountFeedback({
211
+ message: "Avatar editor is unavailable in this environment.",
212
+ severity: "error",
213
+ channel: "banner",
214
+ dedupeKey: "users-web.account-settings-runtime:avatar-editor-unavailable"
215
+ });
216
+ return;
217
+ }
218
+
219
+ const dashboard = uppy.getPlugin("Dashboard");
220
+ if (dashboard && typeof dashboard.openModal === "function") {
221
+ dashboard.openModal();
222
+ }
223
+ }
224
+
225
+ function destroy() {
226
+ if (!avatarUppy) {
227
+ return;
228
+ }
229
+
230
+ avatarUppy.destroy();
231
+ avatarUppy = null;
232
+ }
233
+
234
+ return Object.freeze({
235
+ destroy,
236
+ openEditor,
237
+ setup
238
+ });
239
+ }
240
+
241
+ export { createAccountSettingsAvatarUploadRuntime };
@@ -0,0 +1,88 @@
1
+ import { resolveErrorStatusCode } from "../support/runtimeNormalization.js";
2
+
3
+ function createAccountSettingsInvitesRuntime({
4
+ invitesAvailable,
5
+ isResolvingInvite,
6
+ inviteAction,
7
+ redeemInviteModel,
8
+ redeemInviteCommand,
9
+ pendingInvites,
10
+ pendingInvitesModel,
11
+ pendingInvitesView,
12
+ openWorkspace,
13
+ reportAccountFeedback
14
+ } = {}) {
15
+ async function respondToInvite(invite, decision) {
16
+ if (!invitesAvailable.value) {
17
+ return;
18
+ }
19
+
20
+ const token = String(invite?.token || "").trim();
21
+ const normalizedDecision = String(decision || "").trim().toLowerCase();
22
+ if (!token || (normalizedDecision !== "accept" && normalizedDecision !== "refuse")) {
23
+ return;
24
+ }
25
+ if (isResolvingInvite.value) {
26
+ return;
27
+ }
28
+
29
+ inviteAction.value = {
30
+ token,
31
+ decision: normalizedDecision
32
+ };
33
+ redeemInviteModel.token = token;
34
+ redeemInviteModel.decision = normalizedDecision;
35
+
36
+ try {
37
+ await redeemInviteCommand.run();
38
+ pendingInvitesModel.pendingInvites = pendingInvites.value.filter((entry) => entry.token !== token);
39
+ await pendingInvitesView.refresh();
40
+
41
+ if (normalizedDecision === "accept") {
42
+ const nextWorkspaceSlug = String(invite?.workspaceSlug || "").trim();
43
+ if (nextWorkspaceSlug) {
44
+ await openWorkspace(nextWorkspaceSlug);
45
+ return;
46
+ }
47
+ }
48
+
49
+ reportAccountFeedback({
50
+ message: normalizedDecision === "accept" ? "Invitation accepted." : "Invitation refused.",
51
+ severity: "success",
52
+ channel: "snackbar",
53
+ dedupeKey: `users-web.account-settings-runtime:invite-${normalizedDecision}:${token}`
54
+ });
55
+ } catch (error) {
56
+ const statusCode = resolveErrorStatusCode(error);
57
+ const fallbackMessage = normalizedDecision === "accept"
58
+ ? "Unable to accept invitation."
59
+ : "Unable to refuse invitation.";
60
+ reportAccountFeedback({
61
+ message: statusCode === 404
62
+ ? "Invitation no longer exists."
63
+ : String(error?.message || fallbackMessage),
64
+ severity: "error",
65
+ channel: "banner",
66
+ dedupeKey: `users-web.account-settings-runtime:invite-${normalizedDecision}-error:${token}`
67
+ });
68
+ } finally {
69
+ inviteAction.value = {
70
+ token: "",
71
+ decision: ""
72
+ };
73
+ redeemInviteModel.token = "";
74
+ redeemInviteModel.decision = "";
75
+ }
76
+ }
77
+
78
+ return Object.freeze({
79
+ accept(invite) {
80
+ return respondToInvite(invite, "accept");
81
+ },
82
+ refuse(invite) {
83
+ return respondToInvite(invite, "refuse");
84
+ }
85
+ });
86
+ }
87
+
88
+ export { createAccountSettingsInvitesRuntime };
@@ -0,0 +1,77 @@
1
+ const AVATAR_ALLOWED_MIME_TYPES = Object.freeze(["image/jpeg", "image/png", "image/webp"]);
2
+ const AVATAR_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
3
+ const AVATAR_DEFAULT_SIZE = 64;
4
+
5
+ const THEME_OPTIONS = Object.freeze([
6
+ { title: "System", value: "system" },
7
+ { title: "Light", value: "light" },
8
+ { title: "Dark", value: "dark" }
9
+ ]);
10
+
11
+ const LOCALE_OPTIONS = Object.freeze([
12
+ { title: "English (US)", value: "en-US" },
13
+ { title: "English (UK)", value: "en-GB" },
14
+ { title: "Italian", value: "it-IT" },
15
+ { title: "Spanish", value: "es-ES" }
16
+ ]);
17
+
18
+ const DATE_FORMAT_OPTIONS = Object.freeze([
19
+ { title: "System", value: "system" },
20
+ { title: "MM/DD/YYYY", value: "mdy" },
21
+ { title: "DD/MM/YYYY", value: "dmy" },
22
+ { title: "YYYY-MM-DD", value: "ymd" }
23
+ ]);
24
+
25
+ const NUMBER_FORMAT_OPTIONS = Object.freeze([
26
+ { title: "System", value: "system" },
27
+ { title: "1,234.56", value: "comma-dot" },
28
+ { title: "1.234,56", value: "dot-comma" },
29
+ { title: "1 234,56", value: "space-comma" }
30
+ ]);
31
+
32
+ const CURRENCY_OPTIONS = Object.freeze(["USD", "EUR", "GBP", "AUD", "JPY"]);
33
+
34
+ const TIME_ZONE_OPTIONS = Object.freeze([
35
+ "UTC",
36
+ "America/New_York",
37
+ "America/Chicago",
38
+ "America/Denver",
39
+ "America/Los_Angeles",
40
+ "Europe/London",
41
+ "Europe/Rome",
42
+ "Asia/Tokyo",
43
+ "Australia/Sydney"
44
+ ]);
45
+
46
+ const AVATAR_SIZE_OPTIONS = Object.freeze([32, 40, 48, 56, 64, 72, 80, 96, 112, 128]);
47
+
48
+ const ACCOUNT_SETTINGS_DEFAULTS = Object.freeze({
49
+ preferences: {
50
+ theme: "system",
51
+ locale: "en-US",
52
+ timeZone: "UTC",
53
+ dateFormat: "system",
54
+ numberFormat: "system",
55
+ currencyCode: "USD",
56
+ avatarSize: AVATAR_DEFAULT_SIZE
57
+ },
58
+ notifications: {
59
+ productUpdates: true,
60
+ accountActivity: true,
61
+ securityAlerts: true
62
+ }
63
+ });
64
+
65
+ export {
66
+ ACCOUNT_SETTINGS_DEFAULTS,
67
+ AVATAR_ALLOWED_MIME_TYPES,
68
+ AVATAR_DEFAULT_SIZE,
69
+ AVATAR_MAX_UPLOAD_BYTES,
70
+ AVATAR_SIZE_OPTIONS,
71
+ CURRENCY_OPTIONS,
72
+ DATE_FORMAT_OPTIONS,
73
+ LOCALE_OPTIONS,
74
+ NUMBER_FORMAT_OPTIONS,
75
+ THEME_OPTIONS,
76
+ TIME_ZONE_OPTIONS
77
+ };
@@ -0,0 +1,75 @@
1
+ import { ACCOUNT_SETTINGS_DEFAULTS } from "./accountSettingsRuntimeConstants.js";
2
+ import {
3
+ normalizeReturnToPath as normalizeSharedReturnToPath,
4
+ resolveAllowedOriginsFromPlacementContext
5
+ } from "@jskit-ai/kernel/shared/support";
6
+ import { normalizeRecord } from "../support/runtimeNormalization.js";
7
+
8
+ function normalizeReturnToPath(value, { fallback = "/", accountSettingsPath = "/account", allowedOrigins = [] } = {}) {
9
+ return normalizeSharedReturnToPath(value, {
10
+ fallback,
11
+ allowedOrigins,
12
+ blockedPathnames: [accountSettingsPath],
13
+ pickFirstArrayValue: true
14
+ });
15
+ }
16
+
17
+ function resolveAllowedReturnToOrigins(contextValue = null) {
18
+ return resolveAllowedOriginsFromPlacementContext(contextValue);
19
+ }
20
+
21
+ function normalizeSettingsPayload(value) {
22
+ return normalizeRecord(value);
23
+ }
24
+
25
+ function normalizePendingInvite(entry) {
26
+ if (!entry || typeof entry !== "object") {
27
+ return null;
28
+ }
29
+
30
+ const id = Number(entry.id);
31
+ const workspaceId = Number(entry.workspaceId);
32
+ if (!Number.isInteger(id) || id < 1 || !Number.isInteger(workspaceId) || workspaceId < 1) {
33
+ return null;
34
+ }
35
+
36
+ const workspaceSlug = String(entry.workspaceSlug || "").trim();
37
+ if (!workspaceSlug) {
38
+ return null;
39
+ }
40
+
41
+ const token = String(entry.token || "").trim();
42
+ if (!token) {
43
+ return null;
44
+ }
45
+
46
+ return {
47
+ id,
48
+ token,
49
+ workspaceId,
50
+ workspaceSlug,
51
+ workspaceName: String(entry.workspaceName || workspaceSlug).trim() || workspaceSlug,
52
+ workspaceAvatarUrl: String(entry.workspaceAvatarUrl || "").trim(),
53
+ roleId: String(entry.roleId || "member").trim().toLowerCase() || "member",
54
+ status: String(entry.status || "pending").trim().toLowerCase() || "pending",
55
+ expiresAt: String(entry.expiresAt || "").trim()
56
+ };
57
+ }
58
+
59
+ function normalizeAvatarSize(value) {
60
+ const numeric = Number(value);
61
+ if (!Number.isInteger(numeric)) {
62
+ return ACCOUNT_SETTINGS_DEFAULTS.preferences.avatarSize;
63
+ }
64
+
65
+ const clamped = Math.min(128, Math.max(32, numeric));
66
+ return clamped;
67
+ }
68
+
69
+ export {
70
+ resolveAllowedReturnToOrigins,
71
+ normalizeAvatarSize,
72
+ normalizePendingInvite,
73
+ normalizeReturnToPath,
74
+ normalizeSettingsPayload
75
+ };
@@ -0,0 +1,66 @@
1
+ function normalizeErrorStatus(error) {
2
+ if (error == null || typeof error !== "object") {
3
+ return null;
4
+ }
5
+
6
+ const hasStatus = Object.prototype.hasOwnProperty.call(error, "status");
7
+ const hasStatusCode = Object.prototype.hasOwnProperty.call(error, "statusCode");
8
+ if (!hasStatus && !hasStatusCode) {
9
+ return null;
10
+ }
11
+
12
+ const status = Number(hasStatus ? error.status : error.statusCode);
13
+ return Number.isInteger(status) ? status : null;
14
+ }
15
+
16
+ function isGenericTransportMessage(error) {
17
+ const normalizedMessage = String(error?.message || "").trim();
18
+ if (!normalizedMessage) {
19
+ return false;
20
+ }
21
+
22
+ const status = normalizeErrorStatus(error);
23
+ if (status === 0) {
24
+ return true;
25
+ }
26
+
27
+ return status != null && status >= 400 && normalizedMessage === `Request failed with status ${status}.`;
28
+ }
29
+
30
+ function toQueryErrorMessage(error, fallbackMessage = "", defaultMessage = "Request failed.") {
31
+ if (!error) {
32
+ return "";
33
+ }
34
+
35
+ const normalizedFallback = String(fallbackMessage || "").trim();
36
+ const normalizedMessage = String(error?.message || "").trim();
37
+ if (normalizedMessage && !isGenericTransportMessage(error)) {
38
+ return normalizedMessage;
39
+ }
40
+ if (normalizedFallback) {
41
+ return normalizedFallback;
42
+ }
43
+ if (normalizedMessage) {
44
+ return normalizedMessage;
45
+ }
46
+ return String(defaultMessage || "Request failed.").trim();
47
+ }
48
+
49
+ function toUiErrorMessage(error, fallbackMessage = "", defaultMessage = "Request failed.") {
50
+ const normalizedFallback = String(fallbackMessage || "").trim();
51
+ if (normalizedFallback) {
52
+ return normalizedFallback;
53
+ }
54
+
55
+ const normalizedMessage = String(error?.message || "").trim();
56
+ if (normalizedMessage) {
57
+ return normalizedMessage;
58
+ }
59
+
60
+ return String(defaultMessage || "Request failed.").trim();
61
+ }
62
+
63
+ export {
64
+ toQueryErrorMessage,
65
+ toUiErrorMessage
66
+ };
@@ -0,0 +1,144 @@
1
+ import { computed, unref } from "vue";
2
+ import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
3
+ import { useScopeRuntime } from "../useScopeRuntime.js";
4
+ import { useOperationRealtime } from "../useRealtimeQueryInvalidation.js";
5
+ import {
6
+ normalizePermissions,
7
+ resolvePermissionAccess,
8
+ resolveEnabled,
9
+ resolveQueryKey
10
+ } from "../scopeHelpers.js";
11
+
12
+ function normalizePermissionSets(permissionSets = {}) {
13
+ const source = permissionSets && typeof permissionSets === "object" && !Array.isArray(permissionSets)
14
+ ? permissionSets
15
+ : {};
16
+ const normalized = {};
17
+
18
+ for (const [key, value] of Object.entries(source)) {
19
+ normalized[key] = normalizePermissions(value);
20
+ }
21
+
22
+ return normalized;
23
+ }
24
+
25
+ function hasAnyPermissions(permissionSets = {}) {
26
+ for (const list of Object.values(permissionSets)) {
27
+ if (Array.isArray(list) && list.length > 0) {
28
+ return true;
29
+ }
30
+ }
31
+
32
+ return false;
33
+ }
34
+
35
+ function useOperationScope({
36
+ ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
37
+ surfaceId = "",
38
+ access = "auto",
39
+ placementSource = "users-web.operation",
40
+ apiSuffix = "",
41
+ model,
42
+ readEnabled = true,
43
+ queryKeyFactory = null,
44
+ permissionSets = {},
45
+ realtime = null
46
+ } = {}) {
47
+ const normalizedPermissionSets = normalizePermissionSets(permissionSets);
48
+ const scopeRuntime = useScopeRuntime({
49
+ ownershipFilter,
50
+ surfaceId,
51
+ accessMode: access,
52
+ hasPermissionRequirements: hasAnyPermissions(normalizedPermissionSets),
53
+ placementSource
54
+ });
55
+ const normalizedOwnershipFilter = scopeRuntime.normalizedOwnershipFilter;
56
+ const routeContext = scopeRuntime.routeContext;
57
+ const workspaceSlugFromRoute = scopeRuntime.workspaceSlugFromRoute;
58
+ const hasRouteWorkspaceSlug = scopeRuntime.hasRouteWorkspaceSlug;
59
+ const accessRuntime = scopeRuntime.access;
60
+
61
+ const apiPath = computed(() =>
62
+ scopeRuntime.resolveApiPath(apiSuffix, {
63
+ model
64
+ })
65
+ );
66
+
67
+ const queryEnabled = computed(() =>
68
+ resolveEnabled(readEnabled, {
69
+ surfaceId: routeContext.currentSurfaceId.value,
70
+ workspaceSlug: workspaceSlugFromRoute.value,
71
+ ownershipFilter: normalizedOwnershipFilter,
72
+ model
73
+ })
74
+ );
75
+
76
+ const queryKey = computed(() =>
77
+ resolveQueryKey(queryKeyFactory, {
78
+ surfaceId: routeContext.currentSurfaceId.value,
79
+ workspaceSlug: workspaceSlugFromRoute.value,
80
+ ownershipFilter: normalizedOwnershipFilter
81
+ })
82
+ );
83
+ const realtimeBinding = useOperationRealtime({
84
+ realtime,
85
+ queryKey: typeof queryKeyFactory === "function" ? queryKey : null,
86
+ enabled: computed(() => hasRouteWorkspaceSlug.value && Boolean(apiPath.value))
87
+ });
88
+
89
+ function queryCanRun(accessGate = true) {
90
+ return computed(() =>
91
+ queryEnabled.value &&
92
+ hasRouteWorkspaceSlug.value &&
93
+ Boolean(apiPath.value) &&
94
+ Boolean(unref(accessGate))
95
+ );
96
+ }
97
+
98
+ function permissionGate(key = "") {
99
+ const list = normalizedPermissionSets[String(key || "")] || [];
100
+ return computed(() => resolvePermissionAccess(accessRuntime, list));
101
+ }
102
+
103
+ function loadError(baseError = "") {
104
+ return computed(() => {
105
+ if (scopeRuntime.workspaceRouteError.value) {
106
+ return scopeRuntime.workspaceRouteError.value;
107
+ }
108
+
109
+ const bootstrapError = String(accessRuntime.bootstrapError.value || "").trim();
110
+ if (bootstrapError) {
111
+ return bootstrapError;
112
+ }
113
+
114
+ if (baseError === undefined || baseError === null || baseError === "") {
115
+ return "";
116
+ }
117
+
118
+ return String(unref(baseError) || "").trim();
119
+ });
120
+ }
121
+
122
+ function isLoading(baseLoading = false) {
123
+ return computed(() => Boolean(unref(baseLoading)) || accessRuntime.isBootstrapping.value);
124
+ }
125
+
126
+ return Object.freeze({
127
+ scopeRuntime,
128
+ routeContext,
129
+ normalizedOwnershipFilter,
130
+ workspaceSlugFromRoute,
131
+ hasRouteWorkspaceSlug,
132
+ access: accessRuntime,
133
+ apiPath,
134
+ queryEnabled,
135
+ queryKey,
136
+ queryCanRun,
137
+ realtime: realtimeBinding,
138
+ permissionGate,
139
+ loadError,
140
+ isLoading
141
+ });
142
+ }
143
+
144
+ export { useOperationScope };