@jskit-ai/users-web 0.1.36 → 0.1.37

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.
Files changed (32) hide show
  1. package/package.descriptor.mjs +8 -22
  2. package/package.json +7 -6
  3. package/src/client/components/MembersAdminClientElement.vue +5 -5
  4. package/src/client/components/WorkspaceMembersClientElement.vue +16 -16
  5. package/src/client/components/WorkspacesClientElement.vue +2 -2
  6. package/src/client/composables/accountSettingsAvatarUploadRuntime.js +26 -172
  7. package/src/client/composables/accountSettingsRuntimeConstants.js +0 -4
  8. package/src/client/composables/accountSettingsRuntimeHelpers.js +1 -1
  9. package/src/client/composables/addEditUiRuntime.js +11 -2
  10. package/src/client/composables/crudLookupFieldLabelSupport.js +36 -4
  11. package/src/client/composables/crudLookupFieldRuntime.js +5 -2
  12. package/src/client/composables/crudSchemaFormHelpers.js +23 -3
  13. package/src/client/composables/listQueryParamSupport.js +459 -0
  14. package/src/client/composables/listUiRuntime.js +18 -6
  15. package/src/client/composables/routeTemplateHelpers.js +122 -0
  16. package/src/client/composables/useAddEdit.js +10 -0
  17. package/src/client/composables/useList.js +242 -2
  18. package/src/client/composables/usePagedCollection.js +55 -4
  19. package/src/client/composables/useView.js +4 -1
  20. package/src/client/composables/viewUiRuntime.js +11 -2
  21. package/src/client/lib/bootstrap.js +1 -1
  22. package/src/client/lib/menuIcons.js +27 -6
  23. package/templates/src/components/WorkspaceNotFoundCard.vue +2 -1
  24. package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +1 -1
  25. package/test/addEditUiRuntime.test.js +18 -0
  26. package/test/crudLookupFieldRuntime.test.js +51 -1
  27. package/test/listQueryParamSupport.test.js +190 -0
  28. package/test/listUiRuntime.test.js +21 -0
  29. package/test/menuIcons.test.js +2 -0
  30. package/test/routeTemplateHelpers.test.js +56 -0
  31. package/test/usePagedCollection.test.js +53 -0
  32. package/test/viewUiRuntime.test.js +35 -0
@@ -1,12 +1,13 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/users-web",
4
- version: "0.1.36",
4
+ version: "0.1.37",
5
5
  kind: "runtime",
6
6
  description: "Users web module: workspace selector shell element plus workspace/profile/members UI elements.",
7
7
  dependsOn: [
8
8
  "@jskit-ai/http-runtime",
9
9
  "@jskit-ai/shell-web",
10
+ "@jskit-ai/uploads-image-web",
10
11
  "@jskit-ai/users-core"
11
12
  ],
