@jskit-ai/users-web 0.2.9 → 0.2.11

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.
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Account settings regression</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/main.js"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,140 @@
1
+ import { createApp, h, reactive, ref, shallowReactive } from "vue";
2
+ import { createRouter, createWebHistory, RouterView } from "vue-router";
3
+ import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
4
+ import "vuetify/styles";
5
+ import { createVuetify } from "vuetify";
6
+ import * as components from "vuetify/components";
7
+ import * as directives from "vuetify/directives";
8
+ import { aliases, mdi } from "vuetify/iconsets/mdi-svg";
9
+ import { SHELL_WEB_ERROR_RUNTIME_KEY } from "@jskit-ai/shell-web/client/error";
10
+ import { SHELL_WEB_PLACEMENT_RUNTIME_KEY } from "@jskit-ai/shell-web/client/placement";
11
+ import AccountSettingsClientElement from "../../src/client/components/AccountSettingsClientElement.vue";
12
+ import ProfileClientElement from "../../src/client/components/ProfileClientElement.vue";
13
+ import { useAccountLocale } from "../../src/client/lib/accountLocale.js";
14
+ import NotificationsSection from "../../patterns/account-settings/example/src/components/account/settings/AccountSettingsNotificationsSection.vue";
15
+ import PreferencesSection from "../../patterns/account-settings/example/src/components/account/settings/AccountSettingsPreferencesSection.vue";
16
+ import ProfileSection from "../../patterns/account-settings/example/src/components/account/settings/AccountSettingsProfileSection.vue";
17
+ import SignInMethodsSection from "../../patterns/account-settings/example/src/components/account/settings/AccountSettingsSignInMethodsSection.vue";
18
+
19
+ function section(value, title, component, order) {
20
+ return { order, component, props: { value, title, usesSharedRuntime: true } };
21
+ }
22
+
23
+ // The shell's two runtimes as doubles; everything below them is the real account settings code.
24
+ const context = {
25
+ surfaceConfig: {
26
+ tenancyMode: "none",
27
+ defaultSurfaceId: "home",
28
+ enabledSurfaceIds: ["home", "account"],
29
+ surfacesById: {
30
+ home: { id: "home", enabled: true, pagesRoot: "home", routeBase: "/home", requiresWorkspace: false },
31
+ account: { id: "account", enabled: true, pagesRoot: "account", routeBase: "/account", requiresWorkspace: false }
32
+ }
33
+ }
34
+ };
35
+ const listeners = new Set();
36
+ let revision = 1;
37
+ let placements = new URLSearchParams(window.location.search).has("empty")
38
+ ? []
39
+ : [
40
+ section("profile", "Profile", ProfileSection, 100),
41
+ section("preferences", "Preferences", PreferencesSection, 200),
42
+ section("notifications", "Notifications", NotificationsSection, 300),
43
+ section("sign-in-methods", "Sign-in methods", SignInMethodsSection, 400)
44
+ ];
45
+ const placement = {
46
+ getContext: () => context,
47
+ setContext: () => context,
48
+ getPlacements: () => placements,
49
+ getRevision: () => revision,
50
+ subscribe(listener) {
51
+ listeners.add(listener);
52
+ return () => listeners.delete(listener);
53
+ }
54
+ };
55
+ const errorRuntime = {
56
+ report(event) {
57
+ window.accountFixture.reports.push(event);
58
+ return { skipped: false };
59
+ },
60
+ dismiss: () => 0
61
+ };
62
+
63
+ // What a host passes ProfileClientElement: forms and pending flags, with actions the test can make fail.
64
+ const profileProps = reactive({ copy: {}, variant: {}, features: {}, ui: {} });
65
+ const profileState = shallowReactive({
66
+ profileForm: reactive({ displayName: "Ada", email: "ada@example.com" }),
67
+ preferencesForm: reactive({ avatarSize: 64 }),
68
+ profileAvatar: reactive({ effectiveUrl: "", hasUploadedAvatar: true }),
69
+ profileInitials: "AD",
70
+ selectedAvatarFileName: "",
71
+ profileFieldErrors: reactive({}),
72
+ profileMutation: { isPending: ref(false) },
73
+ avatarDeleteMutation: { isPending: ref(false) }
74
+ });
75
+ const actions = {
76
+ async submitProfile() {},
77
+ async openAvatarEditor() {
78
+ profileState.selectedAvatarFileName = "ada.png";
79
+ },
80
+ async submitAvatarDelete() {
81
+ throw new Error("Avatar service down.");
82
+ }
83
+ };
84
+ function record(name) {
85
+ return (payload) => window.accountFixture.events.push({ name, payload });
86
+ }
87
+
88
+ window.accountFixture = {
89
+ reports: [],
90
+ events: [],
91
+ profileProps,
92
+ profileState,
93
+ placeSection() {
94
+ placements = [...placements, section("extra", "Extra", { render: () => h("p", "Extra section") }, 150)];
95
+ revision += 1;
96
+ listeners.forEach((listener) => listener({ revision }));
97
+ }
98
+ };
99
+
100
+ const AccountPage = {
101
+ setup() {
102
+ const accountLocale = useAccountLocale();
103
+ return () => h("div", [
104
+ h("p", { "data-testid": "account-locale" }, accountLocale.value),
105
+ h(AccountSettingsClientElement)
106
+ ]);
107
+ }
108
+ };
109
+ const ProfileElementPage = {
110
+ render: () => h(ProfileClientElement, {
111
+ state: profileState,
112
+ actions,
113
+ ...profileProps,
114
+ "onProfile:submit": record("profile:submit"),
115
+ "onAvatar:replace": record("avatar:replace"),
116
+ "onAvatar:remove": record("avatar:remove"),
117
+ onInteraction: record("interaction"),
118
+ "onAction:started": record("action:started"),
119
+ "onAction:succeeded": record("action:succeeded"),
120
+ "onAction:failed": record("action:failed")
121
+ }, {
122
+ "form-after": ({ state }) => h("p", { "data-testid": "form-after" }, state.profileForm.email)
123
+ })
124
+ };
125
+ const router = createRouter({
126
+ history: createWebHistory(),
127
+ routes: [
128
+ { path: "/home", component: { render: () => h("p", { "data-testid": "home" }, "Home") } },
129
+ { path: "/account", component: AccountPage },
130
+ { path: "/profile-element", component: ProfileElementPage }
131
+ ]
132
+ });
133
+ const app = createApp({ render: () => h(components.VApp, null, { default: () => h(components.VMain, null, { default: () => h(RouterView) }) }) });
134
+ app.provide(SHELL_WEB_PLACEMENT_RUNTIME_KEY, placement);
135
+ app.provide(SHELL_WEB_ERROR_RUNTIME_KEY, errorRuntime);
136
+ app.use(router);
137
+ app.use(VueQueryPlugin, { queryClient: new QueryClient({ defaultOptions: { queries: { retry: false } } }) });
138
+ app.use(createVuetify({ components, directives, icons: { defaultSet: "mdi", aliases, sets: { mdi } } }));
139
+ await router.isReady();
140
+ app.mount("#app");
@@ -0,0 +1,9 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { defineConfig } from "vite";
3
+ import vue from "@vitejs/plugin-vue";
4
+
5
+ export default defineConfig({
6
+ root: fileURLToPath(new URL("./", import.meta.url)),
7
+ plugins: [vue()],
8
+ server: { host: "127.0.0.1", strictPort: true }
9
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -13,15 +13,15 @@
13
13
  "./locales/en.json": "./locales/en.json"
14
14
  },
15
15
  "dependencies": {
16
- "@jskit-ai/uploads-image-web": "0.2.4",
17
- "@jskit-ai/users-core": "0.2.5"
16
+ "@jskit-ai/uploads-image-web": "0.2.5",
17
+ "@jskit-ai/users-core": "0.2.6"
18
18
  },
19
19
  "peerDependencies": {
20
- "@jskit-ai/auth-web": "0.2.6",
21
- "@jskit-ai/http-runtime": "0.2.4",
22
- "@jskit-ai/http-web": "0.2.8",
23
- "@jskit-ai/kernel": "0.2.4",
24
- "@jskit-ai/shell-web": "0.2.4",
20
+ "@jskit-ai/auth-web": "0.2.8",
21
+ "@jskit-ai/http-runtime": "0.2.5",
22
+ "@jskit-ai/http-web": "0.2.10",
23
+ "@jskit-ai/kernel": "0.2.5",
24
+ "@jskit-ai/shell-web": "0.2.6",
25
25
  "@tanstack/vue-query": "^5.90.5",
26
26
  "vue": "^3.5.13",
27
27
  "vue-router": "^5.0.4",
@@ -24,7 +24,7 @@ const preferences = props.runtime.preferences;
24
24
  variant="outlined"
25
25
  density="comfortable"
26
26
  :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
27
- :error-messages="preferences.fieldErrors.theme ? [preferences.fieldErrors.theme] : []"
27
+ :error-messages="preferences.fieldErrors.theme"
28
28
  />
29
29
  </v-col>
30
30
 
@@ -38,7 +38,7 @@ const preferences = props.runtime.preferences;
38
38
  variant="outlined"
39
39
  density="comfortable"
40
40
  :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
41
- :error-messages="preferences.fieldErrors.locale ? [preferences.fieldErrors.locale] : []"
41
+ :error-messages="preferences.fieldErrors.locale"
42
42
  />
43
43
  </v-col>
44
44
 
@@ -50,7 +50,7 @@ const preferences = props.runtime.preferences;
50
50
  variant="outlined"
51
51
  density="comfortable"
52
52
  :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
53
- :error-messages="preferences.fieldErrors.timeZone ? [preferences.fieldErrors.timeZone] : []"
53
+ :error-messages="preferences.fieldErrors.timeZone"
54
54
  />
55
55
  </v-col>
56
56
 
@@ -64,7 +64,7 @@ const preferences = props.runtime.preferences;
64
64
  variant="outlined"
65
65
  density="comfortable"
66
66
  :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
67
- :error-messages="preferences.fieldErrors.dateFormat ? [preferences.fieldErrors.dateFormat] : []"
67
+ :error-messages="preferences.fieldErrors.dateFormat"
68
68
  />
69
69
  </v-col>
70
70
 
@@ -78,7 +78,7 @@ const preferences = props.runtime.preferences;
78
78
  variant="outlined"
79
79
  density="comfortable"
80
80
  :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
81
- :error-messages="preferences.fieldErrors.numberFormat ? [preferences.fieldErrors.numberFormat] : []"
81
+ :error-messages="preferences.fieldErrors.numberFormat"
82
82
  />
83
83
  </v-col>
84
84
 
@@ -90,7 +90,7 @@ const preferences = props.runtime.preferences;
90
90
  variant="outlined"
91
91
  density="comfortable"
92
92
  :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
93
- :error-messages="preferences.fieldErrors.currencyCode ? [preferences.fieldErrors.currencyCode] : []"
93
+ :error-messages="preferences.fieldErrors.currencyCode"
94
94
  />
95
95
  </v-col>
96
96
 
@@ -102,7 +102,7 @@ const preferences = props.runtime.preferences;
102
102
  variant="outlined"
103
103
  density="comfortable"
104
104
  :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
105
- :error-messages="preferences.fieldErrors.avatarSize ? [preferences.fieldErrors.avatarSize] : []"
105
+ :error-messages="preferences.fieldErrors.avatarSize"
106
106
  />
107
107
  </v-col>
108
108
  </v-row>
@@ -59,7 +59,7 @@ const profile = props.runtime.profile;
59
59
  density="comfortable"
60
60
  autocomplete="nickname"
61
61
  :readonly="profile.isSaving.value || profile.isRefreshing.value"
62
- :error-messages="profile.fieldErrors.displayName ? [profile.fieldErrors.displayName] : []"
62
+ :error-messages="profile.fieldErrors.displayName"
63
63
  />
64
64
  </v-col>
65
65
 
@@ -85,20 +85,16 @@ function resolveAccountSettingsSections(entries = []) {
85
85
  return Object.freeze(deduplicated);
86
86
  }
87
87
 
88
+ function stopListening() {}
89
+
88
90
  function useAccountSettingsSections() {
89
- const placementRuntime = inject(SHELL_WEB_PLACEMENT_RUNTIME_KEY, null);
91
+ /** @type {import("../types.js").AccountSectionsPlacementRuntime} */
92
+ const placementRuntime = inject(SHELL_WEB_PLACEMENT_RUNTIME_KEY);
90
93
  const { context: placementContext } = useWebPlacementContext();
91
- const revision = ref(
92
- placementRuntime && typeof placementRuntime.getRevision === "function"
93
- ? placementRuntime.getRevision()
94
- : 0
95
- );
96
- let unsubscribe = null;
94
+ const revision = ref(placementRuntime.getRevision());
95
+ let unsubscribe = stopListening;
97
96
 
98
97
  onMounted(() => {
99
- if (!placementRuntime || typeof placementRuntime.subscribe !== "function") {
100
- return;
101
- }
102
98
  unsubscribe = placementRuntime.subscribe((event = {}) => {
103
99
  const nextRevision = Number(event.revision);
104
100
  revision.value = Number.isInteger(nextRevision) ? nextRevision : revision.value + 1;
@@ -106,18 +102,12 @@ function useAccountSettingsSections() {
106
102
  });
107
103
 
108
104
  onBeforeUnmount(() => {
109
- if (typeof unsubscribe === "function") {
110
- unsubscribe();
111
- unsubscribe = null;
112
- }
105
+ unsubscribe();
106
+ unsubscribe = stopListening;
113
107
  });
114
108
 
115
109
  return computed(() => {
116
110
  void revision.value;
117
- if (!placementRuntime || typeof placementRuntime.getPlacements !== "function") {
118
- return EMPTY_ACCOUNT_SETTINGS_SECTIONS;
119
- }
120
-
121
111
  const placements = placementRuntime.getPlacements({
122
112
  surface: "account",
123
113
  target: ACCOUNT_SETTINGS_SECTION_TARGET,
@@ -3,43 +3,41 @@ import { resolvePlacementUserFromBootstrapPayload } from "../lib/bootstrap.js";
3
3
  import { resolveBootstrapAccountLocale, setAccountLocale } from "../lib/accountLocale.js";
4
4
 
5
5
  function createUsersBootstrapUserHandler() {
6
- return Object.freeze({
7
- handlerId: "users.web.bootstrap.user",
8
- order: 50,
9
- applyBootstrapPayload({ payload = {}, placementRuntime, source } = {}) {
10
- setAccountLocale(resolveBootstrapAccountLocale(payload));
11
- if (!placementRuntime || typeof placementRuntime.setContext !== "function") {
12
- return;
6
+ /** @param {import("../types.js").UsersBootstrapContext} context */
7
+ function applyBootstrapPayload({ payload = {}, placementRuntime, source }) {
8
+ setAccountLocale(resolveBootstrapAccountLocale(payload));
9
+ const context = placementRuntime.getContext();
10
+ placementRuntime.setContext(
11
+ {
12
+ user: resolvePlacementUserFromBootstrapPayload(payload, context.user)
13
+ },
14
+ {
15
+ source
13
16
  }
17
+ );
18
+ }
14
19
 
15
- const context = placementRuntime.getContext?.() || {};
16
- placementRuntime.setContext(
17
- {
18
- user: resolvePlacementUserFromBootstrapPayload(payload, context?.user)
19
- },
20
- {
21
- source
22
- }
23
- );
24
- },
25
- handleBootstrapError({ error, placementRuntime, source } = {}) {
26
- if (resolveBootstrapErrorStatusCode(error) !== 401) {
27
- return;
28
- }
29
- setAccountLocale("");
30
- if (!placementRuntime || typeof placementRuntime.setContext !== "function") {
31
- return;
20
+ /** @param {import("../types.js").UsersBootstrapContext} context */
21
+ function handleBootstrapError({ error, placementRuntime, source }) {
22
+ if (resolveBootstrapErrorStatusCode(error) !== 401) {
23
+ return;
24
+ }
25
+ setAccountLocale("");
26
+ placementRuntime.setContext(
27
+ {
28
+ user: null
29
+ },
30
+ {
31
+ source
32
32
  }
33
+ );
34
+ }
33
35
 
34
- placementRuntime.setContext(
35
- {
36
- user: null
37
- },
38
- {
39
- source
40
- }
41
- );
42
- }
36
+ return Object.freeze({
37
+ handlerId: "users.web.bootstrap.user",
38
+ order: 50,
39
+ applyBootstrapPayload,
40
+ handleBootstrapError
43
41
  });
44
42
  }
45
43
 
@@ -57,7 +57,7 @@
57
57
  variant="outlined"
58
58
  :density="resolvedVariant.density"
59
59
  autocomplete="nickname"
60
- :error-messages="state.profileFieldErrors.displayName ? [state.profileFieldErrors.displayName] : []"
60
+ :error-messages="state.profileFieldErrors.displayName"
61
61
  />
62
62
  </v-col>
63
63
  <v-col cols="12" md="6">
@@ -89,34 +89,24 @@
89
89
  import { computed } from "vue";
90
90
  import { createComponentInteractionEmitter } from "@jskit-ai/kernel/client";
91
91
  import {
92
- normalizeObject,
93
- normalizeOneOf
94
- } from "@jskit-ai/kernel/shared/support/normalize";
92
+ resolveProfileCopy,
93
+ resolveProfileFeatures,
94
+ resolveProfileRootClasses,
95
+ resolveProfileUiClasses,
96
+ resolveProfileUiTestIds,
97
+ resolveProfileVariant
98
+ } from "../lib/profileElementPresentation.js";
95
99
  import { useJskitText } from "../i18n.js";
96
100
 
97
101
  const { t } = useJskitText();
98
102
 
99
- // Default copy, by copy prop key; the copy prop overrides it.
100
- const DEFAULT_COPY_KEYS = Object.freeze({
101
- title: "jskit.users.accountSettings.profile.title",
102
- previewSizePrefix: "jskit.users.accountSettings.profile.previewSizePrefix",
103
- previewSizeSuffix: "jskit.users.accountSettings.profile.previewSizeSuffix",
104
- replaceAvatar: "jskit.users.accountSettings.profile.replaceAvatar",
105
- removeAvatar: "jskit.users.accountSettings.profile.removeAvatar",
106
- selectedFilePrefix: "jskit.users.accountSettings.profile.selectedFilePrefix",
107
- displayName: "jskit.users.accountSettings.profile.displayName",
108
- email: "jskit.users.accountSettings.profile.email",
109
- emailHint: "jskit.users.accountSettings.profile.emailHint",
110
- saveProfile: "jskit.users.accountSettings.profile.save"
111
- });
112
-
113
103
  const props = defineProps({
114
104
  state: {
115
- type: Object,
105
+ type: /** @type {import("vue").PropType<import("../types.js").ProfileElementState>} */ (Object),
116
106
  required: true
117
107
  },
118
108
  actions: {
119
- type: Object,
109
+ type: /** @type {import("vue").PropType<import("../types.js").ProfileElementActions>} */ (Object),
120
110
  required: true
121
111
  },
122
112
  copy: {
@@ -146,65 +136,16 @@ const {
146
136
  invokeAction
147
137
  } = createComponentInteractionEmitter(emit);
148
138
 
149
- const copyText = computed(() => ({
150
- ...Object.fromEntries(Object.entries(DEFAULT_COPY_KEYS).map(([name, key]) => [name, t(key)])),
151
- ...normalizeObject(props.copy)
152
- }));
153
-
154
- const resolvedVariant = computed(() => {
155
- const variant = normalizeObject(props.variant);
156
- return {
157
- layout: normalizeOneOf(variant.layout, ["compact", "comfortable"], "comfortable"),
158
- surface: normalizeOneOf(variant.surface, ["plain", "carded"], "carded"),
159
- density: normalizeOneOf(variant.density, ["compact", "comfortable"], "comfortable"),
160
- tone: normalizeOneOf(variant.tone, ["neutral", "emphasized"], "neutral")
161
- };
162
- });
163
-
164
- const resolvedFeatures = computed(() => {
165
- const features = normalizeObject(props.features);
166
- return {
167
- header: features.header !== false,
168
- removeAvatar: features.removeAvatar !== false
169
- };
170
- });
171
-
172
- const uiClasses = computed(() => {
173
- const classes = normalizeObject(normalizeObject(props.ui).classes);
174
- return {
175
- root: String(classes.root || "").trim(),
176
- card: String(classes.card || "").trim()
177
- };
178
- });
179
-
180
- const uiTestIds = computed(() => {
181
- const testIds = normalizeObject(normalizeObject(props.ui).testIds);
182
- return {
183
- root: String(testIds.root || "profile-client-element"),
184
- card: String(testIds.card || "profile-client-card"),
185
- submitButton: String(testIds.submitButton || "profile-submit-button"),
186
- avatarReplaceButton: String(testIds.avatarReplaceButton || "profile-avatar-replace-button"),
187
- avatarRemoveButton: String(testIds.avatarRemoveButton || "profile-avatar-remove-button")
188
- };
189
- });
190
-
191
- const rootClasses = computed(() => {
192
- const classes = [
193
- "profile-client-element",
194
- `profile-client-element--layout-${resolvedVariant.value.layout}`,
195
- `profile-client-element--surface-${resolvedVariant.value.surface}`,
196
- `profile-client-element--density-${resolvedVariant.value.density}`,
197
- `profile-client-element--tone-${resolvedVariant.value.tone}`
198
- ];
199
- if (uiClasses.value.root) {
200
- classes.push(uiClasses.value.root);
201
- }
202
- return classes;
203
- });
139
+ const copyText = computed(() => resolveProfileCopy(props.copy, t));
140
+ const resolvedVariant = computed(() => resolveProfileVariant(props.variant));
141
+ const resolvedFeatures = computed(() => resolveProfileFeatures(props.features));
142
+ const uiClasses = computed(() => resolveProfileUiClasses(props.ui));
143
+ const uiTestIds = computed(() => resolveProfileUiTestIds(props.ui));
144
+ const rootClasses = computed(() => resolveProfileRootClasses(resolvedVariant.value, uiClasses.value));
204
145
 
205
146
  async function onSubmitProfile() {
206
147
  const payload = {
207
- displayName: String(state.profileForm?.displayName || "").trim()
148
+ displayName: String(state.profileForm.displayName || "").trim()
208
149
  };
209
150
  emit("profile:submit", payload);
210
151
  emitInteraction("profile:submit", payload);
@@ -4,6 +4,16 @@ import { resolveFieldErrors } from "@jskit-ai/http-runtime/client";
4
4
  import { httpWebClient } from "@jskit-ai/http-web/client/lib/httpClient";
5
5
  import { useJskitText } from "../../i18n.js";
6
6
 
7
+ /**
8
+ * @param {{
9
+ * queryClient: import("@jskit-ai/kernel/client/types").ClientQueryClient,
10
+ * sessionQueryKey: readonly string[],
11
+ * accountSettingsQueryKey: readonly string[],
12
+ * selectedAvatarFileName: import("vue").Ref<string>,
13
+ * applySettingsData: (data: unknown) => void,
14
+ * reportAccountFeedback: (feedback: import("../../types.js").AccountFeedback) => void
15
+ * }} options
16
+ */
7
17
  function createAccountSettingsAvatarUploadRuntime({
8
18
  queryClient,
9
19
  sessionQueryKey,
@@ -11,7 +21,7 @@ function createAccountSettingsAvatarUploadRuntime({
11
21
  selectedAvatarFileName,
12
22
  applySettingsData,
13
23
  reportAccountFeedback
14
- } = {}) {
24
+ }) {
15
25
  const { t, fieldErrorMessages, serverMessage } = useJskitText();
16
26
 
17
27
  async function resolveCsrfToken() {
@@ -359,6 +359,10 @@ function useAccountSettingsRuntime() {
359
359
 
360
360
  const loadingSettings = computed(() => Boolean(settingsView.isLoading));
361
361
  const refreshingSettings = computed(() => Boolean(settingsView.isRefetching));
362
+ const savingProfile = computed(() => Boolean(profileAddEdit.isSaving));
363
+ const deletingAvatar = computed(() => Boolean(avatarDeleteCommand.isRunning));
364
+ const savingPreferences = computed(() => Boolean(preferencesAddEdit.isSaving));
365
+ const savingNotifications = computed(() => Boolean(notificationsAddEdit.isSaving));
362
366
  const settingsLoadError = computed(() => String(settingsView.loadError || "").trim());
363
367
 
364
368
  async function refreshSettings() {
@@ -422,24 +426,24 @@ function useAccountSettingsRuntime() {
422
426
  avatarUploadRuntime.destroy();
423
427
  });
424
428
 
425
- const profile = Object.freeze({
429
+ const profile = {
426
430
  form: profileForm,
427
431
  avatar: profileAvatar,
428
432
  initials: profileInitials,
429
433
  selectedAvatarFileName,
430
434
  fieldErrors: profileAddEdit.fieldErrors,
431
- isSaving: profileAddEdit.isSaving,
432
- isDeletingAvatar: avatarDeleteCommand.isRunning,
435
+ isSaving: savingProfile,
436
+ isDeletingAvatar: deletingAvatar,
433
437
  isRefreshing: refreshingSettings,
434
438
  submit: submitProfile,
435
439
  openAvatarEditor,
436
440
  removeAvatar: submitAvatarDelete
437
- });
441
+ };
438
442
 
439
- const preferences = Object.freeze({
443
+ const preferences = {
440
444
  form: preferencesForm,
441
445
  fieldErrors: preferencesAddEdit.fieldErrors,
442
- isSaving: preferencesAddEdit.isSaving,
446
+ isSaving: savingPreferences,
443
447
  isRefreshing: refreshingSettings,
444
448
  // Plain arrays for v-select items; the getters follow the active language.
445
449
  options: Object.freeze({
@@ -460,24 +464,24 @@ function useAccountSettingsRuntime() {
460
464
  avatarSize: AVATAR_SIZE_OPTIONS
461
465
  }),
462
466
  submit: submitPreferences
463
- });
467
+ };
464
468
 
465
- const notifications = Object.freeze({
469
+ const notifications = {
466
470
  form: notificationsForm,
467
- isSaving: notificationsAddEdit.isSaving,
471
+ isSaving: savingNotifications,
468
472
  isRefreshing: refreshingSettings,
469
473
  submit: submitNotifications
470
- });
474
+ };
471
475
 
472
- const signInMethods = Object.freeze({
476
+ const signInMethods = {
473
477
  methods: signInMethodList,
474
478
  unlinkingProvider: computed(() => signInMethodsState.unlinkingProvider),
475
479
  isRefreshing: refreshingSettings,
476
480
  link: linkSignInMethod,
477
481
  unlink: unlinkSignInMethod
478
- });
482
+ };
479
483
 
480
- return Object.freeze({
484
+ return {
481
485
  backTarget,
482
486
  backNavigationTarget,
483
487
  loadingSettings,
@@ -488,7 +492,7 @@ function useAccountSettingsRuntime() {
488
492
  preferences,
489
493
  notifications,
490
494
  signInMethods
491
- });
495
+ };
492
496
  }
493
497
 
494
498
  export { useAccountSettingsRuntime };
@@ -4,8 +4,8 @@ import { normalizeLocaleCode } from "@jskit-ai/users-core/shared/settings";
4
4
  // The signed-in user's saved language (user_settings.locale) as a canonical BCP 47
5
5
  // code, or "" when nobody is signed in. JSKIT never switches the app's vue-i18n
6
6
  // locale; the app watches this and switches it.
7
+ // readable-check: allow module-state - shared resource: a page has one signed-in user, and the bootstrap handler that sets their language runs outside any component
7
8
  const accountLocale = ref("");
8
- const readonlyAccountLocale = readonly(accountLocale);
9
9
 
10
10
  function setAccountLocale(value) {
11
11
  accountLocale.value = normalizeLocaleCode(value);
@@ -20,7 +20,7 @@ function resolveBootstrapAccountLocale(payload = {}) {
20
20
  }
21
21
 
22
22
  function useAccountLocale() {
23
- return readonlyAccountLocale;
23
+ return readonly(accountLocale);
24
24
  }
25
25
 
26
26
  export {