@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.
- package/package.descriptor.mjs +507 -0
- package/package.json +31 -0
- package/src/client/components/ConsoleSettingsClientElement.vue +24 -0
- package/src/client/components/MembersAdminClientElement.vue +404 -0
- package/src/client/components/ProfileClientElement.vue +242 -0
- package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +39 -0
- package/src/client/components/UsersShellMenuLinkItem.vue +140 -0
- package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +87 -0
- package/src/client/components/UsersWorkspaceMembersMenuItem.vue +36 -0
- package/src/client/components/UsersWorkspacePermissionMenuItem.vue +90 -0
- package/src/client/components/UsersWorkspaceSelector.vue +237 -0
- package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +39 -0
- package/src/client/components/UsersWorkspaceToolsWidget.vue +23 -0
- package/src/client/components/WorkspaceMembersClientElement.vue +663 -0
- package/src/client/components/WorkspaceSettingsClientElement.vue +230 -0
- package/src/client/components/WorkspacesClientElement.vue +514 -0
- package/src/client/composables/accountSettingsAvatarUploadRuntime.js +241 -0
- package/src/client/composables/accountSettingsInvitesRuntime.js +88 -0
- package/src/client/composables/accountSettingsRuntimeConstants.js +77 -0
- package/src/client/composables/accountSettingsRuntimeHelpers.js +75 -0
- package/src/client/composables/errorMessageHelpers.js +66 -0
- package/src/client/composables/internal/useOperationScope.js +144 -0
- package/src/client/composables/modelStateHelpers.js +49 -0
- package/src/client/composables/operationUiHelpers.js +121 -0
- package/src/client/composables/operationValidationHelpers.js +52 -0
- package/src/client/composables/refValueHelpers.js +19 -0
- package/src/client/composables/scopeHelpers.js +145 -0
- package/src/client/composables/useAccess.js +109 -0
- package/src/client/composables/useAccountSettingsRuntime.js +533 -0
- package/src/client/composables/useAddEdit.js +135 -0
- package/src/client/composables/useAddEditCore.js +137 -0
- package/src/client/composables/useBootstrapQuery.js +52 -0
- package/src/client/composables/useCommand.js +112 -0
- package/src/client/composables/useCommandCore.js +130 -0
- package/src/client/composables/useEndpointResource.js +104 -0
- package/src/client/composables/useFieldErrorBag.js +61 -0
- package/src/client/composables/useList.js +85 -0
- package/src/client/composables/useListCore.js +65 -0
- package/src/client/composables/usePagedCollection.js +125 -0
- package/src/client/composables/usePaths.js +108 -0
- package/src/client/composables/useRealtimeQueryInvalidation.js +105 -0
- package/src/client/composables/useScopeRuntime.js +107 -0
- package/src/client/composables/useSurfaceRouteContext.js +31 -0
- package/src/client/composables/useUiFeedback.js +96 -0
- package/src/client/composables/useView.js +89 -0
- package/src/client/composables/useViewCore.js +104 -0
- package/src/client/composables/useWorkspaceRouteContext.js +28 -0
- package/src/client/composables/useWorkspaceSurfaceId.js +43 -0
- package/src/client/index.js +7 -0
- package/src/client/lib/bootstrap.js +95 -0
- package/src/client/lib/httpClient.js +67 -0
- package/src/client/lib/menuIcons.js +192 -0
- package/src/client/lib/permissions.js +34 -0
- package/src/client/lib/profileSurfaceMenuLinks.js +142 -0
- package/src/client/lib/surfaceAccessPolicy.js +350 -0
- package/src/client/lib/theme.js +99 -0
- package/src/client/lib/workspaceLinkResolver.js +207 -0
- package/src/client/lib/workspaceSurfaceContext.js +82 -0
- package/src/client/lib/workspaceSurfacePaths.js +163 -0
- package/src/client/providers/UsersWebClientProvider.js +85 -0
- package/src/client/runtime/bootstrapPlacementRouteGuards.js +371 -0
- package/src/client/runtime/bootstrapPlacementRuntime.js +413 -0
- package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +32 -0
- package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +157 -0
- package/src/client/support/contractGuards.js +34 -0
- package/src/client/support/realtimeWorkspace.js +12 -0
- package/src/client/support/runtimeNormalization.js +27 -0
- package/src/client/support/workspaceQueryKeys.js +15 -0
- package/templates/packages/main/src/client/components/AccountPendingInvitesCue.vue +162 -0
- package/templates/src/components/WorkspaceNotFoundCard.vue +33 -0
- package/templates/src/components/account/settings/AccountSettingsClientElement.vue +153 -0
- package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +77 -0
- package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +55 -0
- package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +125 -0
- package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +94 -0
- package/templates/src/composables/useWorkspaceNotFoundState.js +48 -0
- package/templates/src/pages/account/index.vue +17 -0
- package/templates/src/pages/admin/members/index.vue +7 -0
- package/templates/src/pages/admin/workspace/settings/index.vue +16 -0
- package/templates/src/pages/console/settings/index.vue +16 -0
- package/templates/src/surfaces/admin/index.vue +29 -0
- package/templates/src/surfaces/admin/root.vue +20 -0
- package/templates/src/surfaces/app/index.vue +27 -0
- package/templates/src/surfaces/app/root.vue +20 -0
- package/test/bootstrap.test.js +38 -0
- package/test/bootstrapPlacementRuntime.test.js +991 -0
- package/test/errorMessageHelpers.test.js +28 -0
- package/test/exportsContract.test.js +39 -0
- package/test/menuIcons.test.js +33 -0
- package/test/permissions.test.js +35 -0
- package/test/profileSurfaceMenuLinks.test.js +207 -0
- package/test/refValueHelpers.test.js +14 -0
- package/test/scopeHelpers.test.js +57 -0
- package/test/surfaceAccessPolicy.test.js +129 -0
- package/test/theme.test.js +95 -0
- package/test/workspaceLinkResolver.test.js +61 -0
- 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 };
|