@jskit-ai/users-web 0.1.71 → 0.1.73

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.
@@ -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.71",
6
+ version: "0.1.73",
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: "templates/src/components/account/settings/AccountSettingsClientElement.vue"
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.55",
163
- "@jskit-ai/realtime": "0.1.55",
164
- "@jskit-ai/kernel": "0.1.56",
165
- "@jskit-ai/shell-web": "0.1.55",
166
- "@jskit-ai/uploads-image-web": "0.1.34",
167
- "@jskit-ai/users-core": "0.1.66",
186
+ "@jskit-ai/http-runtime": "0.1.57",
187
+ "@jskit-ai/realtime": "0.1.57",
188
+ "@jskit-ai/kernel": "0.1.58",
189
+ "@jskit-ai/shell-web": "0.1.57",
190
+ "@jskit-ai/uploads-image-web": "0.1.36",
191
+ "@jskit-ai/users-core": "0.1.68",
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.71",
3
+ "version": "0.1.73",
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.55",
38
- "@jskit-ai/kernel": "0.1.56",
39
- "@jskit-ai/realtime": "0.1.55",
40
- "@jskit-ai/shell-web": "0.1.55",
41
- "@jskit-ai/uploads-image-web": "0.1.34",
42
- "@jskit-ai/users-core": "0.1.66",
37
+ "@jskit-ai/http-runtime": "0.1.57",
38
+ "@jskit-ai/kernel": "0.1.58",
39
+ "@jskit-ai/realtime": "0.1.57",
40
+ "@jskit-ai/shell-web": "0.1.57",
41
+ "@jskit-ai/uploads-image-web": "0.1.36",
42
+ "@jskit-ai/users-core": "0.1.68",
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 || seen.has(resolved.value)) {
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
- return sortAccountSettingsSections(normalized);
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 "@jskit-ai/users-web/client/composables/useAccountSettingsRuntime";
6
- import { useAccountSettingsSections } from "@jskit-ai/users-web/client/account-settings/sections";
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 extensionSections = useAccountSettingsSections();
15
-
16
- const sections = computed(() => {
17
- const nextSections = [
18
- { title: "Profile", value: "profile", component: AccountSettingsProfileSection, usesSharedRuntime: true, order: 100 },
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 Object.freeze(
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, "profile");
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 === "profile") {
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
- const lookupRecord = asPlainObject(sourceLookups[key]);
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
  }
@@ -1,6 +1,7 @@
1
1
  import { computed, proxyRefs, reactive, watch } from "vue";
2
2
  import { useRoute, useRouter } from "vue-router";
3
3
  import { asPlainObject } from "../support/scopeHelpers.js";
4
+ import { resolveCrudJsonApiTransport } from "../crud/crudJsonApiTransportSupport.js";
4
5
  import { useAddEdit } from "./useAddEdit.js";
5
6
  import {
6
7
  resolveCrudBoundValues,
@@ -54,6 +55,15 @@ function useCrudAddEdit({
54
55
  const route = useRoute();
55
56
  const normalizedFields = normalizeCrudFormFields(formFields);
56
57
  const normalizedAddEditOptions = asPlainObject(addEditOptions);
58
+ const resolvedResource = normalizedAddEditOptions.resource || resource;
59
+ const resolvedTransport = resolveCrudJsonApiTransport(
60
+ normalizedAddEditOptions.transport,
61
+ resolvedResource,
62
+ {
63
+ mode: "add-edit",
64
+ operationName
65
+ }
66
+ );
57
67
  const saveSuccessOptions = normalizeSaveSuccessOptions(saveSuccess);
58
68
  const defaultFieldErrorKeys = normalizedFields.map((field) => field.key);
59
69
  const providedFieldErrorKeys = normalizeFieldErrorKeys(normalizedAddEditOptions.fieldErrorKeys);
@@ -101,7 +111,6 @@ function useCrudAddEdit({
101
111
  ? normalizedAddEditOptions.onSaveSuccess
102
112
  : null;
103
113
  const shouldApplyDefaultMapPayload = normalizedAddEditOptions.readEnabled !== false;
104
- const resolvedResource = normalizedAddEditOptions.resource || resource;
105
114
  const resolvedInput = inputOverride || resolvedResource?.operations?.[operationName]?.body || null;
106
115
 
107
116
  function resolveBuildRawPayload(model = {}, context = {}) {
@@ -170,6 +179,7 @@ function useCrudAddEdit({
170
179
  const addEdit = useAddEdit({
171
180
  ...normalizedAddEditOptions,
172
181
  resource: resolvedResource,
182
+ transport: resolvedTransport,
173
183
  model: form,
174
184
  fieldErrorKeys,
175
185
  input: resolvedInput,