@lobb-js/studio 0.19.1 → 0.21.0

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 (41) hide show
  1. package/dist/actions.d.ts +1 -0
  2. package/dist/components/dataTable/fieldCell.svelte +38 -37
  3. package/dist/components/dataTable/polymorphicFieldCell.svelte +43 -0
  4. package/dist/components/dataTable/polymorphicFieldCell.svelte.d.ts +9 -0
  5. package/dist/components/dataTable/utils.js +27 -17
  6. package/dist/components/dataTableDrawer/dataTableDrawer.svelte +14 -10
  7. package/dist/components/dataTableDrawer/dataTableDrawer.svelte.d.ts +1 -0
  8. package/dist/components/detailView/create/createDetailView.svelte +38 -69
  9. package/dist/components/detailView/fieldInput.svelte +27 -11
  10. package/dist/components/detailView/fieldInput.svelte.d.ts +1 -1
  11. package/dist/components/detailView/update/updateDetailView.svelte +26 -30
  12. package/dist/components/detailView/utils.d.ts +5 -2
  13. package/dist/components/detailView/utils.js +53 -71
  14. package/dist/components/drawer.svelte +24 -8
  15. package/dist/components/drawer.svelte.d.ts +1 -0
  16. package/dist/components/polymorphicInput.svelte +141 -0
  17. package/dist/components/polymorphicInput.svelte.d.ts +10 -0
  18. package/dist/extensions/extension.types.d.ts +2 -0
  19. package/dist/extensions/extensionUtils.js +2 -0
  20. package/dist/relations.d.ts +14 -0
  21. package/dist/relations.js +47 -0
  22. package/dist/store.types.d.ts +1 -0
  23. package/dist/utils.d.ts +0 -3
  24. package/dist/utils.js +0 -21
  25. package/package.json +2 -2
  26. package/src/lib/actions.ts +1 -0
  27. package/src/lib/components/dataTable/fieldCell.svelte +38 -37
  28. package/src/lib/components/dataTable/polymorphicFieldCell.svelte +43 -0
  29. package/src/lib/components/dataTable/utils.ts +21 -18
  30. package/src/lib/components/dataTableDrawer/dataTableDrawer.svelte +14 -10
  31. package/src/lib/components/detailView/create/createDetailView.svelte +38 -69
  32. package/src/lib/components/detailView/fieldInput.svelte +27 -11
  33. package/src/lib/components/detailView/update/updateDetailView.svelte +26 -30
  34. package/src/lib/components/detailView/utils.ts +44 -75
  35. package/src/lib/components/drawer.svelte +24 -8
  36. package/src/lib/components/polymorphicInput.svelte +141 -0
  37. package/src/lib/extensions/extension.types.ts +2 -1
  38. package/src/lib/extensions/extensionUtils.ts +2 -0
  39. package/src/lib/relations.ts +52 -0
  40. package/src/lib/store.types.ts +1 -0
  41. package/src/lib/utils.ts +0 -21
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ import { ExternalLink, Table } from "lucide-svelte";
3
+ import UpdateDetailViewButton from "../detailView/update/updateDetailViewButton.svelte";
4
+
5
+ interface Props {
6
+ collectionField: string;
7
+ idField: string;
8
+ entry: Record<string, any>;
9
+ tableParams?: any;
10
+ }
11
+
12
+ let {
13
+ collectionField,
14
+ idField,
15
+ entry,
16
+ tableParams = $bindable(),
17
+ }: Props = $props();
18
+
19
+ const targetCollection = $derived(entry[collectionField] ?? null);
20
+ const targetId = $derived(entry[idField] ?? null);
21
+ </script>
22
+
23
+ {#if targetId && targetCollection}
24
+ <div class="flex items-center gap-2">
25
+ <div class="flex items-center gap-1 border bg-muted px-2 py-1 rounded-md text-muted-foreground">
26
+ <Table size="11" />
27
+ {targetCollection}
28
+ </div>
29
+ <div>{targetId}</div>
30
+ <UpdateDetailViewButton
31
+ collectionName={targetCollection}
32
+ recordId={targetId}
33
+ variant="ghost"
34
+ class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
35
+ Icon={ExternalLink}
36
+ onSuccessfullSave={async () => {
37
+ tableParams = { ...tableParams };
38
+ }}
39
+ />
40
+ </div>
41
+ {:else}
42
+ <div class="text-muted-foreground">NULL</div>
43
+ {/if}
@@ -1,5 +1,6 @@
1
1
  import type { TableProps } from "./table.svelte";
2
2
  import type { CTX } from "../../store.types";
3
+ import { getFieldRelationTarget } from "../../relations";
3
4
 
4
5
  import {
5
6
  Binary,
@@ -10,15 +11,18 @@ import {
10
11
  Clock,
11
12
  Hash,
12
13
  Key,
14
+ Link,
13
15
  Text,
14
16
  Type,
15
17
  } from "lucide-svelte/icons";
18
+ import { icons } from "lucide-svelte";
16
19
 
17
20
  export function getCollectionColumns(ctx: CTX, collectionName: string): TableProps['columns'] {
18
21
  const collectionFields = getFields(ctx, collectionName);
19
22
  const headers: TableProps['columns'] = [];
20
23
  for (const fieldName in collectionFields) {
21
24
  const field = collectionFields[fieldName];
25
+ if ((field as any).ui?.hidden) continue;
22
26
  headers.push({
23
27
  id: field.key,
24
28
  subtext: field.type,
@@ -30,6 +34,8 @@ export function getCollectionColumns(ctx: CTX, collectionName: string): TablePro
30
34
 
31
35
  export function getFieldIcon(ctx: CTX, fieldName: string, collectionName: string) {
32
36
  const field = getField(ctx, fieldName, collectionName);
37
+ const uiIcon = (field as any).ui?.icon;
38
+ if (uiIcon && uiIcon in icons) return (icons as any)[uiIcon];
33
39
  if (fieldName === "id") {
34
40
  return Key;
35
41
  } else if (field.type === "string") {
@@ -43,6 +49,12 @@ export function getFieldIcon(ctx: CTX, fieldName: string, collectionName: string
43
49
  } else if (field.type === "bool") {
44
50
  return Binary;
45
51
  } else if (field.type === "integer") {
52
+ const target = getFieldRelationTarget(ctx, collectionName, fieldName);
53
+ if (target) {
54
+ const targetIcon = ctx.meta.collections[target]?.ui?.icon;
55
+ if (targetIcon && targetIcon in icons) return (icons as any)[targetIcon];
56
+ return Link;
57
+ }
46
58
  return Hash;
47
59
  } else if (field.type === "long") {
48
60
  return Hash;
@@ -58,7 +70,7 @@ export function getFieldIcon(ctx: CTX, fieldName: string, collectionName: string
58
70
  return Clock;
59
71
  }
60
72
 
61
- throw new Error(`(${field.type}) doesnt have an icon`);
73
+ return Braces;
62
74
  }
63
75
 
64
76
  export function getFields(ctx: CTX, collectionName: string) {
@@ -91,7 +103,7 @@ export function getCollectionParamsFields(ctx: CTX, collectionName: string, allF
91
103
  const relations = ctx.meta.relations;
92
104
  const foreignFields = relations
93
105
  .filter((relation) => {
94
- return relation.from.collection === collectionName
106
+ return relation.type !== "polymorphic" && relation.from.collection === collectionName;
95
107
  })
96
108
  .map((relation) => {
97
109
  return {
@@ -101,24 +113,15 @@ export function getCollectionParamsFields(ctx: CTX, collectionName: string, allF
101
113
  });
102
114
 
103
115
  const columns = [];
104
- for (let index = 0; index < foreignFields.length; index++) {
105
- const foreignField = foreignFields[index];
106
- if (!foreignField.collection || !ctx.meta.collections[foreignField.collection]) {
107
- continue;
108
- }
109
- columns.push(`${foreignField.field}.id`);
116
+ for (const foreignField of foreignFields) {
117
+ if (!foreignField.collection || !ctx.meta.collections[foreignField.collection]) continue;
110
118
 
111
- if (!allFields) {
112
- const primaryField = getCollectionPrimaryField(ctx, foreignField.collection)
113
- if (primaryField) {
114
- columns.push(`${foreignField.field}.${primaryField}`);
115
- }
119
+ if (allFields) {
120
+ columns.push(`${foreignField.field}.*`);
116
121
  } else {
117
- const fieldNames = Object.keys(ctx.meta.collections[foreignField.collection].fields);
118
- for (let index = 0; index < fieldNames.length; index++) {
119
- const fieldName = fieldNames[index];
120
- columns.push(`${foreignField.field}.${fieldName}`);
121
- }
122
+ columns.push(`${foreignField.field}.id`);
123
+ const primaryField = getCollectionPrimaryField(ctx, foreignField.collection);
124
+ if (primaryField) columns.push(`${foreignField.field}.${primaryField}`);
122
125
  }
123
126
  }
124
127
 
@@ -12,6 +12,7 @@
12
12
  showHeader?: boolean;
13
13
  showFooter?: boolean;
14
14
  tableProps?: Partial<TableProps>;
15
+ position?: "side" | "bottom";
15
16
  onClose?: () => void;
16
17
  }
17
18
 
@@ -22,20 +23,23 @@
22
23
  showHeader = true,
23
24
  showFooter = true,
24
25
  tableProps,
26
+ position = "side",
25
27
  onClose,
26
28
  }: Props = $props();
27
29
  </script>
28
30
 
29
- <Drawer onHide={async () => onClose?.()}>
30
- <div class="flex h-12 shrink-0 items-center gap-4 border-b px-4">
31
- <Button
32
- variant="outline"
33
- onclick={() => onClose?.()}
34
- class="h-8 w-8 rounded-full text-xs font-normal"
35
- Icon={ArrowLeft}
36
- />
37
- <div class="text-sm font-medium">{title ?? collectionName}</div>
38
- </div>
31
+ <Drawer onHide={async () => onClose?.()} {position}>
32
+ {#if position !== "bottom"}
33
+ <div class="flex h-12 shrink-0 items-center gap-4 border-b px-4">
34
+ <Button
35
+ variant="outline"
36
+ onclick={() => onClose?.()}
37
+ class="h-8 w-8 rounded-full text-xs font-normal"
38
+ Icon={ArrowLeft}
39
+ />
40
+ <div class="text-sm font-medium">{title ?? collectionName}</div>
41
+ </div>
42
+ {/if}
39
43
  <div class="min-h-0 flex-1 overflow-auto">
40
44
  <DataTable
41
45
  {collectionName}
@@ -28,7 +28,7 @@
28
28
  import { getField, getFieldIcon } from "../../dataTable/utils";
29
29
  import Children from "./children.svelte";
30
30
  import {
31
- generateTransactionBody,
31
+ buildChildren,
32
32
  getDefaultEntry,
33
33
  parseDetailViewValues,
34
34
  serializeEntry,
@@ -82,31 +82,15 @@
82
82
  }
83
83
  }
84
84
 
85
- const serializedEntry = serializeEntry(
86
- ctx,
87
- collectionName,
88
- localEntry,
89
- rollback,
90
- );
91
-
92
- let transactionBody;
93
85
  if (rollback) {
94
- transactionBody = [
95
- {
96
- method: "createOne",
97
- props: { collectionName: collectionName, data: serializedEntry },
98
- },
99
- ];
100
- } else {
101
- transactionBody = generateTransactionBody(
102
- ctx,
103
- collectionName,
104
- serializedEntry,
105
- );
86
+ if (onSuccessfullSave) await onSuccessfullSave(localEntry);
87
+ onCancel?.();
88
+ return;
106
89
  }
107
90
 
108
- // create the record
109
- let response = await lobb.transactions(transactionBody, rollback);
91
+ const serializedEntry = serializeEntry(ctx, collectionName, localEntry);
92
+ const children = buildChildren(ctx, collectionName, localEntry);
93
+ const response = await lobb.createOne(collectionName, serializedEntry, children);
110
94
 
111
95
  await emitEvent({ lobb, ctx }, "studio.collections.create", {
112
96
  collectionName,
@@ -126,15 +110,8 @@
126
110
  }
127
111
  }
128
112
 
129
- // close detailView side bar
130
- if (onSuccessfullSave) {
131
- await onSuccessfullSave(localEntry);
132
- }
133
-
134
- if (!rollback) {
135
- toast.success(`The record was successfully created`);
136
- }
137
-
113
+ if (onSuccessfullSave) await onSuccessfullSave(localEntry);
114
+ toast.success(`The record was successfully created`);
138
115
  onCancel?.();
139
116
  }
140
117
  </script>
@@ -161,46 +138,38 @@
161
138
  <div class="flex-1 overflow-y-auto">
162
139
  <div class="flex flex-col gap-4 p-4">
163
140
  {#each fieldNames as fieldName}
164
- {@const field = getField(ctx, fieldName, collectionName)}
165
- {@const FieldIcon = getFieldIcon(ctx, fieldName, collectionName)}
166
- <div
167
- class="flex flex-col gap-2"
168
- >
169
- <div
170
- class="flex flex-1 items-end justify-between gap-2 text-xs"
171
- >
172
- <div class="flex gap-2">
173
- <div class="h-fit">{field.label}</div>
174
- <div
175
- class="flex h-fit items-center gap-1 text-[0.7rem] text-muted-foreground"
176
- >
177
- <FieldIcon size="12" />
178
- {field.type}
141
+ {#if !ctx.meta.collections[collectionName].fields[fieldName]?.ui?.hidden}
142
+ {@const field = getField(ctx, fieldName, collectionName)}
143
+ {@const FieldIcon = getFieldIcon(ctx, fieldName, collectionName)}
144
+ <div class="flex flex-col gap-2">
145
+ <div class="flex flex-1 items-end justify-between gap-2 text-xs">
146
+ <div class="flex gap-2">
147
+ <div class="h-fit">{field.label}</div>
148
+ <div class="flex h-fit items-center gap-1 text-[0.7rem] text-muted-foreground">
149
+ <FieldIcon size="12" />
150
+ {field.type}
151
+ </div>
152
+ </div>
153
+ <div>
154
+ <ExtensionsComponents
155
+ name="dvFields.topRight.{collectionName}.{fieldName}"
156
+ utils={getExtensionUtils(lobb, ctx)}
157
+ bind:value={entry[fieldName]}
158
+ />
179
159
  </div>
180
160
  </div>
181
- <div>
182
- <ExtensionsComponents
183
- name="dvFields.topRight.{collectionName}.{fieldName}"
184
- utils={getExtensionUtils(lobb, ctx)}
185
- bind:value={entry[fieldName]}
186
- />
187
- </div>
161
+ <FieldInput
162
+ {collectionName}
163
+ {fieldName}
164
+ bind:value={
165
+ () => entry[fieldName],
166
+ (v) => (entry = { ...entry, [fieldName]: v })
167
+ }
168
+ bind:entry
169
+ errorMessages={fieldsErrors[fieldName]}
170
+ />
188
171
  </div>
189
- <FieldInput
190
- {collectionName}
191
- {fieldName}
192
- bind:value={
193
- () => entry[fieldName],
194
- (v) =>
195
- (entry = {
196
- ...entry,
197
- [fieldName]: v,
198
- })
199
- }
200
- {entry}
201
- errorMessages={fieldsErrors[fieldName]}
202
- />
203
- </div>
172
+ {/if}
204
173
  {/each}
205
174
  </div>
206
175
  {#if showRelatedRecords}
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getStudioContext } from "../../context";
3
- import { getFieldRelation } from "../../utils";
3
+ import { getFieldRelationTarget, getPolymorphicRelation } from "../../relations";
4
4
  import { Ban, Check, CircleAlert, X } from "lucide-svelte";
5
5
  import { getField } from "../dataTable/utils";
6
6
  import Button from "../ui/button/button.svelte";
@@ -11,6 +11,7 @@
11
11
  import type { EnumOption } from "@lobb-js/core";
12
12
  import Textarea from "../ui/textarea/textarea.svelte";
13
13
  import ForeingKeyInput from "../foreingKeyInput.svelte";
14
+ import PolymorphicInput from "../polymorphicInput.svelte";
14
15
  import ExtensionsComponents from "../extensionsComponents.svelte";
15
16
  import { getExtensionUtils } from "../../extensions/extensionUtils";
16
17
 
@@ -29,7 +30,7 @@
29
30
  fieldName,
30
31
  value = $bindable(),
31
32
  errorMessages = [],
32
- entry,
33
+ entry = $bindable(),
33
34
  }: Props = $props();
34
35
 
35
36
  const ui_input =
@@ -37,10 +38,11 @@
37
38
  const ui =
38
39
  ctx.meta.collections[collectionName].fields[fieldName].ui;
39
40
  const field = getField(ctx, fieldName, collectionName);
40
- const fieldRelation = getFieldRelation(ctx, collectionName, fieldName);
41
+ const fieldRelationTarget = getFieldRelationTarget(ctx, collectionName, fieldName);
42
+ const polymorphicRelation = getPolymorphicRelation(ctx, collectionName, fieldName);
41
43
  const isDisabled = field.key === 'id' || Boolean(ui?.disabled)
42
44
  const disabledClasses = "pointer-events-none opacity-50";
43
- const destructive: boolean = $derived(Boolean(errorMessages.length));
45
+ const destructive: boolean = $derived(!isDisabled && Boolean(errorMessages.length));
44
46
 
45
47
  </script>
46
48
 
@@ -53,13 +55,27 @@
53
55
  style="flex: 2;"
54
56
  >
55
57
  <Button
56
- onclick={() => (value = null)}
58
+ onclick={() => {
59
+ value = null;
60
+ if (polymorphicRelation && entry) {
61
+ entry[polymorphicRelation.from.collection_field] = null;
62
+ entry[polymorphicRelation.from.id_field] = null;
63
+ }
64
+ }}
57
65
  variant="outline"
58
66
  class="absolute right-0 top-0 z-10 mr-1.5 mt-1.5 aspect-square h-6 w-6 p-0"
59
67
  Icon={Ban}
60
68
  tabindex={-1}
61
69
  ></Button>
62
- {#if ui_input}
70
+ {#if polymorphicRelation && entry}
71
+ <PolymorphicInput
72
+ collectionField={polymorphicRelation.from.collection_field}
73
+ idField={polymorphicRelation.from.id_field}
74
+ targetCollections={polymorphicRelation.to}
75
+ bind:entry
76
+ {destructive}
77
+ />
78
+ {:else if ui_input}
63
79
  <FieldCustomInput
64
80
  bind:value
65
81
  type={ui_input.type}
@@ -73,12 +89,12 @@
73
89
  class="bg-muted/30 text-xs"
74
90
  bind:value
75
91
  />
76
- {:else if fieldRelation && entry}
92
+ {:else if fieldRelationTarget && entry}
77
93
  <ExtensionsComponents
78
- name="detailView.fields.foreignKey.{fieldRelation.to.collection}"
94
+ name="detailView.fields.foreignKey.{fieldRelationTarget}"
79
95
  utils={getExtensionUtils(lobb, ctx)}
80
96
  parentCollectionName={collectionName}
81
- collectionName={fieldRelation.to.collection}
97
+ collectionName={fieldRelationTarget}
82
98
  bind:value
83
99
  {entry}
84
100
  fieldName={field.key}
@@ -86,7 +102,7 @@
86
102
  >
87
103
  <ForeingKeyInput
88
104
  parentCollectionName={collectionName}
89
- collectionName={fieldRelation.to.collection}
105
+ collectionName={fieldRelationTarget}
90
106
  bind:value
91
107
  {entry}
92
108
  fieldName={field.key}
@@ -263,7 +279,7 @@
263
279
  bind:value
264
280
  />
265
281
  {/if}
266
- {#if errorMessages}
282
+ {#if !isDisabled && errorMessages}
267
283
  {#each errorMessages as message}
268
284
  <div class="flex gap-1 text-destructive">
269
285
  <CircleAlert size="15" class="translate-y-[0.025rem]" />
@@ -113,39 +113,35 @@
113
113
  <div class="flex-1 overflow-y-auto">
114
114
  <div class="flex flex-col gap-4 p-4">
115
115
  {#each fieldNames as fieldName}
116
- {@const field = getField(ctx, fieldName, collectionName)}
117
- {@const FieldIcon = getFieldIcon(ctx, fieldName, collectionName)}
118
- <div
119
- class="flex flex-col gap-2"
120
- >
121
- <div
122
- class="flex flex-1 items-end justify-between gap-2 text-xs"
123
- >
124
- <div class="flex gap-2">
125
- <div class="h-fit">{field.label}</div>
126
- <div
127
- class="flex h-fit items-center gap-1 text-[0.7rem] text-muted-foreground"
128
- >
129
- <FieldIcon size="12" />
130
- {field.type}
116
+ {#if !ctx.meta.collections[collectionName].fields[fieldName]?.ui?.hidden}
117
+ {@const field = getField(ctx, fieldName, collectionName)}
118
+ {@const FieldIcon = getFieldIcon(ctx, fieldName, collectionName)}
119
+ <div class="flex flex-col gap-2">
120
+ <div class="flex flex-1 items-end justify-between gap-2 text-xs">
121
+ <div class="flex gap-2">
122
+ <div class="h-fit">{field.label}</div>
123
+ <div class="flex h-fit items-center gap-1 text-[0.7rem] text-muted-foreground">
124
+ <FieldIcon size="12" />
125
+ {field.type}
126
+ </div>
127
+ </div>
128
+ <div>
129
+ <ExtensionsComponents
130
+ name="dvFields.topRight.{collectionName}.{fieldName}"
131
+ utils={getExtensionUtils(lobb, ctx)}
132
+ bind:value={entry[fieldName]}
133
+ />
131
134
  </div>
132
135
  </div>
133
- <div>
134
- <ExtensionsComponents
135
- name="dvFields.topRight.{collectionName}.{fieldName}"
136
- utils={getExtensionUtils(lobb, ctx)}
137
- bind:value={entry[fieldName]}
138
- />
139
- </div>
136
+ <FieldInput
137
+ {collectionName}
138
+ {fieldName}
139
+ bind:value={entry[fieldName]}
140
+ bind:entry
141
+ errorMessages={fieldsErrors[fieldName]}
142
+ />
140
143
  </div>
141
- <FieldInput
142
- {collectionName}
143
- {fieldName}
144
- bind:value={entry[fieldName]}
145
- {entry}
146
- errorMessages={fieldsErrors[fieldName]}
147
- />
148
- </div>
144
+ {/if}
149
145
  {/each}
150
146
  </div>
151
147
  {#if showRelatedRecords}
@@ -1,6 +1,6 @@
1
1
  import Mustache from "mustache";
2
2
  import type { CTX } from "../../store.types";
3
- import { getFieldRelation } from "../../utils";
3
+ import { isRelationField } from "../../relations";
4
4
  import { getField } from "../dataTable/utils";
5
5
  import type { DetailFormField } from "./detailViewForm.svelte";
6
6
 
@@ -30,108 +30,67 @@ export function serializeEntry(
30
30
  ctx: CTX,
31
31
  collectionName: string,
32
32
  entry: Record<string, any>,
33
- rollback: boolean = false,
34
33
  ) {
35
- // deep clone the object
36
34
  entry = { ...entry }
37
35
 
38
- // serialize the foreign key field's value
36
+ // extract FK object fields ID
39
37
  for (const [fieldName, fieldValue] of Object.entries(entry)) {
40
- const isRefrenceField = Boolean(getFieldRelation(ctx, collectionName, fieldName));
38
+ const isRefrenceField = isRelationField(ctx, collectionName, fieldName);
41
39
  if (isRefrenceField && fieldValue !== null && fieldValue.id !== undefined) {
42
40
  entry[fieldName] = fieldValue.id;
43
41
  }
44
42
  }
45
43
 
46
- // check for related collections properties and serialize them too
47
- if (!rollback) {
48
- const childrenRelations = ctx.meta.relations.filter((relation) => relation.to.collection === collectionName);
49
- const childrenCollectionNames = childrenRelations.map((relation) => relation.from.collection);
50
- for (let index = 0; index < childrenCollectionNames.length; index++) {
51
- const childrenCollectionName = childrenCollectionNames[index];
52
- const childrenEntries = entry[childrenCollectionName];
53
- if (childrenEntries) {
54
- for (let index = 0; index < childrenEntries.length; index++) {
55
- childrenEntries[index] = serializeEntry(ctx, childrenCollectionName, childrenEntries[index]);
56
- }
57
- }
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;
58
52
  }
59
53
  }
60
54
 
61
55
  return entry;
62
56
  }
63
57
 
64
- export function generateTransactionBody(
58
+ export function buildChildren(
65
59
  ctx: CTX,
66
60
  collectionName: string,
67
61
  entry: Record<string, any>,
68
- ) {
69
- entry = { ...entry }
70
- function handleEntryRecursive(
71
- transactionBody: any[],
72
- collectionName: string,
73
- entry: Record<string, any>,
74
- parentTransactionIndex?: number,
75
- ) {
76
- const parentCollectionName = parentTransactionIndex !== undefined ? transactionBody[parentTransactionIndex].props.collectionName : null;
77
- const foreignKeyFieldName = ctx.meta.relations.find(relation => relation.from.collection === collectionName && relation.to.collection === parentCollectionName)?.from.field;
78
- const collectionFieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
79
- const payload: any = {};
80
- for (let index = 0; index < collectionFieldNames.length; index++) {
81
- const fieldName = collectionFieldNames[index];
82
- const isForeignKeyField = fieldName === foreignKeyFieldName;
83
- if (isForeignKeyField) {
84
- payload[fieldName] = `{{ responses[${parentTransactionIndex}].data.id }}`
85
- continue;
86
- }
62
+ ): Record<string, { create?: any[]; link?: any[] }> | undefined {
63
+ const childrenRelations = ctx.meta.relations.filter(
64
+ (relation) => relation.type !== "polymorphic" && relation.to.collection === collectionName,
65
+ );
87
66
 
88
- payload[fieldName] = entry[fieldName];
89
- }
67
+ const children: Record<string, { create?: any[]; link?: any[] }> = {};
90
68
 
91
- const localTransactionIndex = transactionBody.length;
92
- if (payload.id) {
93
- const localPayload = {
94
- [foreignKeyFieldName]: payload[foreignKeyFieldName],
95
- };
96
- transactionBody.push({
97
- method: "updateMany",
98
- props: {
99
- collectionName: collectionName,
100
- data: localPayload,
101
- filter: { id: payload.id },
102
- },
103
- });
104
- } else {
105
- transactionBody.push({
106
- method: "createOne",
107
- props: { collectionName: collectionName, data: payload },
108
- });
109
- }
69
+ for (const relation of childrenRelations) {
70
+ const childCollection = (relation as any).from.collection;
71
+ const childEntries: any[] = entry[childCollection];
72
+ if (!childEntries?.length) continue;
110
73
 
111
- const childrenRelations = ctx.meta.relations.filter((relation) => relation.to.collection === collectionName);
112
- const childrenCollectionNames = childrenRelations.map((relation) => relation.from.collection);
113
- for (let index = 0; index < childrenCollectionNames.length; index++) {
114
- const childrenCollectionName = childrenCollectionNames[index];
115
- const childrenEntries = entry[childrenCollectionName];
116
- if (childrenEntries) {
117
- for (let index = 0; index < childrenEntries.length; index++) {
118
- const childrenEntry = childrenEntries[index];
119
- handleEntryRecursive(transactionBody, childrenCollectionName, childrenEntry, localTransactionIndex);
120
- }
121
- }
74
+ const toCreate = childEntries
75
+ .filter((e) => !e.id)
76
+ .map((e) => serializeEntry(ctx, childCollection, e));
77
+ const toLink = childEntries
78
+ .filter((e) => e.id)
79
+ .map((e) => e.id);
80
+
81
+ if (toCreate.length || toLink.length) {
82
+ children[childCollection] = {};
83
+ if (toCreate.length) children[childCollection].create = toCreate;
84
+ if (toLink.length) children[childCollection].link = toLink;
122
85
  }
123
86
  }
124
87
 
125
- const transactionBody: any[] = [];
126
-
127
- handleEntryRecursive(transactionBody, collectionName, entry);
128
-
129
- return transactionBody
88
+ return Object.keys(children).length ? children : undefined;
130
89
  }
131
90
 
132
91
  export function parseDetailViewValues(ctx: CTX, collectionName: string, values: Record<string, any>) {
133
92
  const forignFieldNames = ctx.meta.relations
134
- .filter((relation) => relation.from.collection === collectionName)
93
+ .filter((relation) => relation.type !== "polymorphic" && relation.from.collection === collectionName)
135
94
  .map((relation) => relation.from.field);
136
95
  const childCollectionNames = ctx.meta.relations
137
96
  .filter((relation) => relation.to.collection === collectionName)
@@ -150,6 +109,16 @@ export function parseDetailViewValues(ctx: CTX, collectionName: string, values:
150
109
  }
151
110
  }
152
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
+ }
153
122
  }
154
123
 
155
124
  export function getCollectionFields(ctx: CTX, collectionName: string) {