12
13
  capabilities: {
@@ -32,17 +33,6 @@ export default Object.freeze({
32
33
  }
33
34
  },
34
35
  metadata: {
35
- client: {
36
- optimizeDeps: {
37
- include: [
38
- "@uppy/core",
39
- "@uppy/dashboard",
40
- "@uppy/image-editor",
41
- "@uppy/compressor",
42
- "@uppy/xhr-upload"
43
- ]
44
- }
45
- },
46
36
  apiSummary: {
47
37
  surfaces: [
48
38
  {
@@ -241,16 +231,12 @@ export default Object.freeze({
241
231
  runtime: {
242
232
  "@tanstack/vue-query": "5.92.12",
243
233
  "@mdi/js": "^7.4.47",
244
- "@uppy/compressor": "^3.1.0",
245
- "@uppy/core": "^5.2.0",
246
- "@uppy/dashboard": "^5.1.1",
247
- "@uppy/image-editor": "^4.2.0",
248
- "@uppy/xhr-upload": "^5.1.1",
249
- "@jskit-ai/http-runtime": "0.1.21",
250
- "@jskit-ai/realtime": "0.1.21",
251
- "@jskit-ai/kernel": "0.1.22",
252
- "@jskit-ai/shell-web": "0.1.21",
253
- "@jskit-ai/users-core": "0.1.31",
234
+ "@jskit-ai/http-runtime": "0.1.22",
235
+ "@jskit-ai/realtime": "0.1.22",
236
+ "@jskit-ai/kernel": "0.1.23",
237
+ "@jskit-ai/shell-web": "0.1.22",
238
+ "@jskit-ai/uploads-image-web": "0.1.1",
239
+ "@jskit-ai/users-core": "0.1.32",
254
240
  "vuetify": "^4.0.0"
255
241
  },
256
242
  dev: {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -22,11 +22,12 @@
22
22
  "dependencies": {
23
23
  "@tanstack/vue-query": "5.92.12",
24
24
  "@mdi/js": "^7.4.47",
25
- "@jskit-ai/users-core": "0.1.31",
26
- "@jskit-ai/realtime": "0.1.21",
27
- "@jskit-ai/http-runtime": "0.1.21",
28
- "@jskit-ai/kernel": "0.1.22",
29
- "@jskit-ai/shell-web": "0.1.21",
25
+ "@jskit-ai/http-runtime": "0.1.22",
26
+ "@jskit-ai/kernel": "0.1.23",
27
+ "@jskit-ai/realtime": "0.1.22",
28
+ "@jskit-ai/shell-web": "0.1.22",
29
+ "@jskit-ai/uploads-image-web": "0.1.1",
30
+ "@jskit-ai/users-core": "0.1.32",
30
31
  "vuetify": "^4.0.0"
31
32
  }
32
33
  }
@@ -44,7 +44,7 @@
44
44
  class="mb-3"
45
45
  />
46
46
  <v-select
47
- v-model="inviteForm.roleId"
47
+ v-model="inviteForm.roleSid"
48
48
  label="Role"
49
49
  :items="inviteRoleOptions"
50
50
  item-title="title"
@@ -105,7 +105,7 @@
105
105
  <template #append>
106
106
  <div class="d-flex align-center ga-2">
107
107
  <v-select
108
- v-model="member.roleId"
108
+ v-model="member.roleSid"
109
109
  :items="memberRoleOptions"
110
110
  item-title="title"
111
111
  item-value="value"
@@ -139,7 +139,7 @@
139
139
  {{ invite.email }}
140
140
  </template>
141
141
  <template #subtitle>
142
- Role: {{ invite.roleId }} • expires {{ formatDateTime(invite.expiresAt) }}
142
+ Role: {{ invite.roleSid }} • expires {{ formatDateTime(invite.expiresAt) }}
143
143
  </template>
144
144
  <template #append>
145
145
  <v-btn
@@ -375,12 +375,12 @@ async function onRevokeInvite(inviteId) {
375
375
  await actionHandlers.submitRevokeInvite(inviteId);
376
376
  }
377
377
 
378
- async function onMemberRoleUpdate(member, roleId) {
378
+ async function onMemberRoleUpdate(member, roleSid) {
379
379
  if (isMemberRoleLocked(member)) {
380
380
  return;
381
381
  }
382
382
 
383
- await actionHandlers.submitMemberRoleUpdate(member, roleId);
383
+ await actionHandlers.submitMemberRoleUpdate(member, roleSid);
384
384
  }
385
385
 
386
386
  async function onRemoveMember(member) {
@@ -37,7 +37,7 @@ import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/su
37
37
  const forms = reactive({
38
38
  invite: {
39
39
  email: "",
40
- roleId: "member"
40
+ roleSid: "member"
41
41
  },
42
42
  workspace: {
43
43
  invitesEnabled: false,
@@ -139,7 +139,7 @@ function clearRoleOptions() {
139
139
  function resetViewState() {
140
140
  resetMessages();
141
141
  forms.invite.email = "";
142
- forms.invite.roleId = "member";
142
+ forms.invite.roleSid = "member";
143
143
  forms.workspace.invitesEnabled = false;
144
144
  forms.workspace.invitesAvailable = false;
145
145
  collections.members = [];
@@ -149,8 +149,8 @@ function resetViewState() {
149
149
  removeMemberUserId.value = 0;
150
150
  }
151
151
 
152
- function toRoleTitle(roleId) {
153
- const normalizedRoleId = String(roleId || "").trim();
152
+ function toRoleTitle(roleSid) {
153
+ const normalizedRoleId = String(roleSid || "").trim();
154
154
  if (!normalizedRoleId) {
155
155
  return "";
156
156
  }
@@ -182,9 +182,9 @@ function normalizeRoleCatalog(payload = {}) {
182
182
  }
183
183
 
184
184
  const uniqueRoleIds = Array.from(new Set(assignableRoleIds));
185
- const roleOptions = uniqueRoleIds.map((roleId) => ({
186
- title: toRoleTitle(roleId),
187
- value: roleId
185
+ const roleOptions = uniqueRoleIds.map((roleSid) => ({
186
+ title: toRoleTitle(roleSid),
187
+ value: roleSid
188
188
  }));
189
189
 
190
190
  const defaultInviteRole = String(source.defaultInviteRole || "")
@@ -202,19 +202,19 @@ function applyRoleCatalog(payload = {}) {
202
202
  options.inviteRoleOptions = [...normalizedCatalog.roleOptions];
203
203
  options.memberRoleOptions = [...normalizedCatalog.roleOptions];
204
204
 
205
- const selectedInviteRole = String(forms.invite.roleId || "").trim().toLowerCase();
205
+ const selectedInviteRole = String(forms.invite.roleSid || "").trim().toLowerCase();
206
206
  const hasSelectedInviteRole = normalizedCatalog.roleOptions.some((entry) => entry.value === selectedInviteRole);
207
207
 
208
208
  if (
209
209
  normalizedCatalog.defaultInviteRole &&
210
210
  normalizedCatalog.roleOptions.some((entry) => entry.value === normalizedCatalog.defaultInviteRole)
211
211
  ) {
212
- forms.invite.roleId = normalizedCatalog.defaultInviteRole;
212
+ forms.invite.roleSid = normalizedCatalog.defaultInviteRole;
213
213
  return;
214
214
  }
215
215
 
216
216
  if (!hasSelectedInviteRole && normalizedCatalog.roleOptions.length > 0) {
217
- forms.invite.roleId = normalizedCatalog.roleOptions[0].value;
217
+ forms.invite.roleSid = normalizedCatalog.roleOptions[0].value;
218
218
  }
219
219
  }
220
220
 
@@ -224,7 +224,7 @@ function normalizeMembers(entries) {
224
224
  const value = entry && typeof entry === "object" ? entry : {};
225
225
  return {
226
226
  userId: Number(value.userId || 0),
227
- roleId: String(value.roleId || "").trim().toLowerCase(),
227
+ roleSid: String(value.roleSid || "").trim().toLowerCase(),
228
228
  status: String(value.status || "").trim().toLowerCase(),
229
229
  displayName: String(value.displayName || "").trim(),
230
230
  email: String(value.email || "").trim().toLowerCase(),
@@ -240,7 +240,7 @@ function normalizeInvites(entries) {
240
240
  return {
241
241
  id: Number(value.id || 0),
242
242
  email: String(value.email || "").trim().toLowerCase(),
243
- roleId: String(value.roleId || "").trim().toLowerCase(),
243
+ roleSid: String(value.roleSid || "").trim().toLowerCase(),
244
244
  status: String(value.status || "").trim().toLowerCase(),
245
245
  expiresAt: value.expiresAt || "",
246
246
  invitedByUserId: value.invitedByUserId == null ? null : Number(value.invitedByUserId)
@@ -319,7 +319,7 @@ const inviteCreateCommand = useCommand({
319
319
  fallbackRunError: "Unable to send invite.",
320
320
  buildRawPayload: () => ({
321
321
  email: forms.invite.email,
322
- roleId: forms.invite.roleId
322
+ roleSid: forms.invite.roleSid
323
323
  }),
324
324
  messages: {
325
325
  success: "Invite sent.",
@@ -352,7 +352,7 @@ const memberRoleCommand = useCommand({
352
352
  writeMethod: "PATCH",
353
353
  fallbackRunError: "Unable to update member role.",
354
354
  buildRawPayload: (_model, { context }) => ({
355
- roleId: String(context?.roleId || "").trim().toLowerCase()
355
+ roleSid: String(context?.roleSid || "").trim().toLowerCase()
356
356
  }),
357
357
  buildCommandOptions: (_parsed, { context }) => {
358
358
  return {
@@ -595,7 +595,7 @@ async function submitRevokeInvite(inviteId) {
595
595
  }
596
596
  }
597
597
 
598
- async function submitMemberRoleUpdate(member, roleId) {
598
+ async function submitMemberRoleUpdate(member, roleSid) {
599
599
  if (!canManageMembers.value) {
600
600
  return;
601
601
  }
@@ -610,7 +610,7 @@ async function submitMemberRoleUpdate(member, roleId) {
610
610
 
611
611
  await memberRoleCommand.run({
612
612
  memberUserId,
613
- roleId
613
+ roleSid
614
614
  });
615
615
  await Promise.all([
616
616
  workspaceMembersList.reload(),
@@ -421,7 +421,7 @@ watch(
421
421
  :title="workspace.name"
422
422
  :subtitle="
423
423
  workspace.isAccessible
424
- ? `/${workspace.slug} • role: ${workspace.roleId || 'member'}`
424
+ ? `/${workspace.slug} • role: ${workspace.roleSid || 'member'}`
425
425
  : `/${workspace.slug} • unavailable on this surface`
426
426
  "
427
427
  class="px-0"
@@ -466,7 +466,7 @@ watch(
466
466
  v-for="invite in pendingInvites"
467
467
  :key="invite.id"
468
468
  :title="invite.workspaceName"
469
- :subtitle="`Role: ${invite.roleId}`"
469
+ :subtitle="`Role: ${invite.roleSid}`"
470
470
  class="px-0"
471
471
  >
472
472
  <template #prepend>
@@ -1,36 +1,7 @@
1
- import Uppy from "@uppy/core";
2
- import Dashboard from "@uppy/dashboard";
3
- import ImageEditor from "@uppy/image-editor";
4
- import Compressor from "@uppy/compressor";
5
- import XHRUpload from "@uppy/xhr-upload";
6
- import "@uppy/core/css/style.min.css";
7
- import "@uppy/dashboard/css/style.min.css";
8
- import "@uppy/image-editor/css/style.min.css";
1
+ import "@jskit-ai/uploads-image-web/client/styles";
2
+ import { createImageUploadRuntime } from "@jskit-ai/uploads-image-web/client/composables/createImageUploadRuntime";
9
3
  import { resolveFieldErrors } from "@jskit-ai/http-runtime/client";
10
4
  import { usersWebHttpClient } from "../lib/httpClient.js";
11
- import {
12
- AVATAR_ALLOWED_MIME_TYPES,
13
- AVATAR_MAX_UPLOAD_BYTES
14
- } from "./accountSettingsRuntimeConstants.js";
15
-
16
- function parseUploadResponse(xhr) {
17
- if (!xhr.responseText) {
18
- return {};
19
- }
20
-
21
- try {
22
- return JSON.parse(xhr.responseText);
23
- } catch {
24
- return {};
25
- }
26
- }
27
-
28
- function stopImageEditor(uppy) {
29
- const imageEditor = uppy.getPlugin("ImageEditor");
30
- if (imageEditor && typeof imageEditor.stop === "function") {
31
- imageEditor.stop();
32
- }
33
- }
34
5
 
35
6
  function createAccountSettingsAvatarUploadRuntime({
36
7
  queryClient,
@@ -40,8 +11,6 @@ function createAccountSettingsAvatarUploadRuntime({
40
11
  applySettingsData,
41
12
  reportAccountFeedback
42
13
  } = {}) {
43
- let avatarUppy = null;
44
-
45
14
  async function resolveCsrfToken() {
46
15
  const sessionPayload = await queryClient.fetchQuery({
47
16
  queryKey: sessionQueryKey,
@@ -60,94 +29,16 @@ function createAccountSettingsAvatarUploadRuntime({
60
29
  return csrfToken;
61
30
  }
62
31
 
63
- function setup() {
64
- if (typeof window === "undefined") {
65
- return;
66
- }
67
-
68
- if (avatarUppy) {
69
- return;
70
- }
71
-
72
- const uppy = new Uppy({
73
- autoProceed: false,
74
- restrictions: {
75
- maxNumberOfFiles: 1,
76
- allowedFileTypes: [...AVATAR_ALLOWED_MIME_TYPES],
77
- maxFileSize: AVATAR_MAX_UPLOAD_BYTES
78
- }
79
- });
80
-
81
- uppy.use(Dashboard, {
82
- inline: false,
83
- closeAfterFinish: false,
84
- showProgressDetails: true,
85
- proudlyDisplayPoweredByUppy: false,
86
- hideUploadButton: false,
87
- doneButtonHandler: () => {
88
- const dashboard = uppy.getPlugin("Dashboard");
89
- if (dashboard && typeof dashboard.closeModal === "function") {
90
- dashboard.closeModal();
91
- }
92
- },
93
- note: `Accepted: ${AVATAR_ALLOWED_MIME_TYPES.join(", ")}, max ${Math.floor(AVATAR_MAX_UPLOAD_BYTES / (1024 * 1024))}MB`
94
- });
95
-
96
- uppy.use(ImageEditor, {
97
- quality: 0.9
98
- });
99
-
100
- uppy.use(Compressor, {
101
- quality: 0.84,
102
- limit: 1
103
- });
104
-
105
- uppy.use(XHRUpload, {
106
- endpoint: "/api/settings/profile/avatar",
107
- method: "POST",
108
- formData: true,
109
- fieldName: "avatar",
110
- withCredentials: true,
111
- onBeforeRequest: async (xhr) => {
112
- const csrfToken = await resolveCsrfToken();
113
- xhr.setRequestHeader("csrf-token", csrfToken);
114
- },
115
- getResponseData: parseUploadResponse
116
- });
117
-
118
- uppy.on("file-added", (file) => {
119
- selectedAvatarFileName.value = String(file?.name || "");
120
- });
121
-
122
- uppy.on("file-removed", () => {
123
- selectedAvatarFileName.value = "";
124
- });
125
-
126
- uppy.on("file-editor:complete", (file) => {
127
- selectedAvatarFileName.value = String(file?.name || selectedAvatarFileName.value || "");
128
- stopImageEditor(uppy);
129
- });
130
-
131
- uppy.on("file-editor:cancel", () => {
132
- stopImageEditor(uppy);
133
- });
134
-
135
- uppy.on("dashboard:modal-closed", () => {
136
- stopImageEditor(uppy);
137
- });
138
-
139
- uppy.on("upload-success", (_file, response) => {
140
- const data = response?.body;
141
- if (!data || typeof data !== "object") {
142
- reportAccountFeedback({
143
- message: "Avatar uploaded, but the response payload was invalid.",
144
- severity: "error",
145
- channel: "banner",
146
- dedupeKey: "users-web.account-settings-runtime:avatar-upload-invalid-response"
147
- });
148
- return;
149
- }
150
-
32
+ return createImageUploadRuntime({
33
+ endpoint: "/api/settings/profile/avatar",
34
+ fieldName: "avatar",
35
+ resolveRequestHeaders: async () => ({
36
+ "csrf-token": await resolveCsrfToken()
37
+ }),
38
+ onSelectedFileNameChanged: (fileName) => {
39
+ selectedAvatarFileName.value = String(fileName || "");
40
+ },
41
+ onUploadSuccess: ({ data, uppy }) => {
151
42
  applySettingsData(data);
152
43
  queryClient.setQueryData(accountSettingsQueryKey, data);
153
44
 
@@ -162,10 +53,16 @@ function createAccountSettingsAvatarUploadRuntime({
162
53
  channel: "snackbar",
163
54
  dedupeKey: "users-web.account-settings-runtime:avatar-uploaded"
164
55
  });
165
- selectedAvatarFileName.value = "";
166
- });
167
-
168
- uppy.on("upload-error", (_file, error, response) => {
56
+ },
57
+ onInvalidResponse: () => {
58
+ reportAccountFeedback({
59
+ message: "Avatar uploaded, but the response payload was invalid.",
60
+ severity: "error",
61
+ channel: "banner",
62
+ dedupeKey: "users-web.account-settings-runtime:avatar-upload-invalid-response"
63
+ });
64
+ },
65
+ onUploadError: ({ error, response }) => {
169
66
  const body = response?.body && typeof response.body === "object" ? response.body : {};
170
67
  const fieldErrors = resolveFieldErrors(body);
171
68
 
@@ -175,66 +72,23 @@ function createAccountSettingsAvatarUploadRuntime({
175
72
  channel: "banner",
176
73
  dedupeKey: "users-web.account-settings-runtime:avatar-upload-error"
177
74
  });
178
- });
179
-
180
- uppy.on("restriction-failed", (_file, error) => {
75
+ },
76
+ onRestrictionFailed: ({ error }) => {
181
77
  reportAccountFeedback({
182
78
  message: String(error?.message || "Selected avatar file does not meet upload restrictions."),
183
79
  severity: "error",
184
80
  channel: "banner",
185
81
  dedupeKey: "users-web.account-settings-runtime:avatar-upload-restriction"
186
82
  });
187
- });
188
-
189
- uppy.on("complete", (result) => {
190
- const successfulCount = Array.isArray(result?.successful) ? result.successful.length : 0;
191
- if (successfulCount <= 0) {
192
- return;
193
- }
194
-
195
- try {
196
- uppy.clear();
197
- } catch {
198
- // Upload succeeded; ignore clear timing issues.
199
- }
200
- });
201
-
202
- avatarUppy = uppy;
203
- }
204
-
205
- function openEditor() {
206
- setup();
207
-
208
- const uppy = avatarUppy;
209
- if (!uppy) {
83
+ },
84
+ onUnavailable: () => {
210
85
  reportAccountFeedback({
211
86
  message: "Avatar editor is unavailable in this environment.",
212
87
  severity: "error",
213
88
  channel: "banner",
214
89
  dedupeKey: "users-web.account-settings-runtime:avatar-editor-unavailable"
215
90
  });
216
- return;
217
91
  }
218
-
219
- const dashboard = uppy.getPlugin("Dashboard");
220
- if (dashboard && typeof dashboard.openModal === "function") {
221
- dashboard.openModal();
222
- }
223
- }
224
-
225
- function destroy() {
226
- if (!avatarUppy) {
227
- return;
228
- }
229
-
230
- avatarUppy.destroy();
231
- avatarUppy = null;
232
- }
233
-
234
- return Object.freeze({
235
- destroy,
236
- openEditor,
237
- setup
238
92
  });
239
93
  }
240
94
 
@@ -1,5 +1,3 @@
1
- const AVATAR_ALLOWED_MIME_TYPES = Object.freeze(["image/jpeg", "image/png", "image/webp"]);
2
- const AVATAR_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
3
1
  const AVATAR_DEFAULT_SIZE = 64;
4
2
 
5
3
  const THEME_OPTIONS = Object.freeze([
@@ -64,9 +62,7 @@ const ACCOUNT_SETTINGS_DEFAULTS = Object.freeze({
64
62
 
65
63
  export {
66
64
  ACCOUNT_SETTINGS_DEFAULTS,
67
- AVATAR_ALLOWED_MIME_TYPES,
68
65
  AVATAR_DEFAULT_SIZE,
69
- AVATAR_MAX_UPLOAD_BYTES,
70
66
  AVATAR_SIZE_OPTIONS,
71
67
  CURRENCY_OPTIONS,
72
68
  DATE_FORMAT_OPTIONS,
@@ -50,7 +50,7 @@ function normalizePendingInvite(entry) {
50
50
  workspaceSlug,
51
51
  workspaceName: String(entry.workspaceName || workspaceSlug).trim() || workspaceSlug,
52
52
  workspaceAvatarUrl: String(entry.workspaceAvatarUrl || "").trim(),
53
- roleId: String(entry.roleId || "member").trim().toLowerCase() || "member",
53
+ roleSid: String(entry.roleSid || "member").trim().toLowerCase() || "member",
54
54
  status: String(entry.status || "pending").trim().toLowerCase() || "pending",
55
55
  expiresAt: String(entry.expiresAt || "").trim()
56
56
  };
@@ -3,7 +3,7 @@ import { asPlainObject } from "./scopeHelpers.js";
3
3
  import {
4
4
  normalizeRouteParamName,
5
5
  resolveRouteParamsSource,
6
- resolveRoutePathnameSource,
6
+ resolveScopedRoutePathname,
7
7
  resolveRouteTemplateLocation,
8
8
  toRouteParamValue
9
9
  } from "./routeTemplateHelpers.js";
@@ -31,6 +31,7 @@ function resolveSavedRecordId(payload, saveRecordIdSelector) {
31
31
  function createAddEditUiRuntime({
32
32
  recordIdParam = "recordId",
33
33
  routeParams = null,
34
+ routeParamNames = null,
34
35
  routePath = "",
35
36
  routeRecordId = null,
36
37
  apiUrlTemplate = "",
@@ -63,10 +64,18 @@ function createAddEditUiRuntime({
63
64
  routeRecordId
64
65
  });
65
66
  sourceParams[normalizedRecordIdParam] = resolvedRecordId;
67
+ const currentPathname = resolveScopedRoutePathname({
68
+ currentPathname: routePath,
69
+ params: currentRouteParams,
70
+ orderedParamNames: routeParamNames,
71
+ anchorParamName: normalizedRecordIdParam,
72
+ anchorParamValue: resolvedRecordId,
73
+ anchorMode: "after"
74
+ });
66
75
 
67
76
  return resolveRouteTemplateLocation(normalizedTemplate, {
68
77
  params: sourceParams,
69
- currentPathname: resolveRoutePathnameSource(routePath)
78
+ currentPathname
70
79
  });
71
80
  }
72
81
 
@@ -4,10 +4,13 @@ import { asPlainObject } from "./scopeHelpers.js";
4
4
 
5
5
  const LOOKUP_LABEL_COMPOSITION_CANDIDATES = Object.freeze([
6
6
  Object.freeze(["name", "surname"]),
7
+ Object.freeze(["name", "lastName"]),
7
8
  Object.freeze(["firstName", "surname"]),
9
+ Object.freeze(["firstName", "lastName"]),
8
10
  Object.freeze(["name"]),
9
11
  Object.freeze(["firstName"])
10
12
  ]);
13
+ const DEFAULT_RECORD_TITLE = "-";
11
14
 
12
15
  function hasDisplayValue(value) {
13
16
  if (value == null) {
@@ -20,9 +23,8 @@ function hasDisplayValue(value) {
20
23
  return true;
21
24
  }
22
25
 
23
- function resolveLookupItemLabel(item = {}, labelKey = "") {
24
- const source = asPlainObject(item);
25
- for (const candidate of LOOKUP_LABEL_COMPOSITION_CANDIDATES) {
26
+ function resolveComposedLabel(source = {}, candidates = LOOKUP_LABEL_COMPOSITION_CANDIDATES) {
27
+ for (const candidate of candidates) {
26
28
  const parts = [];
27
29
  for (const key of candidate) {
28
30
  const part = normalizeText(source[key]);
@@ -37,6 +39,16 @@ function resolveLookupItemLabel(item = {}, labelKey = "") {
37
39
  }
38
40
  }
39
41
 
42
+ return "";
43
+ }
44
+
45
+ function resolveLookupItemLabel(item = {}, labelKey = "") {
46
+ const source = asPlainObject(item);
47
+ const composedLabel = resolveComposedLabel(source);
48
+ if (composedLabel) {
49
+ return composedLabel;
50
+ }
51
+
40
52
  const normalizedLabelKey = normalizeText(labelKey);
41
53
  if (!normalizedLabelKey) {
42
54
  return "";
@@ -45,6 +57,25 @@ function resolveLookupItemLabel(item = {}, labelKey = "") {
45
57
  return normalizeText(source[normalizedLabelKey]);
46
58
  }
47
59
 
60
+ function resolveRecordTitle(record = {}, { fallbackKey = "", defaultValue = DEFAULT_RECORD_TITLE } = {}) {
61
+ const source = asPlainObject(record);
62
+ const composedLabel = resolveComposedLabel(source);
63
+ if (composedLabel) {
64
+ return composedLabel;
65
+ }
66
+
67
+ const normalizedFallbackKey = normalizeText(fallbackKey);
68
+ if (normalizedFallbackKey) {
69
+ const fallbackValue = normalizeText(source[normalizedFallbackKey]);
70
+ if (fallbackValue) {
71
+ return fallbackValue;
72
+ }
73
+ }
74
+
75
+ const normalizedDefaultValue = normalizeText(defaultValue);
76
+ return normalizedDefaultValue || DEFAULT_RECORD_TITLE;
77
+ }
78
+
48
79
  function resolveLookupFieldDescriptor(field = {}, relationKind = "", valueKey = "", labelKey = "") {
49
80
  if (typeof field === "string") {
50
81
  return {
@@ -103,5 +134,6 @@ function resolveLookupFieldDisplayValue(record = {}, field = {}, relationKind =
103
134
 
104
135
  export {
105
136
  resolveLookupItemLabel,
106
- resolveLookupFieldDisplayValue
137
+ resolveLookupFieldDisplayValue,
138
+ resolveRecordTitle
107
139
  };
@@ -66,7 +66,8 @@ function createSelectedLookupItem(selectedValue, selectedRecord = {}, entry = {}
66
66
  const label = displayValue == null || displayValue === "" ? value : displayValue;
67
67
  return {
68
68
  value,
69
- label: String(label ?? "")
69
+ label: String(label ?? ""),
70
+ record: hydratedLookup
70
71
  };
71
72
  }
72
73
 
@@ -161,12 +162,14 @@ function createCrudLookupFieldRuntime({
161
162
  }
162
163
 
163
164
  const items = (Array.isArray(entry.runtime.items) ? entry.runtime.items : []).map((item = {}) => {
165
+ const sourceRecord = asPlainObject(item);
164
166
  const value = normalizeLookupValue(item?.[entry.valueKey]);
165
167
  const resolvedLabel = resolveLookupItemLabel(item, entry.labelKey);
166
168
  const label = resolvedLabel || value;
167
169
  return {
168
170
  value,
169
- label: String(label ?? "")
171
+ label: String(label ?? ""),
172
+ record: sourceRecord
170
173
  };
171
174
  });
172
175