@lobb-js/studio 0.30.0 → 0.32.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 +2 -0
- package/dist/components/Studio.svelte +1 -10
- package/dist/components/dataTable/dataTable.svelte +104 -39
- package/dist/components/dataTable/dataTable.svelte.d.ts +4 -1
- package/dist/components/dataTable/fieldCell.svelte +7 -4
- package/dist/components/dataTable/fieldCell.svelte.d.ts +2 -2
- package/dist/components/dataTable/filter.svelte +0 -15
- package/dist/components/dataTable/header.svelte +13 -14
- package/dist/components/dataTable/header.svelte.d.ts +3 -2
- package/dist/components/dataTable/numberCell.svelte +28 -0
- package/dist/components/dataTable/numberCell.svelte.d.ts +7 -0
- package/dist/components/dataTable/polymorphicFieldCell.svelte +3 -3
- package/dist/components/dataTable/polymorphicFieldCell.svelte.d.ts +2 -2
- package/dist/components/dataTablePopup/dataTablePopup.svelte +17 -0
- package/dist/components/dataTablePopup/dataTablePopup.svelte.d.ts +2 -0
- package/dist/components/detailView/create/createDetailView.svelte +28 -54
- package/dist/components/detailView/create/createDetailView.svelte.d.ts +4 -3
- package/dist/components/detailView/create/createDetailViewChildren.svelte +113 -0
- package/dist/components/detailView/create/createDetailViewChildren.svelte.d.ts +9 -0
- package/dist/components/detailView/create/createManyView.svelte +2 -2
- package/dist/components/detailView/detailView.svelte +6 -1
- package/dist/components/detailView/fieldInput.svelte +7 -5
- package/dist/components/detailView/update/updateDetailView.svelte +46 -40
- package/dist/components/detailView/update/updateDetailView.svelte.d.ts +5 -3
- package/dist/components/detailView/update/updateDetailViewButton.svelte +0 -1
- package/dist/components/detailView/update/updateDetailViewChildren.svelte +122 -0
- package/dist/components/detailView/update/updateDetailViewChildren.svelte.d.ts +10 -0
- package/dist/components/detailView/utils.d.ts +1 -2
- package/dist/components/importButton.svelte +1 -1
- package/dist/components/miniSidebar.svelte +6 -3
- package/dist/components/richTextEditor.svelte +2 -0
- package/dist/components/routes/extensions/extension.svelte +1 -1
- package/dist/components/routes/home.svelte +35 -21
- package/dist/components/ui/input/numberInput.svelte +104 -0
- package/dist/components/ui/input/numberInput.svelte.d.ts +9 -0
- package/dist/components/workflowEditor.svelte +6 -4
- package/package.json +4 -3
- package/src/lib/actions.ts +2 -0
- package/src/lib/components/Studio.svelte +1 -10
- package/src/lib/components/dataTable/dataTable.svelte +104 -39
- package/src/lib/components/dataTable/fieldCell.svelte +7 -4
- package/src/lib/components/dataTable/filter.svelte +0 -15
- package/src/lib/components/dataTable/header.svelte +13 -14
- package/src/lib/components/dataTable/numberCell.svelte +28 -0
- package/src/lib/components/dataTable/polymorphicFieldCell.svelte +3 -3
- package/src/lib/components/dataTablePopup/dataTablePopup.svelte +17 -0
- package/src/lib/components/detailView/create/createDetailView.svelte +28 -54
- package/src/lib/components/detailView/create/createDetailViewChildren.svelte +113 -0
- package/src/lib/components/detailView/create/createManyView.svelte +2 -2
- package/src/lib/components/detailView/detailView.svelte +6 -1
- package/src/lib/components/detailView/fieldInput.svelte +7 -5
- package/src/lib/components/detailView/update/updateDetailView.svelte +46 -40
- package/src/lib/components/detailView/update/updateDetailViewButton.svelte +0 -1
- package/src/lib/components/detailView/update/updateDetailViewChildren.svelte +122 -0
- package/src/lib/components/detailView/utils.ts +1 -1
- package/src/lib/components/importButton.svelte +1 -1
- package/src/lib/components/miniSidebar.svelte +6 -3
- package/src/lib/components/richTextEditor.svelte +2 -0
- package/src/lib/components/routes/extensions/extension.svelte +1 -1
- package/src/lib/components/routes/home.svelte +35 -21
- package/src/lib/components/ui/input/numberInput.svelte +104 -0
- package/src/lib/components/workflowEditor.svelte +6 -4
- package/dist/components/breadCrumbs.svelte +0 -61
- package/dist/components/breadCrumbs.svelte.d.ts +0 -3
- package/dist/components/detailView/update/detailViewChildren.svelte +0 -61
- package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +0 -9
- package/dist/components/header.svelte +0 -45
- package/dist/components/header.svelte.d.ts +0 -6
- package/src/lib/components/breadCrumbs.svelte +0 -61
- package/src/lib/components/detailView/update/detailViewChildren.svelte +0 -61
- package/src/lib/components/header.svelte +0 -45
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import DataTable from "../../dataTable/dataTable.svelte";
|
|
3
|
+
import { getStudioContext } from "../../../context";
|
|
4
|
+
import { Table, Link, Plus } from "lucide-svelte";
|
|
5
|
+
import { untrack } from "svelte";
|
|
6
|
+
import CreateDetailViewButton from "./createDetailViewButton.svelte";
|
|
7
|
+
import SelectRecord from "../../selectRecord.svelte";
|
|
8
|
+
|
|
9
|
+
const { ctx } = getStudioContext();
|
|
10
|
+
|
|
11
|
+
import type { Changes, ChildrenChanges } from "../utils";
|
|
12
|
+
|
|
13
|
+
interface LocalProp {
|
|
14
|
+
collectionName: string;
|
|
15
|
+
changes?: Changes;
|
|
16
|
+
onChanges?: (children: Changes["children"]) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { collectionName, changes, onChanges }: LocalProp = $props();
|
|
20
|
+
|
|
21
|
+
const children = (ctx.meta.collections[collectionName]?.children ?? [])
|
|
22
|
+
.filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
|
|
23
|
+
|
|
24
|
+
let localChildren = $state<Changes["children"]>(untrack(() => changes?.children ?? {}));
|
|
25
|
+
|
|
26
|
+
function handleChildChanges(collection: string, updated: ChildrenChanges) {
|
|
27
|
+
localChildren[collection] = updated;
|
|
28
|
+
onChanges?.($state.snapshot(localChildren));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleEmptyCreate(collection: string, c: Changes) {
|
|
32
|
+
if (!localChildren[collection]) {
|
|
33
|
+
localChildren[collection] = { created: [], updated: [], deleted: [], linked: [], unlinked: [] };
|
|
34
|
+
}
|
|
35
|
+
localChildren[collection].created.push({ data: c.data });
|
|
36
|
+
onChanges?.($state.snapshot(localChildren));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleEmptyLink(collection: string, record: any) {
|
|
40
|
+
if (!localChildren[collection]) {
|
|
41
|
+
localChildren[collection] = { created: [], updated: [], deleted: [], linked: [], unlinked: [] };
|
|
42
|
+
}
|
|
43
|
+
localChildren[collection].linked.push(record);
|
|
44
|
+
onChanges?.($state.snapshot(localChildren));
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
{#if children.length}
|
|
49
|
+
<div class="flex flex-col gap-3 border-t p-4">
|
|
50
|
+
<div class="flex items-center gap-2">
|
|
51
|
+
<Link size="14" class="text-muted-foreground" />
|
|
52
|
+
<span class="text-sm font-medium">Sub Records</span>
|
|
53
|
+
</div>
|
|
54
|
+
{#each children as child}
|
|
55
|
+
{@const localAdditions = (localChildren[child.collection]?.created.length ?? 0) + (localChildren[child.collection]?.linked.length ?? 0)}
|
|
56
|
+
{#if localAdditions === 0}
|
|
57
|
+
<div class="rounded-lg border bg-muted-soft overflow-hidden flex flex-col">
|
|
58
|
+
<div class="flex flex-col items-center justify-center gap-3 py-6 px-4">
|
|
59
|
+
<div class="flex flex-col items-center gap-2 text-center">
|
|
60
|
+
<div class="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
|
|
61
|
+
<span>No records in</span>
|
|
62
|
+
<span class="rounded-md border bg-muted px-2 py-0.5 text-xs font-normal">{child.collection}</span>
|
|
63
|
+
</div>
|
|
64
|
+
<span class="text-xs text-muted-foreground/70">Create a new record or link an existing one.</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="flex gap-2">
|
|
67
|
+
<SelectRecord
|
|
68
|
+
collectionName={child.collection}
|
|
69
|
+
variant="outline"
|
|
70
|
+
class="h-7 px-3 text-xs font-normal"
|
|
71
|
+
Icon={Link}
|
|
72
|
+
onSelect={(r) => handleEmptyLink(child.collection, r)}
|
|
73
|
+
>
|
|
74
|
+
Link
|
|
75
|
+
</SelectRecord>
|
|
76
|
+
<CreateDetailViewButton
|
|
77
|
+
collectionName={child.collection}
|
|
78
|
+
variant="default"
|
|
79
|
+
class="h-7 px-3 text-xs font-normal"
|
|
80
|
+
Icon={Plus}
|
|
81
|
+
onChanges={(c) => handleEmptyCreate(child.collection, c)}
|
|
82
|
+
>
|
|
83
|
+
Create
|
|
84
|
+
</CreateDetailViewButton>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
{:else}
|
|
89
|
+
<div class="rounded-lg border bg-background overflow-hidden flex flex-col max-h-96">
|
|
90
|
+
<DataTable
|
|
91
|
+
collectionName={child.collection}
|
|
92
|
+
searchParams={{ children_of: collectionName, parent_id: -1 }}
|
|
93
|
+
onChanges={(updated) => handleChildChanges(child.collection, updated)}
|
|
94
|
+
changes={localChildren[child.collection]}
|
|
95
|
+
showImport={false}
|
|
96
|
+
showHeader={true}
|
|
97
|
+
showFooter={false}
|
|
98
|
+
showDelete={false}
|
|
99
|
+
showEdit={false}
|
|
100
|
+
tableProps={{ showLastColumnBorder: false, showLastRowBorder: true }}
|
|
101
|
+
>
|
|
102
|
+
{#snippet headerLeft()}
|
|
103
|
+
<div class="flex items-center gap-2 px-1">
|
|
104
|
+
<Table size="14" class="text-muted-foreground" />
|
|
105
|
+
<span class="text-sm font-medium">{child.collection}</span>
|
|
106
|
+
</div>
|
|
107
|
+
{/snippet}
|
|
108
|
+
</DataTable>
|
|
109
|
+
</div>
|
|
110
|
+
{/if}
|
|
111
|
+
{/each}
|
|
112
|
+
</div>
|
|
113
|
+
{/if}
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
class="h-7 px-2 font-normal text-xs"
|
|
146
146
|
Icon={Plus}
|
|
147
147
|
{collectionName}
|
|
148
|
-
|
|
148
|
+
onChanges={(updated) => { addChanges = updated; }}
|
|
149
149
|
showRelatedRecords={true}
|
|
150
150
|
onSuccessfullSave={onRecordAdd}
|
|
151
151
|
values={createValues}
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
class="h-6 w-6 text-muted-foreground hover:bg-transparent p-0"
|
|
201
201
|
Icon={Pencil}
|
|
202
202
|
{collectionName}
|
|
203
|
-
|
|
203
|
+
onChanges={(updated) => { editChanges = updated; }}
|
|
204
204
|
showRelatedRecords={true}
|
|
205
205
|
onSuccessfullSave={(entry) =>
|
|
206
206
|
onRecordOverride(entry, index)}
|
|
@@ -20,8 +20,13 @@
|
|
|
20
20
|
}: Props = $props();
|
|
21
21
|
|
|
22
22
|
const { lobb, ctx } = getStudioContext();
|
|
23
|
+
// Singleton collections only ever have one row, and `id` is auto-
|
|
24
|
+
// generated for that row — there's no value to showing it in the form.
|
|
23
25
|
const fieldNames = $derived(
|
|
24
|
-
Object.keys(ctx.meta.collections[collectionName].fields)
|
|
26
|
+
Object.keys(ctx.meta.collections[collectionName].fields).filter(
|
|
27
|
+
(fieldName) =>
|
|
28
|
+
!(ctx.meta.collections[collectionName].singleton && fieldName === "id"),
|
|
29
|
+
),
|
|
25
30
|
);
|
|
26
31
|
</script>
|
|
27
32
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import Button from "../ui/button/button.svelte";
|
|
7
7
|
import FieldCustomInput from "./fieldCustomInput.svelte";
|
|
8
8
|
import Input from "../ui/input/input.svelte";
|
|
9
|
+
import NumberInput from "../ui/input/numberInput.svelte";
|
|
9
10
|
import * as Select from "../ui/select/index";
|
|
10
11
|
import EnumBadge from "../dataTable/enumBadge.svelte";
|
|
11
12
|
import type { EnumOption } from "@lobb-js/core";
|
|
@@ -257,11 +258,12 @@
|
|
|
257
258
|
</Select.Item>
|
|
258
259
|
</Select.Content>
|
|
259
260
|
</Select.Root>
|
|
260
|
-
{:else if field.type === "decimal"}
|
|
261
|
-
|
|
261
|
+
{:else if field.type === "decimal" || field.type === "float" || field.type === "integer" || field.type === "long"}
|
|
262
|
+
{@const isFloat = field.type === "decimal" || field.type === "float"}
|
|
263
|
+
<NumberInput
|
|
262
264
|
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
scale={isFloat ? 20 : 0}
|
|
266
|
+
groupDigits={ui?.groupDigits ?? false}
|
|
265
267
|
class="
|
|
266
268
|
bg-muted-soft text-xs
|
|
267
269
|
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
@@ -271,7 +273,7 @@
|
|
|
271
273
|
{:else}
|
|
272
274
|
<Input
|
|
273
275
|
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
274
|
-
type="
|
|
276
|
+
type="text"
|
|
275
277
|
class="
|
|
276
278
|
bg-muted-soft text-xs
|
|
277
279
|
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
+
import type { Changes } from "../utils";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
|
|
2
5
|
interface SubmitButton {
|
|
3
6
|
text: string;
|
|
4
7
|
icon: any;
|
|
@@ -8,12 +11,13 @@
|
|
|
8
11
|
collectionName: string;
|
|
9
12
|
recordId: string;
|
|
10
13
|
values?: Record<string, any>;
|
|
14
|
+
changes?: Changes;
|
|
15
|
+
onChanges?: (changes: Changes) => void;
|
|
11
16
|
showRelatedRecords?: boolean;
|
|
12
17
|
submitButton?: SubmitButton;
|
|
13
18
|
title?: Snippet<[string]>;
|
|
14
19
|
onSuccessfullSave?: (entry: any) => Promise<void>;
|
|
15
20
|
onCancel?: () => Promise<void>;
|
|
16
|
-
changes?: import("../utils").Changes | undefined;
|
|
17
21
|
}
|
|
18
22
|
</script>
|
|
19
23
|
|
|
@@ -26,10 +30,9 @@
|
|
|
26
30
|
|
|
27
31
|
const { lobb, ctx } = getStudioContext();
|
|
28
32
|
import { getChangedProperties } from "../../../utils";
|
|
29
|
-
import
|
|
30
|
-
import type { Snippet } from "svelte";
|
|
33
|
+
import UpdateDetailViewChildren from "./updateDetailViewChildren.svelte";
|
|
31
34
|
import { getDefaultEntry } from "../utils";
|
|
32
|
-
import type {
|
|
35
|
+
import type { ChildrenChanges } from "../utils";
|
|
33
36
|
import DetailView from "../detailView.svelte";
|
|
34
37
|
import Drawer from "../../drawer.svelte";
|
|
35
38
|
|
|
@@ -42,50 +45,67 @@
|
|
|
42
45
|
title,
|
|
43
46
|
submitButton,
|
|
44
47
|
recordId,
|
|
45
|
-
changes
|
|
48
|
+
changes,
|
|
49
|
+
onChanges,
|
|
46
50
|
}: UpdateDetailViewProp = $props();
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const isRecordingMode = onChanges !== undefined;
|
|
53
|
+
let localChanges = $state<Changes>(
|
|
54
|
+
untrack(() => ({ data: changes?.data ?? {}, children: changes?.children ?? {} }))
|
|
55
|
+
);
|
|
52
56
|
|
|
53
57
|
const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
const mergedValues = untrack(() => changes?.data
|
|
59
|
+
? { ...passedValues, ...changes.data }
|
|
60
|
+
: passedValues);
|
|
61
|
+
let values = $state(getDefaultEntry(ctx, fieldNames, collectionName, mergedValues));
|
|
62
|
+
const initialValues = $state.snapshot(getDefaultEntry(ctx, fieldNames, collectionName, passedValues));
|
|
56
63
|
let fieldsErrors: Record<string, any> = $state({});
|
|
57
64
|
|
|
58
65
|
const hasChanges = $derived(
|
|
59
|
-
Object.keys(
|
|
60
|
-
Object.values(
|
|
66
|
+
Object.keys(localChanges.data).length > 0 ||
|
|
67
|
+
Object.values(localChanges.children).some(
|
|
61
68
|
(ch: ChildrenChanges) => ch.created.length || ch.updated.length || ch.deleted.length || ch.linked.length || ch.unlinked.length,
|
|
62
69
|
),
|
|
63
70
|
);
|
|
64
71
|
|
|
65
|
-
// Tracks top-level field edits into changes.data.
|
|
66
|
-
// Child ops (create/link/unlink/delete) are written directly by DataTable into changes.children.
|
|
67
72
|
$effect(() => {
|
|
68
73
|
const currentEntrySnap = $state.snapshot(values);
|
|
69
74
|
|
|
70
75
|
untrack(() => {
|
|
71
|
-
|
|
76
|
+
localChanges.data = getChangedProperties(initialValues, currentEntrySnap);
|
|
72
77
|
});
|
|
73
78
|
});
|
|
74
79
|
|
|
75
|
-
// Separate logging effect — needs its own $effect so it tracks mutations to changes.children.
|
|
76
80
|
$effect(() => {
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
const snap = $state.snapshot(localChanges);
|
|
82
|
+
if (isRecordingMode) {
|
|
83
|
+
const hasAny = Object.keys(snap.data).length > 0 ||
|
|
84
|
+
Object.values(snap.children).some((c: ChildrenChanges) =>
|
|
85
|
+
c.created.length || c.updated.length || c.deleted.length || c.linked.length || c.unlinked.length
|
|
86
|
+
);
|
|
87
|
+
if (hasAny) untrack(() => onChanges?.(snap));
|
|
79
88
|
}
|
|
80
89
|
});
|
|
81
90
|
|
|
82
|
-
function
|
|
91
|
+
function buildPayload(changes: Changes): { data: Record<string, any>; children?: Record<string, any> } {
|
|
92
|
+
const { id: _id, ...data } = changes.data;
|
|
93
|
+
const children = buildChildren(changes.children);
|
|
94
|
+
return { data, ...(children ? { children } : {}) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildChildren(children: Record<string, ChildrenChanges>): Record<string, any> | undefined {
|
|
83
98
|
const result: Record<string, any> = {};
|
|
84
99
|
for (const [collection, ops] of Object.entries(children)) {
|
|
85
|
-
const hasOps = ops.created.length || ops.deleted.length || ops.linked.length || ops.unlinked.length;
|
|
100
|
+
const hasOps = ops.created.length || ops.updated.length || ops.deleted.length || ops.linked.length || ops.unlinked.length;
|
|
86
101
|
if (!hasOps) continue;
|
|
87
102
|
result[collection] = {
|
|
88
103
|
...(ops.created.length ? { create: ops.created.map((op) => op.data) } : {}),
|
|
104
|
+
...(ops.updated.length ? { update: ops.updated.map((u) => ({
|
|
105
|
+
id: u.id,
|
|
106
|
+
...(Object.keys(u.changes.data).length ? { data: u.changes.data } : {}),
|
|
107
|
+
...(Object.keys(u.changes.children).length ? { children: buildChildren(u.changes.children) } : {}),
|
|
108
|
+
})) } : {}),
|
|
89
109
|
...(ops.deleted.length ? { delete: ops.deleted.map((r) => r.id) } : {}),
|
|
90
110
|
...(ops.linked.length ? { link: ops.linked.map((r) => r.id) } : {}),
|
|
91
111
|
...(ops.unlinked.length ? { unlink: ops.unlinked.map((r) => r.id) } : {}),
|
|
@@ -95,21 +115,15 @@
|
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
function handleCancel() {
|
|
98
|
-
if (isRecordingMode) {
|
|
99
|
-
changes.data = {};
|
|
100
|
-
changes.children = {};
|
|
101
|
-
}
|
|
102
118
|
onCancel?.();
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
async function handleSave() {
|
|
106
|
-
const snap = $state.snapshot(
|
|
107
|
-
const
|
|
108
|
-
const children = buildApiChildren(snap.children);
|
|
109
|
-
|
|
110
|
-
const response = await lobb.updateOne(collectionName, recordId, data, children, isRecordingMode);
|
|
122
|
+
const snap = $state.snapshot(localChanges);
|
|
123
|
+
const response = await lobb.updateOne(collectionName, recordId, buildPayload(snap), isRecordingMode);
|
|
111
124
|
|
|
112
125
|
if (response.status === 204) {
|
|
126
|
+
onChanges?.(snap);
|
|
113
127
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
114
128
|
toast.success(`The record was successfully updated`);
|
|
115
129
|
onCancel?.();
|
|
@@ -129,15 +143,7 @@
|
|
|
129
143
|
}
|
|
130
144
|
}
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
if (!isRecordingMode) {
|
|
134
|
-
for (const [collection, ops] of Object.entries(snap.children) as [string, ChildrenChanges][]) {
|
|
135
|
-
for (const updated of ops.updated) {
|
|
136
|
-
await lobb.updateOne(collection, String(updated.id), updated.data);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
146
|
+
onChanges?.(snap);
|
|
141
147
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
142
148
|
toast.success(`The record was successfully updated`);
|
|
143
149
|
onCancel?.();
|
|
@@ -166,7 +172,7 @@
|
|
|
166
172
|
<div class="flex-1 overflow-y-auto">
|
|
167
173
|
<DetailView {collectionName} bind:entry={values} {fieldsErrors} />
|
|
168
174
|
{#if showRelatedRecords}
|
|
169
|
-
<
|
|
175
|
+
<UpdateDetailViewChildren {collectionName} entry={values} changes={localChanges} onChanges={(children) => { localChanges.children = children; }} />
|
|
170
176
|
{/if}
|
|
171
177
|
</div>
|
|
172
178
|
<div class="flex h-12 items-center justify-end gap-2 border-t px-4">
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import DataTable from "../../dataTable/dataTable.svelte";
|
|
3
|
+
import { getStudioContext } from "../../../context";
|
|
4
|
+
import { Table, Link, Plus } from "lucide-svelte";
|
|
5
|
+
import { untrack } from "svelte";
|
|
6
|
+
import CreateDetailViewButton from "../create/createDetailViewButton.svelte";
|
|
7
|
+
import SelectRecord from "../../selectRecord.svelte";
|
|
8
|
+
|
|
9
|
+
const { ctx } = getStudioContext();
|
|
10
|
+
|
|
11
|
+
import type { Changes, ChildrenChanges } from "../utils";
|
|
12
|
+
|
|
13
|
+
interface LocalProp {
|
|
14
|
+
collectionName: string;
|
|
15
|
+
entry: any;
|
|
16
|
+
changes?: Changes;
|
|
17
|
+
onChanges?: (children: Changes["children"]) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { collectionName, entry, changes, onChanges }: LocalProp = $props();
|
|
21
|
+
|
|
22
|
+
const children = (ctx.meta.collections[collectionName]?.children ?? [])
|
|
23
|
+
.filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
|
|
24
|
+
|
|
25
|
+
let localChildren = $state<Changes["children"]>(untrack(() => changes?.children ?? {}));
|
|
26
|
+
let serverCounts = $state<Record<string, number | undefined>>({});
|
|
27
|
+
|
|
28
|
+
function handleChildChanges(collection: string, updated: ChildrenChanges) {
|
|
29
|
+
localChildren[collection] = updated;
|
|
30
|
+
onChanges?.($state.snapshot(localChildren));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function handleDataLoad(collection: string, total: number) {
|
|
34
|
+
serverCounts[collection] = total;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function handleEmptyCreate(collection: string, c: Changes) {
|
|
38
|
+
if (!localChildren[collection]) {
|
|
39
|
+
localChildren[collection] = { created: [], updated: [], deleted: [], linked: [], unlinked: [] };
|
|
40
|
+
}
|
|
41
|
+
localChildren[collection].created.push({ data: c.data });
|
|
42
|
+
onChanges?.($state.snapshot(localChildren));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function handleEmptyLink(collection: string, record: any) {
|
|
46
|
+
if (!localChildren[collection]) {
|
|
47
|
+
localChildren[collection] = { created: [], updated: [], deleted: [], linked: [], unlinked: [] };
|
|
48
|
+
}
|
|
49
|
+
localChildren[collection].linked.push(record);
|
|
50
|
+
onChanges?.($state.snapshot(localChildren));
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
{#if children.length}
|
|
55
|
+
<div class="flex flex-col gap-3 border-t p-4">
|
|
56
|
+
<div class="flex items-center gap-2">
|
|
57
|
+
<Link size="14" class="text-muted-foreground" />
|
|
58
|
+
<span class="text-sm font-medium">Sub Records</span>
|
|
59
|
+
</div>
|
|
60
|
+
{#each children as child}
|
|
61
|
+
{@const serverCount = serverCounts[child.collection]}
|
|
62
|
+
{@const localAdditions = (localChildren[child.collection]?.created.length ?? 0) + (localChildren[child.collection]?.linked.length ?? 0)}
|
|
63
|
+
{@const showEmpty = serverCount !== undefined && serverCount === 0 && localAdditions === 0}
|
|
64
|
+
{#if showEmpty}
|
|
65
|
+
<div class="rounded-lg border bg-muted-soft overflow-hidden flex flex-col">
|
|
66
|
+
<div class="flex flex-col items-center justify-center gap-3 py-6 px-4">
|
|
67
|
+
<div class="flex flex-col items-center gap-2 text-center">
|
|
68
|
+
<div class="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
|
|
69
|
+
<span>No records in</span>
|
|
70
|
+
<span class="rounded-md border bg-muted px-2 py-0.5 text-xs font-normal">{child.collection}</span>
|
|
71
|
+
</div>
|
|
72
|
+
<span class="text-xs text-muted-foreground/70">Create a new record or link an existing one.</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="flex gap-2">
|
|
75
|
+
<SelectRecord
|
|
76
|
+
collectionName={child.collection}
|
|
77
|
+
variant="outline"
|
|
78
|
+
class="h-7 px-3 text-xs font-normal"
|
|
79
|
+
Icon={Link}
|
|
80
|
+
onSelect={(r) => handleEmptyLink(child.collection, r)}
|
|
81
|
+
>
|
|
82
|
+
Link
|
|
83
|
+
</SelectRecord>
|
|
84
|
+
<CreateDetailViewButton
|
|
85
|
+
collectionName={child.collection}
|
|
86
|
+
variant="default"
|
|
87
|
+
class="h-7 px-3 text-xs font-normal"
|
|
88
|
+
Icon={Plus}
|
|
89
|
+
onChanges={(c) => handleEmptyCreate(child.collection, c)}
|
|
90
|
+
>
|
|
91
|
+
Create
|
|
92
|
+
</CreateDetailViewButton>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
{:else}
|
|
97
|
+
<div class="rounded-lg border bg-background overflow-hidden flex flex-col max-h-96">
|
|
98
|
+
<DataTable
|
|
99
|
+
collectionName={child.collection}
|
|
100
|
+
searchParams={{ children_of: collectionName, parent_id: entry.id }}
|
|
101
|
+
parentContext={{ collectionName, recordId: entry.id }}
|
|
102
|
+
onChanges={(updated) => handleChildChanges(child.collection, updated)}
|
|
103
|
+
changes={localChildren[child.collection]}
|
|
104
|
+
showImport={false}
|
|
105
|
+
showHeader={true}
|
|
106
|
+
showFooter={true}
|
|
107
|
+
showDelete={child.type === "fk" || child.type === "m2m"}
|
|
108
|
+
tableProps={{ showLastColumnBorder: false, showLastRowBorder: true }}
|
|
109
|
+
onDataLoad={(total) => handleDataLoad(child.collection, total)}
|
|
110
|
+
>
|
|
111
|
+
{#snippet headerLeft()}
|
|
112
|
+
<div class="flex items-center gap-2 px-1">
|
|
113
|
+
<Table size="14" class="text-muted-foreground" />
|
|
114
|
+
<span class="text-sm font-medium">{child.collection}</span>
|
|
115
|
+
</div>
|
|
116
|
+
{/snippet}
|
|
117
|
+
</DataTable>
|
|
118
|
+
</div>
|
|
119
|
+
{/if}
|
|
120
|
+
{/each}
|
|
121
|
+
</div>
|
|
122
|
+
{/if}
|
|
@@ -5,7 +5,7 @@ import type { DetailFormField } from "./detailViewForm.svelte";
|
|
|
5
5
|
|
|
6
6
|
export type ChildrenChanges = {
|
|
7
7
|
created: Array<{ data: Record<string, any> }>;
|
|
8
|
-
updated: Array<{ id: string | number;
|
|
8
|
+
updated: Array<{ id: string | number; changes: Changes }>;
|
|
9
9
|
deleted: Array<Record<string, any>>;
|
|
10
10
|
linked: Array<Record<string, any>>;
|
|
11
11
|
unlinked: Array<Record<string, any>>;
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
importResults = [];
|
|
133
133
|
let hasSuccess = false;
|
|
134
134
|
for (const row of finalRows) {
|
|
135
|
-
const response = await lobb.createOne(collectionName, row);
|
|
135
|
+
const response = await lobb.createOne(collectionName, { data: row });
|
|
136
136
|
if (response.ok) {
|
|
137
137
|
importResults.push({ row, error: null });
|
|
138
138
|
hasSuccess = true;
|
|
@@ -90,12 +90,15 @@
|
|
|
90
90
|
// prefix of everything); other items use startsWith so sub-paths
|
|
91
91
|
// (e.g. /studio/collections/risks) still highlight their parent.
|
|
92
92
|
// Popover items with children are active when any of their children match.
|
|
93
|
-
|
|
93
|
+
// SvelteKit's pathname can come back with a trailing slash (`/studio/`)
|
|
94
|
+
// depending on config, so we normalize it before comparing.
|
|
95
|
+
const currentPath = $derived(page.url.pathname.replace(/\/$/, "") || "/");
|
|
94
96
|
function isItemActive(item: any): boolean {
|
|
95
97
|
if (item.navs) return item.navs.some((c: any) => isItemActive(c));
|
|
96
98
|
if (!item.href) return false;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
const itemHref = item.href.replace(/\/$/, "") || "/";
|
|
100
|
+
if (itemHref === "/studio") return currentPath === "/studio";
|
|
101
|
+
return currentPath === itemHref || currentPath.startsWith(itemHref + "/");
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
// onMount is enough — Studio gets remounted on login/logout (see
|
|
@@ -3,29 +3,43 @@
|
|
|
3
3
|
import { goto } from "$app/navigation";
|
|
4
4
|
import { ArrowRight } from "lucide-svelte";
|
|
5
5
|
import HomeFooter from "./homeFooter.svelte";
|
|
6
|
+
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
7
|
+
import { getExtensionUtils } from "../../extensions/extensionUtils";
|
|
8
|
+
import { getStudioContext } from "../../context";
|
|
9
|
+
|
|
10
|
+
const { lobb, ctx } = getStudioContext();
|
|
6
11
|
</script>
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
<!--
|
|
14
|
+
Any extension that registers a `pages.home` component takes over /studio
|
|
15
|
+
(e.g. an app-specific overview dashboard). ExtensionsComponents falls
|
|
16
|
+
back to rendering its children when nothing matches, so the default
|
|
17
|
+
Lobb welcome below stays as the safety net for projects without an
|
|
18
|
+
overriding extension.
|
|
19
|
+
-->
|
|
20
|
+
<ExtensionsComponents name="pages.home" utils={getExtensionUtils(lobb, ctx)}>
|
|
21
|
+
<div class="flex h-full flex-col">
|
|
22
|
+
<div
|
|
23
|
+
class="flex flex-1 w-full flex-col items-center justify-center gap-4 text-muted-foreground"
|
|
24
|
+
>
|
|
25
|
+
<div class="flex flex-col items-center justify-center p-4">
|
|
26
|
+
<div class="text-3xl">Welcome to Lobb!</div>
|
|
27
|
+
<div class="text-xs text-center">
|
|
28
|
+
Your journey starts here. Explore and make the most of your
|
|
29
|
+
experience.
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="flex flex-col items-center justify-center">
|
|
33
|
+
<Button
|
|
34
|
+
Icon={ArrowRight}
|
|
35
|
+
variant="outline"
|
|
36
|
+
class="h-7 px-3 text-xs font-normal"
|
|
37
|
+
onclick={() => goto("/studio/collections")}
|
|
38
|
+
>
|
|
39
|
+
Go to collections
|
|
40
|
+
</Button>
|
|
17
41
|
</div>
|
|
18
42
|
</div>
|
|
19
|
-
<
|
|
20
|
-
<Button
|
|
21
|
-
Icon={ArrowRight}
|
|
22
|
-
variant="outline"
|
|
23
|
-
class="h-7 px-3 text-xs font-normal"
|
|
24
|
-
onclick={() => goto("/studio/collections")}
|
|
25
|
-
>
|
|
26
|
-
Go to collections
|
|
27
|
-
</Button>
|
|
28
|
-
</div>
|
|
43
|
+
<HomeFooter />
|
|
29
44
|
</div>
|
|
30
|
-
|
|
31
|
-
</div>
|
|
45
|
+
</ExtensionsComponents>
|