@jskit-ai/users-web 0.1.72 → 0.1.74
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 +107 -19
- package/package.json +8 -8
- package/src/client/account-settings/sections.js +14 -10
- package/{templates/src/components/account/settings → src/client/components}/AccountSettingsClientElement.vue +14 -41
- package/src/client/composables/crud/crudJsonApiTransportSupport.js +137 -0
- package/src/client/composables/crud/crudLookupFieldLabelSupport.js +13 -1
- package/src/client/composables/crud/crudLookupFieldRuntime.js +6 -0
- package/src/client/composables/crud/crudSchemaFormHelpers.js +30 -0
- package/src/client/composables/internal/crudListParentTitleSupport.js +4 -0
- package/src/client/composables/records/useCrudAddEdit.js +11 -1
- package/src/client/composables/records/useCrudList.js +4 -0
- package/src/client/composables/records/useCrudView.js +7 -0
- package/src/client/composables/runtime/addEditUiRuntime.js +7 -6
- package/src/client/composables/runtime/useListCore.js +8 -0
- package/src/client/composables/useCrudListFilterLookups.js +5 -0
- package/src/client/composables/useCrudListParentTitle.js +21 -1
- package/src/client/index.js +1 -0
- package/templates/src/components/account/settings/vibe-coding-todo.todo +20 -0
- package/templates/src/pages/account/index.vue +1 -1
- package/test/accountSettingsSections.test.js +16 -5
- package/test/addEditUiRuntime.test.js +17 -0
- package/test/crudJsonApiTransportSupport.test.js +166 -0
- package/test/crudLookupFieldRuntime.test.js +25 -0
- package/test/exportsContract.test.js +1 -0
- package/test/requestTransportOptions.test.js +35 -0
- package/test/settingsPlacementContract.test.js +153 -1
- package/test/useCrudAddEdit.test.js +50 -0
- package/test/useCrudListParentTitle.test.js +106 -0
package/package.descriptor.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { HOME_COG_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.74",
|
|
7
7
|
kind: "runtime",
|
|
8
8
|
description: "Users web module: account/profile UI plus shared users web widgets.",
|
|
9
9
|
dependsOn: [
|
|
@@ -46,6 +46,10 @@ export default Object.freeze({
|
|
|
46
46
|
subpath: "./client/providers/UsersWebClientProvider",
|
|
47
47
|
summary: "Exports users-web client provider class."
|
|
48
48
|
},
|
|
49
|
+
{
|
|
50
|
+
subpath: "./client/components/AccountSettingsClientElement",
|
|
51
|
+
summary: "Exports the package-owned account settings host that renders placement-backed account sections."
|
|
52
|
+
},
|
|
49
53
|
{
|
|
50
54
|
subpath: "./client/components/ProfileClientElement",
|
|
51
55
|
summary: "Exports profile settings client element scaffold component."
|
|
@@ -90,10 +94,6 @@ export default Object.freeze({
|
|
|
90
94
|
subpath: "./client/lib/httpClient",
|
|
91
95
|
summary: "Exports the shared users-web HTTP client with credentials and CSRF behavior."
|
|
92
96
|
},
|
|
93
|
-
{
|
|
94
|
-
subpath: "./client/composables/useAccountSettingsRuntime",
|
|
95
|
-
summary: "Exports account settings runtime composable for app-owned settings UI."
|
|
96
|
-
},
|
|
97
97
|
{
|
|
98
98
|
subpath: "./client/account-settings/sections",
|
|
99
99
|
summary: "Exports placement-backed account settings section helpers."
|
|
@@ -119,7 +119,7 @@ export default Object.freeze({
|
|
|
119
119
|
{
|
|
120
120
|
target: "account-settings:sections",
|
|
121
121
|
surfaces: ["account"],
|
|
122
|
-
source: "
|
|
122
|
+
source: "src/client/components/AccountSettingsClientElement.vue"
|
|
123
123
|
}
|
|
124
124
|
],
|
|
125
125
|
contributions: [
|
|
@@ -149,6 +149,30 @@ export default Object.freeze({
|
|
|
149
149
|
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
150
150
|
when: "auth.authenticated === true",
|
|
151
151
|
source: "mutations.text#users-web-home-tools-placement"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "users.account.settings.profile",
|
|
155
|
+
target: "account-settings:sections",
|
|
156
|
+
surfaces: ["account"],
|
|
157
|
+
order: 100,
|
|
158
|
+
componentToken: "local.main.account-settings.section.profile",
|
|
159
|
+
source: "mutations.text#users-web-account-settings-sections-placement"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "users.account.settings.preferences",
|
|
163
|
+
target: "account-settings:sections",
|
|
164
|
+
surfaces: ["account"],
|
|
165
|
+
order: 200,
|
|
166
|
+
componentToken: "local.main.account-settings.section.preferences",
|
|
167
|
+
source: "mutations.text#users-web-account-settings-sections-placement"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "users.account.settings.notifications",
|
|
171
|
+
target: "account-settings:sections",
|
|
172
|
+
surfaces: ["account"],
|
|
173
|
+
order: 300,
|
|
174
|
+
componentToken: "local.main.account-settings.section.notifications",
|
|
175
|
+
source: "mutations.text#users-web-account-settings-sections-placement"
|
|
152
176
|
}
|
|
153
177
|
]
|
|
154
178
|
}
|
|
@@ -159,12 +183,12 @@ export default Object.freeze({
|
|
|
159
183
|
runtime: {
|
|
160
184
|
"@tanstack/vue-query": "5.92.12",
|
|
161
185
|
"@mdi/js": "^7.4.47",
|
|
162
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
163
|
-
"@jskit-ai/realtime": "0.1.
|
|
164
|
-
"@jskit-ai/kernel": "0.1.
|
|
165
|
-
"@jskit-ai/shell-web": "0.1.
|
|
166
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
167
|
-
"@jskit-ai/users-core": "0.1.
|
|
186
|
+
"@jskit-ai/http-runtime": "0.1.58",
|
|
187
|
+
"@jskit-ai/realtime": "0.1.58",
|
|
188
|
+
"@jskit-ai/kernel": "0.1.59",
|
|
189
|
+
"@jskit-ai/shell-web": "0.1.58",
|
|
190
|
+
"@jskit-ai/uploads-image-web": "0.1.37",
|
|
191
|
+
"@jskit-ai/users-core": "0.1.69",
|
|
168
192
|
vuetify: "^4.0.0"
|
|
169
193
|
},
|
|
170
194
|
dev: {}
|
|
@@ -185,13 +209,6 @@ export default Object.freeze({
|
|
|
185
209
|
category: "users-web",
|
|
186
210
|
id: "users-web-page-account-root"
|
|
187
211
|
},
|
|
188
|
-
{
|
|
189
|
-
from: "templates/src/components/account/settings/AccountSettingsClientElement.vue",
|
|
190
|
-
to: "src/components/account/settings/AccountSettingsClientElement.vue",
|
|
191
|
-
reason: "Install app-owned account settings container component scaffold.",
|
|
192
|
-
category: "users-web",
|
|
193
|
-
id: "users-web-component-account-settings-root"
|
|
194
|
-
},
|
|
195
212
|
{
|
|
196
213
|
from: "templates/src/components/account/settings/AccountSettingsProfileSection.vue",
|
|
197
214
|
to: "src/components/account/settings/AccountSettingsProfileSection.vue",
|
|
@@ -247,6 +264,77 @@ export default Object.freeze({
|
|
|
247
264
|
reason: "Append users-web home tools widget and settings menu placements into app-owned placement registry.",
|
|
248
265
|
category: "users-web",
|
|
249
266
|
id: "users-web-home-tools-placement"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
op: "append-text",
|
|
270
|
+
file: "src/placement.js",
|
|
271
|
+
position: "bottom",
|
|
272
|
+
skipIfContains: "id: \"users.account.settings.profile\"",
|
|
273
|
+
value:
|
|
274
|
+
"\naddPlacement({\n id: \"users.account.settings.profile\",\n target: \"account-settings:sections\",\n surfaces: [\"account\"],\n order: 100,\n componentToken: \"local.main.account-settings.section.profile\",\n props: {\n title: \"Profile\",\n value: \"profile\",\n usesSharedRuntime: true\n }\n});\n\naddPlacement({\n id: \"users.account.settings.preferences\",\n target: \"account-settings:sections\",\n surfaces: [\"account\"],\n order: 200,\n componentToken: \"local.main.account-settings.section.preferences\",\n props: {\n title: \"Preferences\",\n value: \"preferences\",\n usesSharedRuntime: true\n }\n});\n\naddPlacement({\n id: \"users.account.settings.notifications\",\n target: \"account-settings:sections\",\n surfaces: [\"account\"],\n order: 300,\n componentToken: \"local.main.account-settings.section.notifications\",\n props: {\n title: \"Notifications\",\n value: \"notifications\",\n usesSharedRuntime: true\n }\n});\n",
|
|
275
|
+
reason: "Append users-web account settings section placements into the app-owned placement registry.",
|
|
276
|
+
category: "users-web",
|
|
277
|
+
id: "users-web-account-settings-sections-placement"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
op: "append-text",
|
|
281
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
282
|
+
position: "top",
|
|
283
|
+
skipIfContains: "import AccountSettingsProfileSection from \"/src/components/account/settings/AccountSettingsProfileSection.vue\";",
|
|
284
|
+
value: "import AccountSettingsProfileSection from \"/src/components/account/settings/AccountSettingsProfileSection.vue\";\n",
|
|
285
|
+
reason: "Bind the app-owned account profile settings section into local main client provider imports.",
|
|
286
|
+
category: "users-web",
|
|
287
|
+
id: "users-web-main-client-provider-account-settings-profile-import"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
op: "append-text",
|
|
291
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
292
|
+
position: "top",
|
|
293
|
+
skipIfContains: "import AccountSettingsPreferencesSection from \"/src/components/account/settings/AccountSettingsPreferencesSection.vue\";",
|
|
294
|
+
value: "import AccountSettingsPreferencesSection from \"/src/components/account/settings/AccountSettingsPreferencesSection.vue\";\n",
|
|
295
|
+
reason: "Bind the app-owned account preferences settings section into local main client provider imports.",
|
|
296
|
+
category: "users-web",
|
|
297
|
+
id: "users-web-main-client-provider-account-settings-preferences-import"
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
op: "append-text",
|
|
301
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
302
|
+
position: "top",
|
|
303
|
+
skipIfContains: "import AccountSettingsNotificationsSection from \"/src/components/account/settings/AccountSettingsNotificationsSection.vue\";",
|
|
304
|
+
value: "import AccountSettingsNotificationsSection from \"/src/components/account/settings/AccountSettingsNotificationsSection.vue\";\n",
|
|
305
|
+
reason: "Bind the app-owned account notifications settings section into local main client provider imports.",
|
|
306
|
+
category: "users-web",
|
|
307
|
+
id: "users-web-main-client-provider-account-settings-notifications-import"
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
op: "append-text",
|
|
311
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
312
|
+
position: "bottom",
|
|
313
|
+
skipIfContains: "registerMainClientComponent(\"local.main.account-settings.section.profile\", () => AccountSettingsProfileSection);",
|
|
314
|
+
value: "\nregisterMainClientComponent(\"local.main.account-settings.section.profile\", () => AccountSettingsProfileSection);\n",
|
|
315
|
+
reason: "Bind the app-owned account profile settings section token into local main client provider registry.",
|
|
316
|
+
category: "users-web",
|
|
317
|
+
id: "users-web-main-client-provider-account-settings-profile-register"
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
op: "append-text",
|
|
321
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
322
|
+
position: "bottom",
|
|
323
|
+
skipIfContains: "registerMainClientComponent(\"local.main.account-settings.section.preferences\", () => AccountSettingsPreferencesSection);",
|
|
324
|
+
value: "\nregisterMainClientComponent(\"local.main.account-settings.section.preferences\", () => AccountSettingsPreferencesSection);\n",
|
|
325
|
+
reason: "Bind the app-owned account preferences settings section token into local main client provider registry.",
|
|
326
|
+
category: "users-web",
|
|
327
|
+
id: "users-web-main-client-provider-account-settings-preferences-register"
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
op: "append-text",
|
|
331
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
332
|
+
position: "bottom",
|
|
333
|
+
skipIfContains: "registerMainClientComponent(\"local.main.account-settings.section.notifications\", () => AccountSettingsNotificationsSection);",
|
|
334
|
+
value: "\nregisterMainClientComponent(\"local.main.account-settings.section.notifications\", () => AccountSettingsNotificationsSection);\n",
|
|
335
|
+
reason: "Bind the app-owned account notifications settings section token into local main client provider registry.",
|
|
336
|
+
category: "users-web",
|
|
337
|
+
id: "users-web-main-client-provider-account-settings-notifications-register"
|
|
250
338
|
}
|
|
251
339
|
]
|
|
252
340
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.74",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
9
|
"./client": "./src/client/index.js",
|
|
10
|
+
"./client/components/AccountSettingsClientElement": "./src/client/components/AccountSettingsClientElement.vue",
|
|
10
11
|
"./client/account-settings/sections": "./src/client/account-settings/sections.js",
|
|
11
12
|
"./client/composables/useAddEdit": "./src/client/composables/records/useAddEdit.js",
|
|
12
13
|
"./client/composables/useAccess": "./src/client/composables/useAccess.js",
|
|
@@ -24,7 +25,6 @@
|
|
|
24
25
|
"./client/composables/useView": "./src/client/composables/records/useView.js",
|
|
25
26
|
"./client/composables/useCrudView": "./src/client/composables/records/useCrudView.js",
|
|
26
27
|
"./client/composables/usePagedCollection": "./src/client/composables/usePagedCollection.js",
|
|
27
|
-
"./client/composables/useAccountSettingsRuntime": "./src/client/composables/useAccountSettingsRuntime.js",
|
|
28
28
|
"./client/composables/usePaths": "./src/client/composables/usePaths.js",
|
|
29
29
|
"./client/composables/runtime/useUiFeedback": "./src/client/composables/runtime/useUiFeedback.js",
|
|
30
30
|
"./client/lib/httpClient": "./src/client/lib/httpClient.js",
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@tanstack/vue-query": "5.92.12",
|
|
36
36
|
"@mdi/js": "^7.4.47",
|
|
37
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
38
|
-
"@jskit-ai/kernel": "0.1.
|
|
39
|
-
"@jskit-ai/realtime": "0.1.
|
|
40
|
-
"@jskit-ai/shell-web": "0.1.
|
|
41
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
42
|
-
"@jskit-ai/users-core": "0.1.
|
|
37
|
+
"@jskit-ai/http-runtime": "0.1.58",
|
|
38
|
+
"@jskit-ai/kernel": "0.1.59",
|
|
39
|
+
"@jskit-ai/realtime": "0.1.58",
|
|
40
|
+
"@jskit-ai/shell-web": "0.1.58",
|
|
41
|
+
"@jskit-ai/uploads-image-web": "0.1.37",
|
|
42
|
+
"@jskit-ai/users-core": "0.1.69",
|
|
43
43
|
"vuetify": "^4.0.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
@@ -9,11 +9,6 @@ import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
|
9
9
|
|
|
10
10
|
const ACCOUNT_SETTINGS_SECTION_TARGET = "account-settings:sections";
|
|
11
11
|
const EMPTY_ACCOUNT_SETTINGS_SECTIONS = Object.freeze([]);
|
|
12
|
-
const RESERVED_ACCOUNT_SETTINGS_SECTION_VALUES = Object.freeze([
|
|
13
|
-
"profile",
|
|
14
|
-
"preferences",
|
|
15
|
-
"notifications"
|
|
16
|
-
]);
|
|
17
12
|
const WEB_PLACEMENT_RUNTIME_INJECTION_KEY = "jskit.shell-web.runtime.web-placement.client";
|
|
18
13
|
|
|
19
14
|
function normalizeAccountSettingsSectionEntry(entry = null) {
|
|
@@ -53,20 +48,30 @@ function sortAccountSettingsSections(entries = []) {
|
|
|
53
48
|
}
|
|
54
49
|
|
|
55
50
|
function resolveAccountSettingsSections(entries = []) {
|
|
56
|
-
const seen = new Set(RESERVED_ACCOUNT_SETTINGS_SECTION_VALUES);
|
|
57
51
|
const normalized = [];
|
|
58
52
|
|
|
59
53
|
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
60
54
|
const resolved = normalizeAccountSettingsSectionEntry(entry);
|
|
61
|
-
if (!resolved
|
|
55
|
+
if (!resolved) {
|
|
62
56
|
continue;
|
|
63
57
|
}
|
|
64
58
|
|
|
65
|
-
seen.add(resolved.value);
|
|
66
59
|
normalized.push(resolved);
|
|
67
60
|
}
|
|
68
61
|
|
|
69
|
-
|
|
62
|
+
const sorted = sortAccountSettingsSections(normalized);
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const deduplicated = [];
|
|
65
|
+
for (const entry of sorted) {
|
|
66
|
+
if (seen.has(entry.value)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
seen.add(entry.value);
|
|
71
|
+
deduplicated.push(entry);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Object.freeze(deduplicated);
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
function useAccountSettingsSections() {
|
|
@@ -114,7 +119,6 @@ function useAccountSettingsSections() {
|
|
|
114
119
|
export {
|
|
115
120
|
ACCOUNT_SETTINGS_SECTION_TARGET,
|
|
116
121
|
EMPTY_ACCOUNT_SETTINGS_SECTIONS,
|
|
117
|
-
RESERVED_ACCOUNT_SETTINGS_SECTION_VALUES,
|
|
118
122
|
normalizeAccountSettingsSectionEntry,
|
|
119
123
|
resolveAccountSettingsSections,
|
|
120
124
|
sortAccountSettingsSections,
|
|
@@ -2,55 +2,25 @@
|
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import { useRoute, useRouter } from "vue-router";
|
|
4
4
|
import { normalizeOneOf } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
|
-
import { useAccountSettingsRuntime } from "
|
|
6
|
-
import { useAccountSettingsSections } from "
|
|
7
|
-
import AccountSettingsProfileSection from "./AccountSettingsProfileSection.vue";
|
|
8
|
-
import AccountSettingsPreferencesSection from "./AccountSettingsPreferencesSection.vue";
|
|
9
|
-
import AccountSettingsNotificationsSection from "./AccountSettingsNotificationsSection.vue";
|
|
5
|
+
import { useAccountSettingsRuntime } from "../composables/useAccountSettingsRuntime.js";
|
|
6
|
+
import { useAccountSettingsSections } from "../account-settings/sections.js";
|
|
10
7
|
|
|
11
8
|
const runtime = useAccountSettingsRuntime();
|
|
12
9
|
const route = useRoute();
|
|
13
10
|
const router = useRouter();
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
title: "Preferences",
|
|
21
|
-
value: "preferences",
|
|
22
|
-
component: AccountSettingsPreferencesSection,
|
|
23
|
-
usesSharedRuntime: true,
|
|
24
|
-
order: 200
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
title: "Notifications",
|
|
28
|
-
value: "notifications",
|
|
29
|
-
component: AccountSettingsNotificationsSection,
|
|
30
|
-
usesSharedRuntime: true,
|
|
31
|
-
order: 300
|
|
32
|
-
}
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
for (const entry of extensionSections.value) {
|
|
36
|
-
nextSections.push(entry);
|
|
11
|
+
const sections = useAccountSettingsSections();
|
|
12
|
+
const sectionValues = computed(() => Object.freeze(sections.value.map((section) => section.value)));
|
|
13
|
+
const defaultSection = computed(() => {
|
|
14
|
+
if (sectionValues.value.includes("profile")) {
|
|
15
|
+
return "profile";
|
|
37
16
|
}
|
|
38
17
|
|
|
39
|
-
return
|
|
40
|
-
nextSections.sort((left, right) => {
|
|
41
|
-
const orderDelta = Number(left.order || 0) - Number(right.order || 0);
|
|
42
|
-
if (orderDelta !== 0) {
|
|
43
|
-
return orderDelta;
|
|
44
|
-
}
|
|
45
|
-
return String(left.value || "").localeCompare(String(right.value || ""));
|
|
46
|
-
})
|
|
47
|
-
);
|
|
18
|
+
return sectionValues.value[0] || "";
|
|
48
19
|
});
|
|
49
|
-
const sectionValues = computed(() => Object.freeze(sections.value.map((section) => section.value)));
|
|
50
20
|
|
|
51
21
|
function normalizeSection(value) {
|
|
52
22
|
const source = Array.isArray(value) ? value[0] : value;
|
|
53
|
-
return normalizeOneOf(source, sectionValues.value,
|
|
23
|
+
return normalizeOneOf(source, sectionValues.value, defaultSection.value);
|
|
54
24
|
}
|
|
55
25
|
|
|
56
26
|
function readRouteSection() {
|
|
@@ -64,14 +34,14 @@ const activeTab = computed({
|
|
|
64
34
|
set(nextValue) {
|
|
65
35
|
const normalizedSection = normalizeSection(nextValue);
|
|
66
36
|
const currentSection = readRouteSection();
|
|
67
|
-
if (normalizedSection === currentSection) {
|
|
37
|
+
if (!normalizedSection || normalizedSection === currentSection) {
|
|
68
38
|
return;
|
|
69
39
|
}
|
|
70
40
|
|
|
71
41
|
const nextQuery = {
|
|
72
42
|
...route.query
|
|
73
43
|
};
|
|
74
|
-
if (normalizedSection ===
|
|
44
|
+
if (normalizedSection === defaultSection.value) {
|
|
75
45
|
delete nextQuery.section;
|
|
76
46
|
} else {
|
|
77
47
|
nextQuery.section = normalizedSection;
|
|
@@ -108,6 +78,9 @@ const activeTab = computed({
|
|
|
108
78
|
<v-skeleton-loader type="text@2, list-item-two-line@4" class="mb-4" />
|
|
109
79
|
<v-skeleton-loader type="text@2, paragraph, button" />
|
|
110
80
|
</template>
|
|
81
|
+
<template v-else-if="sections.length < 1">
|
|
82
|
+
<p class="text-body-2 text-medium-emphasis mb-0">No account settings sections are registered.</p>
|
|
83
|
+
</template>
|
|
111
84
|
<template v-else>
|
|
112
85
|
<v-progress-linear v-if="runtime.refreshingSettings.value" indeterminate class="mb-4" />
|
|
113
86
|
<v-row class="settings-layout" no-gutters>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeCrudLookupNamespace } from "@jskit-ai/kernel/shared/support/crudLookup";
|
|
3
|
+
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function resolveSchemaFieldDefinitions(definition = null) {
|
|
9
|
+
const schema = definition?.schema;
|
|
10
|
+
if (!schema || typeof schema.getFieldDefinitions !== "function") {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const definitions = schema.getFieldDefinitions();
|
|
15
|
+
return isRecord(definitions) ? definitions : {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveLookupFieldMap(resource = null) {
|
|
19
|
+
const outputDefinition =
|
|
20
|
+
resource?.operations?.view?.output ||
|
|
21
|
+
resource?.operations?.create?.output ||
|
|
22
|
+
resource?.operations?.patch?.output ||
|
|
23
|
+
null;
|
|
24
|
+
const mapping = {};
|
|
25
|
+
|
|
26
|
+
for (const [fieldKey, fieldDefinition] of Object.entries(resolveSchemaFieldDefinitions(outputDefinition))) {
|
|
27
|
+
const normalizedFieldDefinition = isRecord(fieldDefinition) ? fieldDefinition : {};
|
|
28
|
+
if (!normalizeText(normalizedFieldDefinition.belongsTo)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const relationshipName = normalizeText(normalizedFieldDefinition.as, {
|
|
33
|
+
fallback: fieldKey
|
|
34
|
+
});
|
|
35
|
+
const normalizedFieldKey = normalizeText(fieldKey);
|
|
36
|
+
if (!normalizedFieldKey || !relationshipName) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
mapping[relationshipName] = normalizedFieldKey;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Object.freeze(mapping);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isJsonApiResourceTransport(transport = null) {
|
|
47
|
+
return normalizeText(transport?.kind).toLowerCase() === "jsonapi-resource";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveCrudJsonApiResourceType(resource = null) {
|
|
51
|
+
return normalizeText(resource?.namespace);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function inferCrudLookupJsonApiTransport({ namespace = "", apiPath = "" } = {}) {
|
|
55
|
+
const resourceType =
|
|
56
|
+
normalizeCrudLookupNamespace(namespace) ||
|
|
57
|
+
normalizeCrudLookupNamespace(apiPath);
|
|
58
|
+
if (!resourceType) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Object.freeze({
|
|
63
|
+
kind: "jsonapi-resource",
|
|
64
|
+
responseType: resourceType,
|
|
65
|
+
responseKind: "collection"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function inferCrudJsonApiTransport(resource = null, { mode = "", operationName = "" } = {}) {
|
|
70
|
+
const resourceType = resolveCrudJsonApiResourceType(resource);
|
|
71
|
+
if (!resourceType) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const normalizedMode = normalizeText(mode).toLowerCase();
|
|
76
|
+
const normalizedOperationName = normalizeText(operationName).toLowerCase();
|
|
77
|
+
if (normalizedMode === "list") {
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
kind: "jsonapi-resource",
|
|
80
|
+
responseType: resourceType,
|
|
81
|
+
responseKind: "collection"
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (normalizedMode === "view") {
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
kind: "jsonapi-resource",
|
|
88
|
+
responseType: resourceType,
|
|
89
|
+
responseKind: "record"
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (normalizedMode === "add-edit") {
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
kind: "jsonapi-resource",
|
|
96
|
+
...(normalizedOperationName ? { requestType: resourceType } : {}),
|
|
97
|
+
responseType: resourceType,
|
|
98
|
+
responseKind: "record"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveCrudJsonApiTransport(transport = null, resource = null, options = {}) {
|
|
106
|
+
if (transport != null) {
|
|
107
|
+
throw new TypeError(
|
|
108
|
+
"CRUD hooks no longer accept explicit transport. Derive JSON:API transport from the shared resource instead."
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const baseTransport = inferCrudJsonApiTransport(resource, options);
|
|
113
|
+
if (!isJsonApiResourceTransport(baseTransport)) {
|
|
114
|
+
return baseTransport;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const lookupFieldMap = resolveLookupFieldMap(resource);
|
|
118
|
+
const lookupContainerKey = normalizeText(resource?.contract?.lookup?.containerKey, {
|
|
119
|
+
fallback: "lookups"
|
|
120
|
+
});
|
|
121
|
+
if (Object.keys(lookupFieldMap).length < 1 && !lookupContainerKey) {
|
|
122
|
+
return baseTransport;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
...baseTransport,
|
|
127
|
+
...(Object.keys(lookupFieldMap).length > 0 ? { lookupFieldMap } : {}),
|
|
128
|
+
...(lookupContainerKey ? { lookupContainerKey } : {})
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
inferCrudJsonApiTransport,
|
|
134
|
+
inferCrudLookupJsonApiTransport,
|
|
135
|
+
resolveCrudJsonApiTransport,
|
|
136
|
+
resolveLookupFieldMap
|
|
137
|
+
};
|
|
@@ -102,6 +102,15 @@ function resolveLookupFieldDescriptor(field = {}, relationKind = "", valueKey =
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
function resolveFallbackLookupKey(key = "") {
|
|
106
|
+
const normalizedKey = normalizeText(key);
|
|
107
|
+
if (!normalizedKey.endsWith("Id") || normalizedKey.length <= 2) {
|
|
108
|
+
return "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return normalizedKey.slice(0, -2);
|
|
112
|
+
}
|
|
113
|
+
|
|
105
114
|
function resolveLookupFieldDisplayValue(record = {}, field = {}, relationKind = "", valueKey = "", labelKey = "") {
|
|
106
115
|
const sourceRecord = asPlainObject(record);
|
|
107
116
|
const descriptor = resolveLookupFieldDescriptor(field, relationKind, valueKey, labelKey);
|
|
@@ -118,7 +127,10 @@ function resolveLookupFieldDisplayValue(record = {}, field = {}, relationKind =
|
|
|
118
127
|
context: `lookup relation "${key}" containerKey`
|
|
119
128
|
});
|
|
120
129
|
const sourceLookups = asPlainObject(sourceRecord[lookupContainerKey]);
|
|
121
|
-
|
|
130
|
+
let lookupRecord = asPlainObject(sourceLookups[key]);
|
|
131
|
+
if (Object.keys(lookupRecord).length < 1) {
|
|
132
|
+
lookupRecord = asPlainObject(sourceLookups[resolveFallbackLookupKey(key)]);
|
|
133
|
+
}
|
|
122
134
|
const lookupLabel = resolveLookupItemLabel(lookupRecord, descriptor.relation.labelKey);
|
|
123
135
|
if (lookupLabel) {
|
|
124
136
|
return lookupLabel;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolveLookupItemLabel,
|
|
12
12
|
resolveLookupFieldDisplayValue
|
|
13
13
|
} from "./crudLookupFieldLabelSupport.js";
|
|
14
|
+
import { inferCrudLookupJsonApiTransport } from "./crudJsonApiTransportSupport.js";
|
|
14
15
|
import { asPlainObject } from "../support/scopeHelpers.js";
|
|
15
16
|
|
|
16
17
|
function normalizeQueryKeyPrefix(value) {
|
|
@@ -104,6 +105,10 @@ function createCrudLookupFieldRuntime({
|
|
|
104
105
|
}
|
|
105
106
|
const explicitApiPath = normalizeCrudLookupApiPath(rawRelation.apiPath);
|
|
106
107
|
const apiPath = explicitApiPath || resolveCrudLookupApiPathFromNamespace(namespace);
|
|
108
|
+
const transport = inferCrudLookupJsonApiTransport({
|
|
109
|
+
namespace,
|
|
110
|
+
apiPath
|
|
111
|
+
});
|
|
107
112
|
const valueKey = normalizeText(rawRelation.valueKey);
|
|
108
113
|
const labelKey = normalizeText(rawRelation.labelKey);
|
|
109
114
|
const relationLookupContainerKey = normalizeCrudLookupContainerKey(rawRelation.containerKey, {
|
|
@@ -119,6 +124,7 @@ function createCrudLookupFieldRuntime({
|
|
|
119
124
|
adapter: adapter || undefined,
|
|
120
125
|
...(relationSurfaceId ? { surfaceId: relationSurfaceId } : {}),
|
|
121
126
|
apiSuffix: apiPath,
|
|
127
|
+
...(transport ? { transport } : {}),
|
|
122
128
|
queryKeyFactory: (surfaceId = "", scopeParamValue = "") => [
|
|
123
129
|
...normalizedQueryKeyPrefix,
|
|
124
130
|
key,
|
|
@@ -51,6 +51,10 @@ function isNullableFormField(field = {}) {
|
|
|
51
51
|
return field?.nullable === true;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function isLookupFormField(field = {}) {
|
|
55
|
+
return field?.component === "lookup" || field?.relation?.kind === "lookup";
|
|
56
|
+
}
|
|
57
|
+
|
|
54
58
|
function padDateTimePart(value) {
|
|
55
59
|
return String(value).padStart(2, "0");
|
|
56
60
|
}
|
|
@@ -138,6 +142,10 @@ function resolveFormFieldInitialValue(field = {}) {
|
|
|
138
142
|
return isNullableFormField(field) ? null : false;
|
|
139
143
|
}
|
|
140
144
|
|
|
145
|
+
if (isNullableFormField(field) && isLookupFormField(field)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
return "";
|
|
142
150
|
}
|
|
143
151
|
|
|
@@ -146,6 +154,10 @@ function shouldSerializeClearedFieldAsNull(field = {}) {
|
|
|
146
154
|
return false;
|
|
147
155
|
}
|
|
148
156
|
|
|
157
|
+
if (isLookupFormField(field)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
149
161
|
const fieldType = resolveFormFieldType(field);
|
|
150
162
|
const fieldFormat = resolveFormFieldFormat(field);
|
|
151
163
|
|
|
@@ -208,6 +220,19 @@ function buildCrudFormPayload(fields = [], model = {}) {
|
|
|
208
220
|
continue;
|
|
209
221
|
}
|
|
210
222
|
|
|
223
|
+
if (isLookupFormField(field)) {
|
|
224
|
+
const normalizedLookupValue = String(rawValue).trim();
|
|
225
|
+
if (!normalizedLookupValue) {
|
|
226
|
+
if (clearAsNull) {
|
|
227
|
+
payload[fieldKey] = null;
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
payload[fieldKey] = normalizedLookupValue;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
211
236
|
if (fieldFormat === "date") {
|
|
212
237
|
const normalizedValue = String(rawValue).trim();
|
|
213
238
|
if (!normalizedValue) {
|
|
@@ -284,6 +309,11 @@ function applyCrudPayloadToForm(fields = [], model = {}, payload = {}) {
|
|
|
284
309
|
continue;
|
|
285
310
|
}
|
|
286
311
|
|
|
312
|
+
if (isLookupFormField(field)) {
|
|
313
|
+
targetModel[fieldKey] = rawValue == null ? null : String(rawValue);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
287
317
|
targetModel[fieldKey] = rawValue == null ? "" : String(rawValue);
|
|
288
318
|
}
|
|
289
319
|
}
|
|
@@ -133,7 +133,11 @@ function resolveCrudListParentTitleFromItems(items = [], descriptor = null) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
for (const item of sourceItems) {
|
|
136
|
+
const rawParentValue = toRouteParamValue(item?.[descriptor.fieldKey]);
|
|
136
137
|
const resolvedTitle = normalizeText(resolveLookupFieldDisplayValue(item, descriptor.fieldDescriptor));
|
|
138
|
+
if (resolvedTitle && rawParentValue && resolvedTitle === rawParentValue) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
137
141
|
if (resolvedTitle) {
|
|
138
142
|
return resolvedTitle;
|
|
139
143
|
}
|