@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.
- package/dist/actions.d.ts +1 -0
- package/dist/components/dataTable/fieldCell.svelte +38 -37
- package/dist/components/dataTable/polymorphicFieldCell.svelte +43 -0
- package/dist/components/dataTable/polymorphicFieldCell.svelte.d.ts +9 -0
- package/dist/components/dataTable/utils.js +27 -17
- package/dist/components/dataTableDrawer/dataTableDrawer.svelte +14 -10
- package/dist/components/dataTableDrawer/dataTableDrawer.svelte.d.ts +1 -0
- package/dist/components/detailView/create/createDetailView.svelte +38 -69
- package/dist/components/detailView/fieldInput.svelte +27 -11
- package/dist/components/detailView/fieldInput.svelte.d.ts +1 -1
- package/dist/components/detailView/update/updateDetailView.svelte +26 -30
- package/dist/components/detailView/utils.d.ts +5 -2
- package/dist/components/detailView/utils.js +53 -71
- package/dist/components/drawer.svelte +24 -8
- package/dist/components/drawer.svelte.d.ts +1 -0
- package/dist/components/polymorphicInput.svelte +141 -0
- package/dist/components/polymorphicInput.svelte.d.ts +10 -0
- package/dist/extensions/extension.types.d.ts +2 -0
- package/dist/extensions/extensionUtils.js +2 -0
- package/dist/relations.d.ts +14 -0
- package/dist/relations.js +47 -0
- package/dist/store.types.d.ts +1 -0
- package/dist/utils.d.ts +0 -3
- package/dist/utils.js +0 -21
- package/package.json +2 -2
- package/src/lib/actions.ts +1 -0
- package/src/lib/components/dataTable/fieldCell.svelte +38 -37
- package/src/lib/components/dataTable/polymorphicFieldCell.svelte +43 -0
- package/src/lib/components/dataTable/utils.ts +21 -18
- package/src/lib/components/dataTableDrawer/dataTableDrawer.svelte +14 -10
- package/src/lib/components/detailView/create/createDetailView.svelte +38 -69
- package/src/lib/components/detailView/fieldInput.svelte +27 -11
- package/src/lib/components/detailView/update/updateDetailView.svelte +26 -30
- package/src/lib/components/detailView/utils.ts +44 -75
- package/src/lib/components/drawer.svelte +24 -8
- package/src/lib/components/polymorphicInput.svelte +141 -0
- package/src/lib/extensions/extension.types.ts +2 -1
- package/src/lib/extensions/extensionUtils.ts +2 -0
- package/src/lib/relations.ts +52 -0
- package/src/lib/store.types.ts +1 -0
- 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
|
-
|
|
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 (
|
|
105
|
-
|
|
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 (
|
|
112
|
-
|
|
113
|
-
if (primaryField) {
|
|
114
|
-
columns.push(`${foreignField.field}.${primaryField}`);
|
|
115
|
-
}
|
|
119
|
+
if (allFields) {
|
|
120
|
+
columns.push(`${foreignField.field}.*`);
|
|
116
121
|
} else {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
class="flex flex-col gap-2"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
>
|
|
177
|
-
<
|
|
178
|
-
|
|
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
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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={() =>
|
|
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
|
|
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
|
|
92
|
+
{:else if fieldRelationTarget && entry}
|
|
77
93
|
<ExtensionsComponents
|
|
78
|
-
name="detailView.fields.foreignKey.{
|
|
94
|
+
name="detailView.fields.foreignKey.{fieldRelationTarget}"
|
|
79
95
|
utils={getExtensionUtils(lobb, ctx)}
|
|
80
96
|
parentCollectionName={collectionName}
|
|
81
|
-
collectionName={
|
|
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={
|
|
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
|
-
{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class="flex flex-col gap-2"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
>
|
|
129
|
-
<
|
|
130
|
-
|
|
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
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
<FieldInput
|
|
137
|
+
{collectionName}
|
|
138
|
+
{fieldName}
|
|
139
|
+
bind:value={entry[fieldName]}
|
|
140
|
+
bind:entry
|
|
141
|
+
errorMessages={fieldsErrors[fieldName]}
|
|
142
|
+
/>
|
|
140
143
|
</div>
|
|
141
|
-
|
|
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 {
|
|
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
|
-
//
|
|
36
|
+
// extract FK object fields → ID
|
|
39
37
|
for (const [fieldName, fieldValue] of Object.entries(entry)) {
|
|
40
|
-
const isRefrenceField =
|
|
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
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
58
|
+
export function buildChildren(
|
|
65
59
|
ctx: CTX,
|
|
66
60
|
collectionName: string,
|
|
67
61
|
entry: Record<string, any>,
|
|
68
|
-
) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
89
|
-
}
|
|
67
|
+
const children: Record<string, { create?: any[]; link?: any[] }> = {};
|
|
90
68
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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) {
|