@jskit-ai/users-web 0.2.8 → 0.2.10
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/fixtures/account-settings/index.html +12 -0
- package/fixtures/account-settings/main.js +140 -0
- package/fixtures/account-settings/vite.config.mjs +9 -0
- package/package.json +8 -8
- package/patterns/account-settings/example/src/components/account/settings/AccountSettingsPreferencesSection.vue +7 -7
- package/patterns/account-settings/example/src/components/account/settings/AccountSettingsProfileSection.vue +1 -1
- package/src/client/account-settings/sections.js +8 -18
- package/src/client/bootstrap/user-bootstrap-handler.js +31 -33
- package/src/client/components/ProfileClientElement.vue +17 -76
- package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +11 -1
- package/src/client/composables/useAccountSettingsRuntime.js +18 -14
- package/src/client/lib/accountLocale.js +2 -2
- package/src/client/lib/profileElementPresentation.js +85 -0
- package/src/client/lib/theme.js +20 -46
- package/src/client/providers/UsersWebClientProvider.js +6 -0
- package/src/client/types.js +65 -0
- package/test/accountLocale.test.js +12 -5
- package/test/accountSettings.browser.test.js +395 -0
|
@@ -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.
|
|
3
|
+
"version": "0.2.10",
|
|
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.
|
|
17
|
-
"@jskit-ai/users-core": "0.2.
|
|
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.
|
|
21
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
22
|
-
"@jskit-ai/http-web": "0.2.
|
|
23
|
-
"@jskit-ai/kernel": "0.2.
|
|
24
|
-
"@jskit-ai/shell-web": "0.2.
|
|
20
|
+
"@jskit-ai/auth-web": "0.2.7",
|
|
21
|
+
"@jskit-ai/http-runtime": "0.2.5",
|
|
22
|
+
"@jskit-ai/http-web": "0.2.9",
|
|
23
|
+
"@jskit-ai/kernel": "0.2.5",
|
|
24
|
+
"@jskit-ai/shell-web": "0.2.5",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
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
|
|
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 =
|
|
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:
|
|
432
|
-
isDeletingAvatar:
|
|
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 =
|
|
443
|
+
const preferences = {
|
|
440
444
|
form: preferencesForm,
|
|
441
445
|
fieldErrors: preferencesAddEdit.fieldErrors,
|
|
442
|
-
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 =
|
|
469
|
+
const notifications = {
|
|
466
470
|
form: notificationsForm,
|
|
467
|
-
isSaving:
|
|
471
|
+
isSaving: savingNotifications,
|
|
468
472
|
isRefreshing: refreshingSettings,
|
|
469
473
|
submit: submitNotifications
|
|
470
|
-
}
|
|
474
|
+
};
|
|
471
475
|
|
|
472
|
-
const signInMethods =
|
|
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
|
|
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
|
|
23
|
+
return readonly(accountLocale);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export {
|