@lobb-js/studio 0.20.0 → 0.22.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/utils.js +8 -14
- package/dist/components/dataTableDrawer/dataTableDrawer.svelte +14 -10
- package/dist/components/dataTableDrawer/dataTableDrawer.svelte.d.ts +1 -0
- package/dist/components/detailView/create/children.svelte +13 -30
- package/dist/components/detailView/create/createDetailView.svelte +9 -32
- package/dist/components/detailView/update/children.svelte +122 -35
- package/dist/components/detailView/utils.d.ts +5 -2
- package/dist/components/detailView/utils.js +28 -71
- package/dist/components/drawer.svelte +24 -8
- package/dist/components/drawer.svelte.d.ts +1 -0
- package/dist/extensions/extension.types.d.ts +2 -0
- package/dist/extensions/extensionUtils.js +2 -0
- package/dist/store.types.d.ts +13 -0
- package/package.json +2 -2
- package/src/lib/actions.ts +1 -0
- package/src/lib/components/dataTable/utils.ts +7 -16
- package/src/lib/components/dataTableDrawer/dataTableDrawer.svelte +14 -10
- package/src/lib/components/detailView/create/children.svelte +13 -30
- package/src/lib/components/detailView/create/createDetailView.svelte +9 -32
- package/src/lib/components/detailView/update/children.svelte +122 -35
- package/src/lib/components/detailView/utils.ts +24 -76
- package/src/lib/components/drawer.svelte +24 -8
- package/src/lib/extensions/extension.types.ts +2 -1
- package/src/lib/extensions/extensionUtils.ts +2 -0
- package/src/lib/store.types.ts +7 -0
|
@@ -45,6 +45,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
|
45
45
|
};
|
|
46
46
|
import { toast } from "svelte-sonner";
|
|
47
47
|
import { showDialog, openDataTableDrawer } from "../actions";
|
|
48
|
+
import { emitEvent } from "../eventSystem";
|
|
48
49
|
import { Button } from "../components/ui/button";
|
|
49
50
|
import { Input } from "../components/ui/input";
|
|
50
51
|
import { Separator } from "../components/ui/separator";
|
|
@@ -100,6 +101,7 @@ export function getExtensionUtils(lobb, ctx) {
|
|
|
100
101
|
toast: toast,
|
|
101
102
|
showDialog: showDialog,
|
|
102
103
|
openDataTableDrawer: function (props) { return openDataTableDrawer({ lobb: lobb, ctx: ctx }, props); },
|
|
104
|
+
emitEvent: function (eventName, input) { return emitEvent({ lobb: lobb, ctx: ctx }, eventName, input); },
|
|
103
105
|
components: getComponents(),
|
|
104
106
|
mediaQueries: mediaQueries,
|
|
105
107
|
intlDate: intlDate,
|
package/dist/store.types.d.ts
CHANGED
|
@@ -5,12 +5,25 @@ interface CollectionTab {
|
|
|
5
5
|
filter?: Record<string, any>;
|
|
6
6
|
default?: boolean;
|
|
7
7
|
}
|
|
8
|
+
type CollectionChild = {
|
|
9
|
+
type: "fk";
|
|
10
|
+
collection: string;
|
|
11
|
+
field: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: "m2m";
|
|
14
|
+
collection: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: "polymorphic";
|
|
17
|
+
collection: string;
|
|
18
|
+
};
|
|
8
19
|
interface Collection {
|
|
9
20
|
category: string;
|
|
10
21
|
owner: string;
|
|
11
22
|
fields: Record<string, any>;
|
|
12
23
|
singleton: boolean;
|
|
13
24
|
virtual?: boolean;
|
|
25
|
+
junction?: boolean;
|
|
26
|
+
children?: CollectionChild[];
|
|
14
27
|
ui?: {
|
|
15
28
|
icon?: string;
|
|
16
29
|
tabs?: CollectionTab[];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobb-js/studio",
|
|
3
3
|
"license": "UNLICENSED",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.22.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"postpublish": "./scripts/postpublish.sh"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@lobb-js/core": "^0.
|
|
45
|
+
"@lobb-js/core": "^0.26.0",
|
|
46
46
|
"@chromatic-com/storybook": "^4.1.2",
|
|
47
47
|
"@storybook/addon-a11y": "^10.0.1",
|
|
48
48
|
"@storybook/addon-docs": "^10.0.1",
|
package/src/lib/actions.ts
CHANGED
|
@@ -113,24 +113,15 @@ export function getCollectionParamsFields(ctx: CTX, collectionName: string, allF
|
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
const columns = [];
|
|
116
|
-
for (
|
|
117
|
-
|
|
118
|
-
if (!foreignField.collection || !ctx.meta.collections[foreignField.collection]) {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
columns.push(`${foreignField.field}.id`);
|
|
116
|
+
for (const foreignField of foreignFields) {
|
|
117
|
+
if (!foreignField.collection || !ctx.meta.collections[foreignField.collection]) continue;
|
|
122
118
|
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
if (primaryField) {
|
|
126
|
-
columns.push(`${foreignField.field}.${primaryField}`);
|
|
127
|
-
}
|
|
119
|
+
if (allFields) {
|
|
120
|
+
columns.push(`${foreignField.field}.*`);
|
|
128
121
|
} else {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
columns.push(`${foreignField.field}.${fieldName}`);
|
|
133
|
-
}
|
|
122
|
+
columns.push(`${foreignField.field}.id`);
|
|
123
|
+
const primaryField = getCollectionPrimaryField(ctx, foreignField.collection);
|
|
124
|
+
if (primaryField) columns.push(`${foreignField.field}.${primaryField}`);
|
|
134
125
|
}
|
|
135
126
|
}
|
|
136
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}
|
|
@@ -15,53 +15,36 @@
|
|
|
15
15
|
|
|
16
16
|
let { collectionName, entry = $bindable(), values = {} }: LocalProp = $props();
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
(
|
|
20
|
-
);
|
|
18
|
+
const fkChildren = (ctx.meta.collections[collectionName]?.children ?? [])
|
|
19
|
+
.filter((c) => c.type === "fk") as { type: "fk"; collection: string; field: string }[];
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const relation = childrenRelations[index];
|
|
25
|
-
const childCollection = relation.from.collection;
|
|
26
|
-
entry[childCollection] = [];
|
|
27
|
-
if (values[childCollection]) {
|
|
28
|
-
entry[childCollection] = [
|
|
29
|
-
...entry[childCollection],
|
|
30
|
-
...values[childCollection],
|
|
31
|
-
];
|
|
32
|
-
}
|
|
21
|
+
for (const child of fkChildren) {
|
|
22
|
+
entry[child.collection] = values[child.collection] ? [...values[child.collection]] : [];
|
|
33
23
|
}
|
|
34
24
|
</script>
|
|
35
25
|
|
|
36
|
-
{#if
|
|
26
|
+
{#if fkChildren.length}
|
|
37
27
|
<div class="flex flex-col gap-4 border-t p-4">
|
|
38
28
|
<div class="flex items-center gap-2">
|
|
39
29
|
<Link size="17.5" />
|
|
40
30
|
<div>Sub Records</div>
|
|
41
31
|
</div>
|
|
42
32
|
<div class="flex flex-col gap-4">
|
|
43
|
-
{#each
|
|
44
|
-
{@const childCollection = relation.from.collection}
|
|
33
|
+
{#each fkChildren as child}
|
|
45
34
|
<ExtensionsComponents
|
|
46
|
-
name="detailView.create.subRecords.{
|
|
35
|
+
name="detailView.create.subRecords.{child.collection}"
|
|
47
36
|
utils={getExtensionUtils(lobb, ctx)}
|
|
48
37
|
parentCollectionName={collectionName}
|
|
49
|
-
collectionName={
|
|
50
|
-
parentRecord={{
|
|
51
|
-
id: entry.id,
|
|
52
|
-
collectionName: collectionName,
|
|
53
|
-
}}
|
|
38
|
+
collectionName={child.collection}
|
|
39
|
+
parentRecord={{ id: entry.id, collectionName }}
|
|
54
40
|
class="bg-muted/30 border rounded-md overflow-hidden"
|
|
55
|
-
bind:value={entry[
|
|
41
|
+
bind:value={entry[child.collection]}
|
|
56
42
|
>
|
|
57
43
|
<CreateManyView
|
|
58
44
|
parentCollectionName={collectionName}
|
|
59
|
-
collectionName={
|
|
60
|
-
parentRecord={{
|
|
61
|
-
|
|
62
|
-
collectionName: collectionName,
|
|
63
|
-
}}
|
|
64
|
-
bind:entries={entry[childCollection]}
|
|
45
|
+
collectionName={child.collection}
|
|
46
|
+
parentRecord={{ id: entry.id, collectionName }}
|
|
47
|
+
bind:entries={entry[child.collection]}
|
|
65
48
|
/>
|
|
66
49
|
</ExtensionsComponents>
|
|
67
50
|
{/each}
|
|
@@ -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>
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import DataTable from "../../../components/dataTable/dataTable.svelte";
|
|
3
3
|
import { getStudioContext } from "../../../context";
|
|
4
|
-
import { Link, Plus, TableIcon } from "lucide-svelte";
|
|
4
|
+
import { Link, Plus, TableIcon, Unlink } from "lucide-svelte";
|
|
5
5
|
import CreateDetailViewButton from "../create/createDetailViewButton.svelte";
|
|
6
6
|
import ExtensionsComponents from "../../../components/extensionsComponents.svelte";
|
|
7
7
|
import { getExtensionUtils } from "../../../extensions/extensionUtils";
|
|
8
|
+
import SelectRecord from "../../../components/selectRecord.svelte";
|
|
9
|
+
import { getCollectionColumns } from "../../dataTable/utils";
|
|
10
|
+
import Table from "../../../components/dataTable/table.svelte";
|
|
11
|
+
import Button from "../../../components/ui/button/button.svelte";
|
|
8
12
|
|
|
9
13
|
const { ctx, lobb } = getStudioContext();
|
|
10
14
|
|
|
@@ -15,67 +19,92 @@
|
|
|
15
19
|
|
|
16
20
|
let { collectionName, entry }: LocalProp = $props();
|
|
17
21
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const allChildren = ctx.meta.collections[collectionName]?.children ?? [];
|
|
23
|
+
const fkChildren = allChildren.filter((c) => c.type === "fk") as { type: "fk"; collection: string; field: string }[];
|
|
24
|
+
const relationalChildren = allChildren.filter((c) => c.type === "m2m" || c.type === "polymorphic") as { type: string; collection: string }[];
|
|
25
|
+
|
|
26
|
+
const fkRefresh: boolean[] = $state(new Array(fkChildren.length).fill(true));
|
|
27
|
+
|
|
28
|
+
let relationalRecords: Record<string, any[]> = $state({});
|
|
29
|
+
let relationalLoading = $state(false);
|
|
30
|
+
|
|
31
|
+
async function fetchRelational() {
|
|
32
|
+
if (!relationalChildren.length || !entry?.id) return;
|
|
33
|
+
relationalLoading = true;
|
|
34
|
+
const childrenParam: Record<string, any> = {};
|
|
35
|
+
for (const child of relationalChildren) {
|
|
36
|
+
childrenParam[child.collection] = { fields: ["*"] };
|
|
37
|
+
}
|
|
38
|
+
const response = await lobb.findAll(collectionName, {
|
|
39
|
+
filter: { id: entry.id },
|
|
40
|
+
limit: 1,
|
|
41
|
+
children: childrenParam,
|
|
42
|
+
});
|
|
43
|
+
const result = await response.json();
|
|
44
|
+
const record = result.data?.[0];
|
|
45
|
+
if (record) {
|
|
46
|
+
for (const child of relationalChildren) {
|
|
47
|
+
relationalRecords[child.collection] = record[child.collection] ?? [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
relationalLoading = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function linkRecord(childCollection: string, selected: any) {
|
|
54
|
+
await lobb.updateOne(collectionName, entry.id, {}, { [childCollection]: { link: [selected.id] } });
|
|
55
|
+
await fetchRelational();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function unlinkRecord(childCollection: string, id: any) {
|
|
59
|
+
await lobb.updateOne(collectionName, entry.id, {}, { [childCollection]: { unlink: [id] } });
|
|
60
|
+
await fetchRelational();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
$effect(() => {
|
|
64
|
+
fetchRelational();
|
|
65
|
+
});
|
|
24
66
|
</script>
|
|
25
67
|
|
|
26
|
-
{#if
|
|
68
|
+
{#if allChildren.length}
|
|
27
69
|
<div class="flex flex-col gap-4 border-t p-4">
|
|
28
70
|
<div class="flex items-center gap-2">
|
|
29
71
|
<Link size="17.5" />
|
|
30
72
|
<div>Sub Records</div>
|
|
31
73
|
</div>
|
|
32
74
|
<div class="flex flex-col gap-4">
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
75
|
+
|
|
76
|
+
<!-- FK children -->
|
|
77
|
+
{#each fkChildren as child, index}
|
|
36
78
|
<ExtensionsComponents
|
|
37
|
-
name="detailView.update.subRecords.{
|
|
79
|
+
name="detailView.update.subRecords.{child.collection}"
|
|
38
80
|
utils={getExtensionUtils(lobb, ctx)}
|
|
39
|
-
collectionName={
|
|
40
|
-
filter={{
|
|
41
|
-
[childField]: entry.id,
|
|
42
|
-
}}
|
|
81
|
+
collectionName={child.collection}
|
|
82
|
+
filter={{ [child.field]: entry.id }}
|
|
43
83
|
class="bg-muted/30 border rounded-md overflow-hidden"
|
|
44
84
|
>
|
|
45
85
|
<div class="border rounded-md overflow-clip">
|
|
46
|
-
<div
|
|
47
|
-
class="flex items-center justify-between px-2 h-10 bg-muted/30 border-b"
|
|
48
|
-
>
|
|
86
|
+
<div class="flex items-center justify-between px-2 h-10 bg-muted/30 border-b">
|
|
49
87
|
<div class="flex-1 flex h-full items-center gap-2">
|
|
50
|
-
<TableIcon
|
|
51
|
-
|
|
52
|
-
size="17.5"
|
|
53
|
-
/>
|
|
54
|
-
<div class="text-sm text-muted-foreground">
|
|
55
|
-
{childCollection}
|
|
56
|
-
</div>
|
|
88
|
+
<TableIcon class="text-muted-foreground" size="17.5" />
|
|
89
|
+
<div class="text-sm text-muted-foreground">{child.collection}</div>
|
|
57
90
|
</div>
|
|
58
91
|
<div class="flex gap-2">
|
|
59
92
|
<CreateDetailViewButton
|
|
60
93
|
variant="ghost"
|
|
61
94
|
class="h-7 px-2 font-normal text-xs"
|
|
62
95
|
Icon={Plus}
|
|
63
|
-
collectionName={
|
|
64
|
-
onSuccessfullSave={async () => {
|
|
65
|
-
refresh[index] = !refresh[index];
|
|
66
|
-
}}
|
|
96
|
+
collectionName={child.collection}
|
|
97
|
+
onSuccessfullSave={async () => { fkRefresh[index] = !fkRefresh[index]; }}
|
|
67
98
|
>
|
|
68
99
|
Create
|
|
69
100
|
</CreateDetailViewButton>
|
|
70
101
|
</div>
|
|
71
102
|
</div>
|
|
72
103
|
<div class="max-h-72 overflow-auto rounded-md">
|
|
73
|
-
{#key
|
|
104
|
+
{#key fkRefresh[index]}
|
|
74
105
|
<DataTable
|
|
75
|
-
collectionName={
|
|
76
|
-
filter={{
|
|
77
|
-
[childField]: entry.id,
|
|
78
|
-
}}
|
|
106
|
+
collectionName={child.collection}
|
|
107
|
+
filter={{ [child.field]: entry.id }}
|
|
79
108
|
unifiedBgColor="bg-muted/30"
|
|
80
109
|
showHeader={false}
|
|
81
110
|
showFooter={false}
|
|
@@ -91,6 +120,64 @@
|
|
|
91
120
|
</div>
|
|
92
121
|
</ExtensionsComponents>
|
|
93
122
|
{/each}
|
|
123
|
+
|
|
124
|
+
<!-- M2M and polymorphic children -->
|
|
125
|
+
{#each relationalChildren as child}
|
|
126
|
+
{@const columns = getCollectionColumns(ctx, child.collection)}
|
|
127
|
+
{@const linkedIds = new Set((relationalRecords[child.collection] ?? []).map((r: any) => String(r.id)))}
|
|
128
|
+
<div class="border rounded-md overflow-clip">
|
|
129
|
+
<div class="flex items-center justify-between px-2 h-10 bg-muted/30 border-b">
|
|
130
|
+
<div class="flex-1 flex h-full items-center gap-2">
|
|
131
|
+
<TableIcon class="text-muted-foreground" size="17.5" />
|
|
132
|
+
<div class="text-sm text-muted-foreground">{child.collection}</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="flex gap-2">
|
|
135
|
+
<CreateDetailViewButton
|
|
136
|
+
variant="ghost"
|
|
137
|
+
class="h-7 px-2 font-normal text-xs"
|
|
138
|
+
Icon={Plus}
|
|
139
|
+
collectionName={child.collection}
|
|
140
|
+
onSuccessfullSave={async (newEntry) => { await linkRecord(child.collection, newEntry); }}
|
|
141
|
+
>
|
|
142
|
+
Create & link
|
|
143
|
+
</CreateDetailViewButton>
|
|
144
|
+
<SelectRecord
|
|
145
|
+
collectionName={child.collection}
|
|
146
|
+
text="Link existing"
|
|
147
|
+
onSelect={(selected) => linkRecord(child.collection, selected)}
|
|
148
|
+
filter={{ id: { $nin: [...linkedIds] } }}
|
|
149
|
+
variant="ghost"
|
|
150
|
+
class="h-7 px-2 font-normal text-xs"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="max-h-72 overflow-auto bg-muted/30">
|
|
155
|
+
{#if relationalLoading}
|
|
156
|
+
<div class="p-4 text-sm text-muted-foreground">Loading...</div>
|
|
157
|
+
{:else}
|
|
158
|
+
<Table
|
|
159
|
+
data={relationalRecords[child.collection] ?? []}
|
|
160
|
+
{columns}
|
|
161
|
+
unifiedBgColor="bg-muted/30"
|
|
162
|
+
showLastRowBorder={false}
|
|
163
|
+
showLastColumnBorder={false}
|
|
164
|
+
showCheckboxes={false}
|
|
165
|
+
>
|
|
166
|
+
{#snippet tools(record, _index)}
|
|
167
|
+
<Button
|
|
168
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
169
|
+
variant="ghost"
|
|
170
|
+
size="icon"
|
|
171
|
+
onclick={() => unlinkRecord(child.collection, record.id)}
|
|
172
|
+
Icon={Unlink}
|
|
173
|
+
/>
|
|
174
|
+
{/snippet}
|
|
175
|
+
</Table>
|
|
176
|
+
{/if}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
{/each}
|
|
180
|
+
|
|
94
181
|
</div>
|
|
95
182
|
</div>
|
|
96
183
|
{/if}
|
|
@@ -30,12 +30,10 @@ 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
38
|
const isRefrenceField = isRelationField(ctx, collectionName, fieldName);
|
|
41
39
|
if (isRefrenceField && fieldValue !== null && fieldValue.id !== undefined) {
|
|
@@ -43,7 +41,7 @@ export function serializeEntry(
|
|
|
43
41
|
}
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
//
|
|
44
|
+
// extract polymorphic id_field objects → ID
|
|
47
45
|
for (const relation of ctx.meta.relations) {
|
|
48
46
|
if (relation.type !== "polymorphic") continue;
|
|
49
47
|
if ((relation as any).from.collection !== collectionName) continue;
|
|
@@ -54,90 +52,40 @@ export function serializeEntry(
|
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
// check for related collections properties and serialize them too
|
|
58
|
-
if (!rollback) {
|
|
59
|
-
const childrenRelations = ctx.meta.relations.filter((relation) => relation.to.collection === collectionName);
|
|
60
|
-
const childrenCollectionNames = childrenRelations.map((relation) => relation.from.collection);
|
|
61
|
-
for (let index = 0; index < childrenCollectionNames.length; index++) {
|
|
62
|
-
const childrenCollectionName = childrenCollectionNames[index];
|
|
63
|
-
const childrenEntries = entry[childrenCollectionName];
|
|
64
|
-
if (childrenEntries) {
|
|
65
|
-
for (let index = 0; index < childrenEntries.length; index++) {
|
|
66
|
-
childrenEntries[index] = serializeEntry(ctx, childrenCollectionName, childrenEntries[index]);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
55
|
return entry;
|
|
73
56
|
}
|
|
74
57
|
|
|
75
|
-
export function
|
|
58
|
+
export function buildChildren(
|
|
76
59
|
ctx: CTX,
|
|
77
60
|
collectionName: string,
|
|
78
61
|
entry: Record<string, any>,
|
|
79
|
-
) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
collectionName: string,
|
|
84
|
-
entry: Record<string, any>,
|
|
85
|
-
parentTransactionIndex?: number,
|
|
86
|
-
) {
|
|
87
|
-
const parentCollectionName = parentTransactionIndex !== undefined ? transactionBody[parentTransactionIndex].props.collectionName : null;
|
|
88
|
-
const foreignKeyFieldName = ctx.meta.relations.find(relation => relation.from.collection === collectionName && relation.to.collection === parentCollectionName)?.from.field;
|
|
89
|
-
const collectionFieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
|
|
90
|
-
const payload: any = {};
|
|
91
|
-
for (let index = 0; index < collectionFieldNames.length; index++) {
|
|
92
|
-
const fieldName = collectionFieldNames[index];
|
|
93
|
-
const isForeignKeyField = fieldName === foreignKeyFieldName;
|
|
94
|
-
if (isForeignKeyField) {
|
|
95
|
-
payload[fieldName] = `{{ responses[${parentTransactionIndex}].data.id }}`
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
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
|
+
);
|
|
98
66
|
|
|
99
|
-
|
|
100
|
-
}
|
|
67
|
+
const children: Record<string, { create?: any[]; link?: any[] }> = {};
|
|
101
68
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
};
|
|
107
|
-
transactionBody.push({
|
|
108
|
-
method: "updateMany",
|
|
109
|
-
props: {
|
|
110
|
-
collectionName: collectionName,
|
|
111
|
-
data: localPayload,
|
|
112
|
-
filter: { id: payload.id },
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
} else {
|
|
116
|
-
transactionBody.push({
|
|
117
|
-
method: "createOne",
|
|
118
|
-
props: { collectionName: collectionName, data: payload },
|
|
119
|
-
});
|
|
120
|
-
}
|
|
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;
|
|
121
73
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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;
|
|
133
85
|
}
|
|
134
86
|
}
|
|
135
87
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
handleEntryRecursive(transactionBody, collectionName, entry);
|
|
139
|
-
|
|
140
|
-
return transactionBody
|
|
88
|
+
return Object.keys(children).length ? children : undefined;
|
|
141
89
|
}
|
|
142
90
|
|
|
143
91
|
export function parseDetailViewValues(ctx: CTX, collectionName: string, values: Record<string, any>) {
|