@lobb-js/studio 0.26.0 → 0.27.1

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 (30) hide show
  1. package/dist/components/dataTable/dataTable.svelte +70 -9
  2. package/dist/components/dataTable/dataTable.svelte.d.ts +24 -0
  3. package/dist/components/dataTable/header.svelte +88 -24
  4. package/dist/components/dataTable/header.svelte.d.ts +4 -0
  5. package/dist/components/dataTable/listViewChildren.svelte +10 -3
  6. package/dist/components/dataTable/table.svelte +1 -1
  7. package/dist/components/detailView/create/createDetailView.svelte +1 -6
  8. package/dist/components/detailView/update/detailViewChildren.svelte +50 -55
  9. package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +8 -1
  10. package/dist/components/detailView/update/updateDetailView.svelte +6 -6
  11. package/dist/components/detailView/update/updateDetailViewButton.svelte +2 -3
  12. package/dist/components/detailView/utils.d.ts +0 -2
  13. package/dist/components/detailView/utils.js +5 -81
  14. package/dist/components/foreingKeyInput.svelte +42 -17
  15. package/dist/components/foreingKeyInput.svelte.d.ts +1 -1
  16. package/dist/components/polymorphicInput.svelte +25 -9
  17. package/dist/utils.js +2 -1
  18. package/package.json +2 -2
  19. package/src/lib/components/dataTable/dataTable.svelte +70 -9
  20. package/src/lib/components/dataTable/header.svelte +88 -24
  21. package/src/lib/components/dataTable/listViewChildren.svelte +10 -3
  22. package/src/lib/components/dataTable/table.svelte +1 -1
  23. package/src/lib/components/detailView/create/createDetailView.svelte +1 -6
  24. package/src/lib/components/detailView/update/detailViewChildren.svelte +50 -55
  25. package/src/lib/components/detailView/update/updateDetailView.svelte +6 -6
  26. package/src/lib/components/detailView/update/updateDetailViewButton.svelte +2 -3
  27. package/src/lib/components/detailView/utils.ts +2 -64
  28. package/src/lib/components/foreingKeyInput.svelte +42 -17
  29. package/src/lib/components/polymorphicInput.svelte +25 -9
  30. package/src/lib/utils.ts +2 -1
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getStudioContext } from "../../context";
3
- import { ChevronRight, Table, Plus } from "lucide-svelte";
3
+ import { ChevronRight, Table, Plus, Link, ArrowLeftRight, GitFork } from "lucide-svelte";
4
4
  import DataTable from "./dataTable.svelte";
5
5
  import CreateDetailViewButton from "../detailView/create/createDetailViewButton.svelte";
6
6
  import ExtensionsComponents from "../extensionsComponents.svelte";
@@ -18,7 +18,7 @@
18
18
  let { collectionName, recordId, width, unifiedBgColor }: Props = $props();
19
19
 
20
20
  const children = (ctx.meta.collections[collectionName]?.children ?? [])
21
- .filter((c: any) => c.type === "fk" || c.type === "m2m");
21
+ .filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
22
22
 
23
23
  let expandedRows: boolean[] = $state(new Array(children.length).fill(false));
24
24
  let refreshDataTable = $state(true);
@@ -49,6 +49,13 @@
49
49
  />
50
50
  <Table size="17.5" class="text-muted-foreground" />
51
51
  <div class="text-muted-foreground">{child.collection}</div>
