@jskit-ai/users-web 0.1.67 → 0.1.68
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 +13 -8
- package/package.json +7 -7
- package/src/client/account-settings/sections.js +57 -9
- package/src/client/providers/UsersWebClientProvider.js +0 -5
- package/templates/src/components/account/settings/AccountSettingsClientElement.vue +1 -1
- package/test/accountSettingsSections.test.js +68 -55
- package/test/settingsPlacementContract.test.js +10 -0
- package/src/client/providers/bootUsersWebClientProvider.js +0 -28
package/package.descriptor.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { HOME_TOOLS_OUTLET } from "./src/shared/toolsOutletContracts.js";
|
|
|
3
3
|
export default Object.freeze({
|
|
4
4
|
packageVersion: 1,
|
|
5
5
|
packageId: "@jskit-ai/users-web",
|
|
6
|
-
version: "0.1.
|
|
6
|
+
version: "0.1.68",
|
|
7
7
|
kind: "runtime",
|
|
8
8
|
description: "Users web module: account/profile UI plus shared users web widgets.",
|
|
9
9
|
dependsOn: [
|
|
@@ -96,7 +96,7 @@ export default Object.freeze({
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
subpath: "./client/account-settings/sections",
|
|
99
|
-
summary: "Exports account settings section
|
|
99
|
+
summary: "Exports placement-backed account settings section helpers."
|
|
100
100
|
}
|
|
101
101
|
],
|
|
102
102
|
containerTokens: {
|
|
@@ -115,6 +115,11 @@ export default Object.freeze({
|
|
|
115
115
|
defaultLinkComponentToken: HOME_TOOLS_OUTLET.defaultLinkComponentToken,
|
|
116
116
|
surfaces: ["home"],
|
|
117
117
|
source: "src/client/components/UsersHomeToolsWidget.vue"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
target: "account-settings:sections",
|
|
121
|
+
surfaces: ["account"],
|
|
122
|
+
source: "templates/src/components/account/settings/AccountSettingsClientElement.vue"
|
|
118
123
|
}
|
|
119
124
|
],
|
|
120
125
|
contributions: [
|
|
@@ -154,12 +159,12 @@ export default Object.freeze({
|
|
|
154
159
|
runtime: {
|
|
155
160
|
"@tanstack/vue-query": "5.92.12",
|
|
156
161
|
"@mdi/js": "^7.4.47",
|
|
157
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
158
|
-
"@jskit-ai/realtime": "0.1.
|
|
159
|
-
"@jskit-ai/kernel": "0.1.
|
|
160
|
-
"@jskit-ai/shell-web": "0.1.
|
|
161
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
162
|
-
"@jskit-ai/users-core": "0.1.
|
|
162
|
+
"@jskit-ai/http-runtime": "0.1.52",
|
|
163
|
+
"@jskit-ai/realtime": "0.1.52",
|
|
164
|
+
"@jskit-ai/kernel": "0.1.53",
|
|
165
|
+
"@jskit-ai/shell-web": "0.1.52",
|
|
166
|
+
"@jskit-ai/uploads-image-web": "0.1.31",
|
|
167
|
+
"@jskit-ai/users-core": "0.1.63",
|
|
163
168
|
vuetify: "^4.0.0"
|
|
164
169
|
},
|
|
165
170
|
dev: {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.68",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@tanstack/vue-query": "5.92.12",
|
|
37
37
|
"@mdi/js": "^7.4.47",
|
|
38
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
39
|
-
"@jskit-ai/kernel": "0.1.
|
|
40
|
-
"@jskit-ai/realtime": "0.1.
|
|
41
|
-
"@jskit-ai/shell-web": "0.1.
|
|
42
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
43
|
-
"@jskit-ai/users-core": "0.1.
|
|
38
|
+
"@jskit-ai/http-runtime": "0.1.52",
|
|
39
|
+
"@jskit-ai/kernel": "0.1.53",
|
|
40
|
+
"@jskit-ai/realtime": "0.1.52",
|
|
41
|
+
"@jskit-ai/shell-web": "0.1.52",
|
|
42
|
+
"@jskit-ai/uploads-image-web": "0.1.31",
|
|
43
|
+
"@jskit-ai/users-core": "0.1.63",
|
|
44
44
|
"vuetify": "^4.0.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
inject,
|
|
4
|
+
onBeforeUnmount,
|
|
5
|
+
onMounted,
|
|
6
|
+
ref
|
|
7
|
+
} from "vue";
|
|
8
|
+
import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
2
9
|
|
|
3
|
-
const
|
|
4
|
-
const ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG = "users.web.account-settings.sections";
|
|
10
|
+
const ACCOUNT_SETTINGS_SECTION_TARGET = "account-settings:sections";
|
|
5
11
|
const EMPTY_ACCOUNT_SETTINGS_SECTIONS = Object.freeze([]);
|
|
6
12
|
const RESERVED_ACCOUNT_SETTINGS_SECTION_VALUES = Object.freeze([
|
|
7
13
|
"profile",
|
|
8
14
|
"preferences",
|
|
9
15
|
"notifications"
|
|
10
16
|
]);
|
|
17
|
+
const WEB_PLACEMENT_RUNTIME_INJECTION_KEY = "jskit.shell-web.runtime.web-placement.client";
|
|
11
18
|
|
|
12
19
|
function normalizeAccountSettingsSectionEntry(entry = null) {
|
|
13
20
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
14
21
|
return null;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
const
|
|
18
|
-
|
|
24
|
+
const props = entry?.props && typeof entry.props === "object" && !Array.isArray(entry.props)
|
|
25
|
+
? entry.props
|
|
26
|
+
: {};
|
|
27
|
+
const value = String(props.value || entry.value || "").trim().toLowerCase();
|
|
28
|
+
const title = String(props.title || entry.title || "").trim();
|
|
19
29
|
const component = entry.component;
|
|
20
30
|
if (!value || !title || !component) {
|
|
21
31
|
return null;
|
|
@@ -26,7 +36,7 @@ function normalizeAccountSettingsSectionEntry(entry = null) {
|
|
|
26
36
|
title,
|
|
27
37
|
component,
|
|
28
38
|
order: Number.isFinite(Number(entry.order)) ? Number(entry.order) : 500,
|
|
29
|
-
usesSharedRuntime: entry.usesSharedRuntime === true
|
|
39
|
+
usesSharedRuntime: props.usesSharedRuntime === true || entry.usesSharedRuntime === true
|
|
30
40
|
});
|
|
31
41
|
}
|
|
32
42
|
|
|
@@ -51,6 +61,7 @@ function resolveAccountSettingsSections(entries = []) {
|
|
|
51
61
|
if (!resolved || seen.has(resolved.value)) {
|
|
52
62
|
continue;
|
|
53
63
|
}
|
|
64
|
+
|
|
54
65
|
seen.add(resolved.value);
|
|
55
66
|
normalized.push(resolved);
|
|
56
67
|
}
|
|
@@ -59,12 +70,49 @@ function resolveAccountSettingsSections(entries = []) {
|
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
function useAccountSettingsSections() {
|
|
62
|
-
|
|
73
|
+
const placementRuntime = inject(WEB_PLACEMENT_RUNTIME_INJECTION_KEY, null);
|
|
74
|
+
const { context: placementContext } = useWebPlacementContext();
|
|
75
|
+
const revision = ref(
|
|
76
|
+
placementRuntime && typeof placementRuntime.getRevision === "function"
|
|
77
|
+
? placementRuntime.getRevision()
|
|
78
|
+
: 0
|
|
79
|
+
);
|
|
80
|
+
let unsubscribe = null;
|
|
81
|
+
|
|
82
|
+
onMounted(() => {
|
|
83
|
+
if (!placementRuntime || typeof placementRuntime.subscribe !== "function") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
unsubscribe = placementRuntime.subscribe((event = {}) => {
|
|
87
|
+
const nextRevision = Number(event.revision);
|
|
88
|
+
revision.value = Number.isInteger(nextRevision) ? nextRevision : revision.value + 1;
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
onBeforeUnmount(() => {
|
|
93
|
+
if (typeof unsubscribe === "function") {
|
|
94
|
+
unsubscribe();
|
|
95
|
+
unsubscribe = null;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return computed(() => {
|
|
100
|
+
void revision.value;
|
|
101
|
+
if (!placementRuntime || typeof placementRuntime.getPlacements !== "function") {
|
|
102
|
+
return EMPTY_ACCOUNT_SETTINGS_SECTIONS;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const placements = placementRuntime.getPlacements({
|
|
106
|
+
surface: "account",
|
|
107
|
+
target: ACCOUNT_SETTINGS_SECTION_TARGET,
|
|
108
|
+
context: placementContext.value
|
|
109
|
+
});
|
|
110
|
+
return resolveAccountSettingsSections(placements);
|
|
111
|
+
});
|
|
63
112
|
}
|
|
64
113
|
|
|
65
114
|
export {
|
|
66
|
-
|
|
67
|
-
ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG,
|
|
115
|
+
ACCOUNT_SETTINGS_SECTION_TARGET,
|
|
68
116
|
EMPTY_ACCOUNT_SETTINGS_SECTIONS,
|
|
69
117
|
RESERVED_ACCOUNT_SETTINGS_SECTION_VALUES,
|
|
70
118
|
normalizeAccountSettingsSectionEntry,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import UsersHomeToolsWidget from "../components/UsersHomeToolsWidget.vue";
|
|
2
2
|
import ProfileClientElement from "../components/ProfileClientElement.vue";
|
|
3
|
-
import { bootUsersWebClientProvider } from "./bootUsersWebClientProvider.js";
|
|
4
3
|
|
|
5
4
|
class UsersWebClientProvider {
|
|
6
5
|
static id = "users.web.client";
|
|
@@ -14,10 +13,6 @@ class UsersWebClientProvider {
|
|
|
14
13
|
app.singleton("users.web.home.tools.widget", () => UsersHomeToolsWidget);
|
|
15
14
|
app.singleton("users.web.profile.element", () => ProfileClientElement);
|
|
16
15
|
}
|
|
17
|
-
|
|
18
|
-
async boot(app) {
|
|
19
|
-
await bootUsersWebClientProvider(app);
|
|
20
|
-
}
|
|
21
16
|
}
|
|
22
17
|
|
|
23
18
|
export { UsersWebClientProvider };
|
|
@@ -1,79 +1,92 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
ACCOUNT_SETTINGS_SECTION_TARGET,
|
|
5
|
+
normalizeAccountSettingsSectionEntry,
|
|
5
6
|
resolveAccountSettingsSections
|
|
6
7
|
} from "../src/client/account-settings/sections.js";
|
|
7
|
-
import { bootUsersWebClientProvider } from "../src/client/providers/bootUsersWebClientProvider.js";
|
|
8
8
|
|
|
9
|
-
test("
|
|
9
|
+
test("account settings sections use the standard placement target", () => {
|
|
10
|
+
assert.equal(ACCOUNT_SETTINGS_SECTION_TARGET, "account-settings:sections");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("resolveAccountSettingsSections normalizes, deduplicates, and sorts placement-backed account section entries", () => {
|
|
10
14
|
const SectionA = {};
|
|
11
15
|
const SectionB = {};
|
|
12
16
|
|
|
13
17
|
const resolved = resolveAccountSettingsSections([
|
|
14
|
-
{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
test("bootUsersWebClientProvider provides normalized account section extensions", async () => {
|
|
31
|
-
const SectionComponent = {};
|
|
32
|
-
const provided = new Map();
|
|
33
|
-
let resolvedTagName = "";
|
|
34
|
-
|
|
35
|
-
await bootUsersWebClientProvider({
|
|
36
|
-
make(token) {
|
|
37
|
-
if (token === "jskit.client.vue.app") {
|
|
38
|
-
return {
|
|
39
|
-
provide(key, value) {
|
|
40
|
-
provided.set(key, value);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
18
|
+
{
|
|
19
|
+
id: "users.notifications.duplicate",
|
|
20
|
+
order: 999,
|
|
21
|
+
component: SectionA,
|
|
22
|
+
props: {
|
|
23
|
+
value: "notifications",
|
|
24
|
+
title: "Ignore duplicate"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "workspaces.invites",
|
|
29
|
+
order: 400,
|
|
30
|
+
component: SectionA,
|
|
31
|
+
props: {
|
|
32
|
+
value: "invites",
|
|
33
|
+
title: "Invites"
|
|
43
34
|
}
|
|
44
|
-
throw new Error(`Unexpected token: ${token}`);
|
|
45
35
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
36
|
+
{
|
|
37
|
+
id: "invalid.missing-component",
|
|
38
|
+
order: 250,
|
|
39
|
+
props: {
|
|
40
|
+
value: "profile",
|
|
41
|
+
title: "Broken missing component"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "security.section",
|
|
46
|
+
order: 350,
|
|
47
|
+
component: SectionB,
|
|
48
|
+
props: {
|
|
49
|
+
value: "security",
|
|
50
|
+
title: "Security",
|
|
51
|
+
usesSharedRuntime: true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "workspaces.invites.duplicate",
|
|
56
|
+
order: 100,
|
|
57
|
+
component: SectionB,
|
|
58
|
+
props: {
|
|
59
|
+
value: "invites",
|
|
60
|
+
title: "Second duplicate"
|
|
61
|
+
}
|
|
57
62
|
}
|
|
58
|
-
|
|
63
|
+
]);
|
|
59
64
|
|
|
60
|
-
assert.equal(resolvedTagName, ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG);
|
|
61
|
-
const [providedSections] = [...provided.values()];
|
|
62
|
-
assert.equal(Array.isArray(providedSections), true);
|
|
63
65
|
assert.deepEqual(
|
|
64
|
-
|
|
66
|
+
resolved.map((entry) => ({
|
|
65
67
|
value: entry.value,
|
|
66
68
|
title: entry.title,
|
|
67
69
|
order: entry.order,
|
|
68
70
|
usesSharedRuntime: entry.usesSharedRuntime
|
|
69
71
|
})),
|
|
70
72
|
[
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
title: "Invites",
|
|
74
|
-
order: 400,
|
|
75
|
-
usesSharedRuntime: false
|
|
76
|
-
}
|
|
73
|
+
{ value: "security", title: "Security", order: 350, usesSharedRuntime: true },
|
|
74
|
+
{ value: "invites", title: "Invites", order: 400, usesSharedRuntime: false }
|
|
77
75
|
]
|
|
78
76
|
);
|
|
79
77
|
});
|
|
78
|
+
|
|
79
|
+
test("normalizeAccountSettingsSectionEntry rejects malformed placement entries", () => {
|
|
80
|
+
assert.equal(normalizeAccountSettingsSectionEntry(null), null);
|
|
81
|
+
assert.equal(normalizeAccountSettingsSectionEntry({}), null);
|
|
82
|
+
assert.equal(
|
|
83
|
+
normalizeAccountSettingsSectionEntry({
|
|
84
|
+
component: {},
|
|
85
|
+
props: {
|
|
86
|
+
value: "",
|
|
87
|
+
title: "Broken"
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
null
|
|
91
|
+
);
|
|
92
|
+
});
|
|
@@ -92,6 +92,16 @@ test("users-web descriptor metadata advertises home tools outlet and standard ho
|
|
|
92
92
|
}
|
|
93
93
|
]
|
|
94
94
|
);
|
|
95
|
+
assert.deepEqual(
|
|
96
|
+
readOutlets("account-settings:sections"),
|
|
97
|
+
[
|
|
98
|
+
{
|
|
99
|
+
target: "account-settings:sections",
|
|
100
|
+
surfaces: ["account"],
|
|
101
|
+
source: "templates/src/components/account/settings/AccountSettingsClientElement.vue"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
);
|
|
95
105
|
|
|
96
106
|
expectContribution("users.profile.menu.settings", {
|
|
97
107
|
target: "auth-profile-menu:primary-menu",
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ACCOUNT_SETTINGS_SECTIONS_INJECTION_KEY,
|
|
3
|
-
ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG,
|
|
4
|
-
resolveAccountSettingsSections
|
|
5
|
-
} from "../account-settings/sections.js";
|
|
6
|
-
|
|
7
|
-
async function bootUsersWebClientProvider(app) {
|
|
8
|
-
if (!app || typeof app.make !== "function") {
|
|
9
|
-
throw new Error("bootUsersWebClientProvider requires application make().");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const vueApp = app.make("jskit.client.vue.app");
|
|
13
|
-
if (!vueApp || typeof vueApp.provide !== "function") {
|
|
14
|
-
throw new Error("bootUsersWebClientProvider requires jskit.client.vue.app provide().");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const extensionSections =
|
|
18
|
-
typeof app.resolveTag === "function"
|
|
19
|
-
? app.resolveTag(ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG)
|
|
20
|
-
: [];
|
|
21
|
-
|
|
22
|
-
vueApp.provide(
|
|
23
|
-
ACCOUNT_SETTINGS_SECTIONS_INJECTION_KEY,
|
|
24
|
-
resolveAccountSettingsSections(extensionSections)
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export { bootUsersWebClientProvider };
|