52
+ {#if child.type === "fk"}
53
+ <span title="Direct (FK)"><Link size="13" class="text-muted-foreground/50" /></span>
54
+ {:else if child.type === "m2m"}
55
+ <span title="Many to Many"><ArrowLeftRight size="13" class="text-muted-foreground/50" /></span>
56
+ {:else if child.type === "polymorphic"}
57
+ <span title="Polymorphic"><GitFork size="13" class="text-muted-foreground/50" /></span>
58
+ {/if}
52
59
  </button>
53
60
  {#if child.type === "fk"}
54
61
  <div class="flex items-center px-2">
@@ -57,7 +64,7 @@
57
64
  variant="ghost"
58
65
  class="h-7 px-3 text-xs font-normal"
59
66
  Icon={Plus}
60
- values={{ [child.field]: { id: recordId } }}
67
+ values={{ [child.field]: recordId }}
61
68
  onSuccessfullSave={async () => { refreshDataTable = !refreshDataTable; }}
62
69
  >
63
70
  Create
@@ -172,7 +172,7 @@
172
172
  <div
173
173
  style="
174
174
  display: grid;
175
- grid-template-columns: minmax(auto, 7.5rem) repeat({columnsLength - 1}, minmax(auto, 15rem)){rowActionsExists ? ' minmax(auto, 7.5rem)' : ''};
175
+ grid-template-columns: minmax(auto, 10rem) repeat({columnsLength - 1}, minmax(auto, 15rem)){rowActionsExists ? ' minmax(auto, 7.5rem)' : ''};
176
176
  grid-template-rows: 2.5rem;
177
177
  "
178
178
  >
@@ -30,8 +30,6 @@
30
30
  import {
31
31
  buildChildren,
32
32
  getDefaultEntry,
33
- parseDetailViewValues,
34
- serializeEntry,
35
33
  } from "../utils";
36
34
  import { getChangedProperties } from "../../../utils";
37
35
  import type { Snippet } from "svelte";
@@ -50,8 +48,6 @@
50
48
  submitButton,
51
49
  }: CreateDetailViewProp = $props();
52
50
 
53
- parseDetailViewValues(ctx, collectionName, values)
54
-
55
51
  const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
56
52
  let entry: Record<string, any> = $state(
57
53
  getDefaultEntry(ctx, fieldNames, collectionName, values),
@@ -88,9 +84,8 @@
88
84
  return;
89
85
  }
90
86
 
91
- const serializedEntry = serializeEntry(ctx, collectionName, localEntry);
92
87
  const children = buildChildren(ctx, collectionName, localEntry);
93
- const response = await lobb.createOne(collectionName, serializedEntry, children);
88
+ const response = await lobb.createOne(collectionName, localEntry, children);
94
89
 
95
90
  await emitEvent({ lobb, ctx }, "studio.collections.create", {
96
91
  collectionName,
@@ -1,77 +1,72 @@
1
1
  <script lang="ts">
2
- import DataTable from "../../../components/dataTable/dataTable.svelte";
2
+ import DataTable, { type RecordOperation } from "../../../components/dataTable/dataTable.svelte";
3
3
  import { getStudioContext } from "../../../context";
4
- import { Link, Plus, TableIcon } from "lucide-svelte";
5
- import CreateDetailViewButton from "../create/createDetailViewButton.svelte";
6
- import ExtensionsComponents from "../../../components/extensionsComponents.svelte";
7
- import { getExtensionUtils } from "../../../extensions/extensionUtils";
4
+ import { Table, Link } from "lucide-svelte";
8
5
 
9
- const { ctx, lobb } = getStudioContext();
6
+ const { ctx } = getStudioContext();
7
+
8
+ type PendingOps = {
9
+ link?: (string | number)[];
10
+ unlink?: (string | number)[];
11
+ delete?: (string | number)[];
12
+ create?: any[];
13
+ };
10
14
 
11
15
  interface LocalProp {
12
16
  collectionName: string;
13
17
  entry: any;
18
+ pendingChildren?: Record<string, PendingOps>;
14
19
  }
15
20
 
16
- let { collectionName, entry }: LocalProp = $props();
21
+ let { collectionName, entry, pendingChildren = $bindable({}) }: LocalProp = $props();
17
22
 
18
- const children = (ctx.meta.collections[collectionName]?.children ?? [])
19
- .filter((c: any) => c.type === "fk" || c.type === "m2m");
23
+ function makeHandler(collection: string) {
24
+ return (op: RecordOperation) => {
25
+ if (!pendingChildren[collection]) pendingChildren[collection] = {};
26
+ const c = pendingChildren[collection];
27
+ if (op.type === "link") {
28
+ (c.link ??= []).push(op.record.id);
29
+ } else if (op.type === "unlink") {
30
+ (c.unlink ??= []).push(op.id);
31
+ } else if (op.type === "delete") {
32
+ (c.delete ??= []).push(op.id);
33
+ } else if (op.type === "create") {
34
+ (c.create ??= []).push(op.record);
35
+ }
36
+ };
37
+ }
20
38
 
21
- const refresh: boolean[] = $state(new Array(children.length).fill(true));
39
+ const children = (ctx.meta.collections[collectionName]?.children ?? [])
40
+ .filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
22
41
  </script>
23
42
 
24
43
  {#if children.length}
25
- <div class="flex flex-col gap-4 border-t p-4">
44
+ <div class="flex flex-col gap-3 border-t p-4">
26
45
  <div class="flex items-center gap-2">
27
- <Link size="17.5" />
28
- <div>Sub Records</div>
46
+ <Link size="14" class="text-muted-foreground" />
47
+ <span class="text-sm font-medium">Sub Records</span>
29
48
  </div>
30
- <div class="flex flex-col gap-4">
31
- {#each children as child, index}
32
- <ExtensionsComponents
33
- name="detailView.update.subRecords.{child.collection}"
34
- utils={getExtensionUtils(lobb, ctx)}
49
+ {#each children as child}
50
+ <div class="rounded-lg border bg-background overflow-hidden flex flex-col max-h-96">
51
+ <DataTable
35
52
  collectionName={child.collection}
36
53
  searchParams={{ children_of: collectionName, parent_id: entry.id }}
37
- class="bg-muted/30 border rounded-md overflow-hidden"
54
+ parentContext={{ collectionName, recordId: entry.id }}
55
+ onOperation={makeHandler(child.collection)}
56
+ showImport={false}
57
+ showHeader={true}
58
+ showFooter={true}
59
+ showDelete={child.type === "fk" || child.type === "m2m"}
60
+ tableProps={{ showLastColumnBorder: false, showLastRowBorder: true }}
38
61
  >
39
- <div class="border rounded-md overflow-clip">
40
- <div class="flex items-center justify-between px-2 h-10 bg-muted/30 border-b">
41
- <div class="flex-1 flex h-full items-center gap-2">
42
- <TableIcon class="text-muted-foreground" size="17.5" />
43
- <div class="text-sm text-muted-foreground">{child.collection}</div>
44
- </div>
45
- {#if child.type === "fk"}
46
- <div class="flex gap-2">
47
- <CreateDetailViewButton
48
- variant="ghost"
49
- class="h-7 px-2 font-normal text-xs"
50
- Icon={Plus}
51
- collectionName={child.collection}
52
- onSuccessfullSave={async () => { refresh[index] = !refresh[index]; }}
53
- >
54
- Create
55
- </CreateDetailViewButton>
56
- </div>
57
- {/if}
58
- </div>
59
- <div class="max-h-72 overflow-auto rounded-md">
60
- {#key refresh[index]}
61
- <DataTable
62
- collectionName={child.collection}
63
- searchParams={{ children_of: collectionName, parent_id: entry.id }}
64
- unifiedBgColor="bg-muted/30"
65
- showHeader={false}
66
- showFooter={false}
67
- showDelete={child.type === "fk"}
68
- tableProps={{ showLastColumnBorder: false, showLastRowBorder: false, showCheckboxes: false }}
69
- />
70
- {/key}
62
+ {#snippet headerLeft()}
63
+ <div class="flex items-center gap-2 px-1">
64
+ <Table size="14" class="text-muted-foreground" />
65
+ <span class="text-sm font-medium">{child.collection}</span>
71
66
  </div>
72
- </div>
73
- </ExtensionsComponents>
74
- {/each}
75
- </div>
67
+ {/snippet}
68
+ </DataTable>
69
+ </div>
70
+ {/each}
76
71
  </div>
77
72
  {/if}
@@ -31,7 +31,7 @@
31
31
  import { getField, getFieldIcon } from "../../dataTable/utils";
32
32
  import DetailViewChildren from "../update/detailViewChildren.svelte";
33
33
  import type { Snippet } from "svelte";
34
- import { getDefaultEntry, parseDetailViewValues, serializeEntry } from "../utils";
34
+ import { getDefaultEntry } from "../utils";
35
35
  import FieldInput from "../fieldInput.svelte";
36
36
  import Drawer from "../../../components/drawer.svelte";
37
37
 
@@ -46,8 +46,6 @@
46
46
  recordId,
47
47
  }: UpdateDetailViewProp = $props();
48
48
 
49
- parseDetailViewValues(ctx, collectionName, values)
50
-
51
49
  const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
52
50
  let entry: Record<string, any> = $state(
53
51
  getDefaultEntry(ctx, fieldNames, collectionName, values),
@@ -57,15 +55,17 @@
57
55
  getChangedProperties(initialEntry, $state.snapshot(entry)),
58
56
  );
59
57
  let fieldsErrors: Record<string, any> = $state({});
58
+ let pendingChildren = $state<Record<string, any>>({});
60
59
 
61
60
  async function handleSave() {
62
61
  delete localEntry.id;
63
- localEntry = serializeEntry(ctx, collectionName, localEntry);
64
62
 
63
+ const children = Object.keys(pendingChildren).length ? pendingChildren : undefined;
65
64
  const response = await lobb.updateOne(
66
65
  collectionName,
67
66
  recordId,
68
67
  localEntry,
68
+ children,
69
69
  );
70
70
 
71
71
  if (!response.bodyUsed) {
@@ -145,7 +145,7 @@
145
145
  {/each}
146
146
  </div>
147
147
  {#if showRelatedRecords}
148
- <DetailViewChildren {collectionName} {entry} />
148
+ <DetailViewChildren {collectionName} {entry} bind:pendingChildren />
149
149
  {/if}
150
150
  </div>
151
151
  <div class="flex h-12 items-center justify-end gap-2 border-t px-4">
@@ -163,7 +163,7 @@
163
163
  class="h-7 px-3 text-xs font-normal"
164
164
  Icon={submitButton?.icon ? submitButton.icon : Pencil}
165
165
  onclick={handleSave}
166
- disabled={!Object.keys(localEntry).length}
166
+ disabled={!Object.keys(localEntry).length && !Object.keys(pendingChildren).length}
167
167
  >
168
168
  {submitButton?.text ? submitButton.text : "Update"}
169
169
  </Button>
@@ -4,7 +4,6 @@
4
4
  import Button from "../../../components/ui/button/button.svelte";
5
5
  import UpdateDetailView from "./updateDetailView.svelte";
6
6
  import { getStudioContext } from "../../../context";
7
- import { getCollectionParamsFields } from "../../dataTable/utils";
8
7
 
9
8
  interface LocalProp extends UpdateDetailViewProp {
10
9
  variant?: ButtonProps["variant"];
@@ -17,11 +16,11 @@
17
16
  let open = $state(false);
18
17
  let values: Record<string, any> | undefined = $state(undefined);
19
18
 
20
- const { lobb, ctx } = getStudioContext();
19
+ const { lobb } = getStudioContext();
21
20
 
22
21
  async function openView() {
23
22
  const params = {
24
- fields: getCollectionParamsFields(ctx, props.collectionName, true),
23
+ fields: "*",
25
24
  filter: { id: props.recordId },
26
25
  limit: 1,
27
26
  };
@@ -1,6 +1,5 @@
1
1
  import Mustache from "mustache";
2
2
  import type { CTX } from "../../store.types";
3
- import { isRelationField } from "../../relations";
4
3
  import { getField } from "../dataTable/utils";
5
4
  import type { DetailFormField } from "./detailViewForm.svelte";
6
5
 
@@ -26,34 +25,6 @@ export function getDefaultEntry(ctx: CTX, fieldNames: string[], collectionName:
26
25
  );
27
26
  }
28
27
 
29
- export function serializeEntry(
30
- ctx: CTX,
31
- collectionName: string,
32
- entry: Record<string, any>,
33
- ) {
34
- entry = { ...entry }
35
-
36
- // extract FK object fields → ID
37
- for (const [fieldName, fieldValue] of Object.entries(entry)) {
38
- const isRefrenceField = isRelationField(ctx, collectionName, fieldName);
39
- if (isRefrenceField && fieldValue !== null && fieldValue.id !== undefined) {
40
- entry[fieldName] = fieldValue.id;
41
- }
42
- }
43
-
44
- // extract polymorphic id_field objects → ID
45
- for (const relation of ctx.meta.relations) {
46
- if (relation.type !== "polymorphic") continue;
47
- if ((relation as any).from.collection !== collectionName) continue;
48
- const idField = (relation as any).from.id_field;
49
- const fieldValue = entry[idField];
50
- if (fieldValue !== null && typeof fieldValue === "object" && fieldValue.id !== undefined) {
51
- entry[idField] = fieldValue.id;
52
- }
53
- }
54
-
55
- return entry;
56
- }
57
28
 
58
29
  export function buildChildren(
59
30
  ctx: CTX,
@@ -72,8 +43,7 @@ export function buildChildren(
72
43
  if (!childEntries?.length) continue;
73
44
 
74
45
  const toCreate = childEntries
75
- .filter((e) => !e.id)
76
- .map((e) => serializeEntry(ctx, childCollection, e));
46
+ .filter((e) => !e.id);
77
47
  const toLink = childEntries
78
48
  .filter((e) => e.id)
79
49
  .map((e) => e.id);
@@ -88,45 +58,13 @@ export function buildChildren(
88
58
  return Object.keys(children).length ? children : undefined;
89
59
  }
90
60
 
91
- export function parseDetailViewValues(ctx: CTX, collectionName: string, values: Record<string, any>) {
92
- const forignFieldNames = ctx.meta.relations
93
- .filter((relation) => relation.type !== "polymorphic" && relation.from.collection === collectionName)
94
- .map((relation) => relation.from.field);
95
- const childCollectionNames = ctx.meta.relations
96
- .filter((relation) => relation.to.collection === collectionName)
97
- .map((relation) => relation.from.collection);
98
-
99
- for (const [key, value] of Object.entries(values)) {
100
- if (forignFieldNames.includes(key)) {
101
- if (typeof value === 'number') {
102
- values[key] = {
103
- id: value,
104
- }
105
- }
106
- } else if (childCollectionNames.includes(key)) {
107
- for (let index = 0; index < values[key].length; index++) {
108
- parseDetailViewValues(ctx, key, values[key][index]);
109
- }
110
- }
111
- }
112
-
113
- // wrap polymorphic id_field scalar values into { id: value }
114
- for (const relation of ctx.meta.relations) {
115
- if (relation.type !== "polymorphic") continue;
116
- if ((relation as any).from.collection !== collectionName) continue;
117
- const idField = (relation as any).from.id_field;
118
- if (idField in values && typeof values[idField] === 'number') {
119
- values[idField] = { id: values[idField] };
120
- }
121
- }
122
- }
123
61
 
124
62
  export function getCollectionFields(ctx: CTX, collectionName: string) {
125
63
  let returnedData: DetailFormField[] = [];
126
64
 
127
65
  const collectionFields = ctx.meta.collections[collectionName].fields;
128
66
  const isSingleton = ctx.meta.collections[collectionName].singleton;
129
- for (const [fieldName, value] of Object.entries(collectionFields)) {
67
+ for (const [fieldName] of Object.entries(collectionFields)) {
130
68
 
131
69
  if (isSingleton && fieldName === "id") {
132
70
  continue;
@@ -1,14 +1,19 @@
1
1
  <script lang="ts">
2
+ import { onMount } from "svelte";
2
3
  import Input from "./ui/input/input.svelte";
3
4
  import SelectRecord from "./selectRecord.svelte";
4
5
  import UpdateDetailViewButton from "./detailView/update/updateDetailViewButton.svelte";
6
+ import { getCollectionPrimaryField } from "./dataTable/utils";
7
+ import { getStudioContext } from "../context";
5
8
  import { ExternalLink } from "lucide-svelte";
6
9
 
10
+ const { lobb, ctx } = getStudioContext();
11
+
7
12
  interface LocalProps {
8
13
  parentCollectionName: string;
9
14
  collectionName: string;
10
15
  fieldName: string;
11
- value?: any;
16
+ value?: number | null;
12
17
  destructive?: boolean;
13
18
  entry: Record<string, any>;
14
19
  }
@@ -22,31 +27,48 @@
22
27
  entry,
23
28
  }: LocalProps = $props();
24
29
 
25
- const idIsZero = $derived(value?.id === 0);
26
- const refrenceId = $derived(value ? value.id : null);
27
- const primaryField = $derived(value ? Object.values(value)[1] : null);
30
+ let displayName = $state<string | null>(null);
31
+
32
+ onMount(async () => {
33
+ if (value == null) return;
34
+ try {
35
+ const res = await lobb.findOne(collectionName, value);
36
+ const record = (await res.json()).data;
37
+ const primaryFieldName = getCollectionPrimaryField(ctx, collectionName);
38
+ displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
39
+ } catch {
40
+ displayName = null;
41
+ }
42
+ });
43
+
44
+ $effect(() => {
45
+ if (value == null) displayName = null;
46
+ });
47
+
48
+ function handleSelect(selectedEntry: any) {
49
+ const primaryFieldName = getCollectionPrimaryField(ctx, collectionName);
50
+ value = selectedEntry.id;
51
+ displayName = primaryFieldName ? String(selectedEntry[primaryFieldName]) : null;
52
+ }
53
+
54
+ const idIsZero = $derived(value === 0);
28
55
  </script>
29
56
 
30
- <!-- THE SELECT BUTTON -->
31
57
  {#if !idIsZero}
32
58
  <div class="relative">
33
- <div
34
- class="flex gap-2 absolute right-0 top-0 mr-9 h-full items-center text-xs"
35
- >
36
- {#if value !== null}
59
+ <div class="flex gap-2 absolute right-0 top-0 mr-9 h-full items-center text-xs">
60
+ {#if value != null}
37
61
  <UpdateDetailViewButton
38
62
  collectionName={collectionName}
39
- recordId={value.id}
63
+ recordId={value}
40
64
  variant="ghost"
41
65
  class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
42
66
  Icon={ExternalLink}
43
67
  ></UpdateDetailViewButton>
44
68
  {/if}
45
- {#if primaryField}
46
- <div
47
- class="flex items-center bg-background rounded-full border h-6 px-3 shadow-sm"
48
- >
49
- {primaryField}
69
+ {#if displayName}
70
+ <div class="flex items-center bg-background rounded-full border h-6 px-3 shadow-sm">
71
+ {displayName}
50
72
  </div>
51
73
  {/if}
52
74
  <SelectRecord
@@ -55,7 +77,7 @@
55
77
  {parentCollectionName}
56
78
  {collectionName}
57
79
  {fieldName}
58
- bind:value
80
+ onSelect={handleSelect}
59
81
  {entry}
60
82
  />
61
83
  </div>
@@ -66,7 +88,10 @@
66
88
  bg-muted/30 text-xs
67
89
  {destructive ? 'border-destructive bg-destructive/10' : ''}
68
90
  "
69
- bind:value={() => refrenceId, (v) => (value.id = v)}
91
+ bind:value={
92
+ () => value ?? "",
93
+ (v) => (value = (v === "" || v == null) ? null : Number(v))
94
+ }
70
95
  />
71
96
  </div>
72
97
  {:else}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { onMount } from "svelte";
2
3
  import Button from "./ui/button/button.svelte";
3
4
  import DataTable from "./dataTable/dataTable.svelte";
4
5
  import Drawer from "./drawer.svelte";
@@ -7,7 +8,7 @@
7
8
  import { getStudioContext } from "../context";
8
9
  import { ArrowLeft, Link, ChevronDown } from "lucide-svelte";
9
10
 
10
- const { ctx } = getStudioContext();
11
+ const { ctx, lobb } = getStudioContext();
11
12
 
12
13
  interface Props {
13
14
  collectionField: string;
@@ -26,12 +27,28 @@
26
27
  }: Props = $props();
27
28
 
28
29
  const selectedCollection = $derived(entry[collectionField] ?? null);
29
- const selectedId = $derived(entry[idField]?.id ?? entry[idField] ?? null);
30
- const primaryField = $derived(entry[idField] ? Object.values(entry[idField])[1] : null);
30
+ const selectedId = $derived(entry[idField] ?? null);
31
31
 
32
+ let displayName = $state<string | null>(null);
32
33
  let collectionPopoverOpen = $state(false);
33
34
  let recordDrawerOpen = $state(false);
34
35
 
36
+ onMount(async () => {
37
+ if (selectedCollection == null || selectedId == null) return;
38
+ try {
39
+ const res = await lobb.findOne(selectedCollection, selectedId);
40
+ const record = await res.json();
41
+ const primaryFieldName = getCollectionPrimaryField(ctx, selectedCollection);
42
+ displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
43
+ } catch {
44
+ displayName = null;
45
+ }
46
+ });
47
+
48
+ $effect(() => {
49
+ if (entry[idField] == null) displayName = null;
50
+ });
51
+
35
52
  function onCollectionChange(col: string) {
36
53
  collectionPopoverOpen = false;
37
54
  if (entry[collectionField] !== col) {
@@ -42,14 +59,13 @@
42
59
  function onIdChange(e: Event) {
43
60
  const raw = (e.target as HTMLInputElement).value;
44
61
  const id = raw === "" ? null : Number(raw);
45
- entry = { ...entry, [idField]: id === null ? null : { id } };
62
+ entry = { ...entry, [idField]: id };
46
63
  }
47
64
 
48
65
  function onRecordSelect(record: any) {
49
66
  const primaryFieldName = getCollectionPrimaryField(ctx, selectedCollection!);
50
- const value: any = { id: record.id };
51
- if (primaryFieldName) value[primaryFieldName] = record[primaryFieldName];
52
- entry = { ...entry, [idField]: value };
67
+ entry = { ...entry, [idField]: record.id };
68
+ displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
53
69
  recordDrawerOpen = false;
54
70
  }
55
71
  </script>
@@ -93,9 +109,9 @@
93
109
  />
94
110
 
95
111
  <!-- Primary field badge -->
96
- {#if primaryField}
112
+ {#if displayName}
97
113
  <div class="flex shrink-0 items-center bg-background rounded-full border h-6 px-3 shadow-sm">
98
- {primaryField}
114
+ {displayName}
99
115
  </div>
100
116
  {/if}
101
117
 
package/src/lib/utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { clsx, type ClassValue } from "clsx";
2
2
  import { twMerge } from "tailwind-merge";
3
+ import { isEqual } from "lodash";
3
4
 
4
5
  import { MediaQuery } from 'svelte/reactivity';
5
6
 
@@ -34,7 +35,7 @@ export function calculateDrawerWidth() {
34
35
  export function getChangedProperties(oldObj: Record<string, any>, newObj: Record<string, any>) {
35
36
  const changes: Record<string, any> = {};
36
37
  for (const key of Object.keys(newObj)) {
37
- if (oldObj[key] !== newObj[key]) {
38
+ if (!isEqual(oldObj[key], newObj[key])) {
38
39
  changes[key] = newObj[key];
39
40
  }
40
41
  }