@lobb-js/studio 0.28.6 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/actions.d.ts +2 -0
- package/dist/components/Studio.svelte +46 -47
- package/dist/components/StudioRoot.svelte +19 -0
- package/dist/components/StudioRoot.svelte.d.ts +6 -0
- package/dist/components/breadCrumbs.svelte +5 -4
- package/dist/components/codeEditor.svelte +1 -1
- package/dist/components/combobox.svelte +3 -3
- package/dist/components/confirmationDialog/confirmationDialog.svelte +1 -1
- package/dist/components/dataTable/dataTable.svelte +108 -101
- package/dist/components/dataTable/dataTable.svelte.d.ts +5 -20
- package/dist/components/dataTable/dataTableTabs.svelte +4 -2
- package/dist/components/dataTable/dataTableTabs.svelte.d.ts +2 -0
- package/dist/components/dataTable/filter.svelte +1 -1
- package/dist/components/dataTable/filterButton.svelte +1 -1
- package/dist/components/dataTable/header.svelte +30 -47
- package/dist/components/dataTable/header.svelte.d.ts +4 -2
- package/dist/components/dataTable/listViewChildren.svelte +4 -6
- package/dist/components/dataTable/listViewChildren.svelte.d.ts +0 -1
- package/dist/components/dataTable/sort.svelte +1 -1
- package/dist/components/dataTable/sortButton.svelte +2 -2
- package/dist/components/dataTable/table.svelte +8 -10
- package/dist/components/dataTable/table.svelte.d.ts +0 -1
- package/dist/components/dataTableDrawer/dataTableDrawer.svelte +4 -1
- package/dist/components/dataTableDrawer/dataTableDrawer.svelte.d.ts +2 -0
- package/dist/components/detailView/create/children.svelte +2 -2
- package/dist/components/detailView/create/createDetailView.svelte +81 -88
- package/dist/components/detailView/create/createDetailView.svelte.d.ts +2 -2
- package/dist/components/detailView/create/createDetailViewButton.svelte +2 -2
- package/dist/components/detailView/create/createDetailViewButton.svelte.d.ts +1 -1
- package/dist/components/detailView/create/createManyView.svelte +12 -10
- package/dist/components/detailView/detailView.svelte +81 -0
- package/dist/components/detailView/detailView.svelte.d.ts +8 -0
- package/dist/components/detailView/fieldInput.svelte +11 -11
- package/dist/components/detailView/fieldInputReplacement.svelte +8 -8
- package/dist/components/detailView/passwordInput.svelte +1 -1
- package/dist/components/detailView/update/detailViewChildren.svelte +15 -26
- package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +3 -8
- package/dist/components/detailView/update/updateDetailView.svelte +90 -69
- package/dist/components/detailView/update/updateDetailView.svelte.d.ts +2 -2
- package/dist/components/detailView/update/updateDetailViewButton.svelte +3 -2
- package/dist/components/detailView/update/updateDetailViewButton.svelte.d.ts +1 -1
- package/dist/components/detailView/utils.d.ts +17 -0
- package/dist/components/diffViewer.svelte +1 -1
- package/dist/components/extensionsComponents.svelte +3 -1
- package/dist/components/foreingKeyInput.svelte +2 -2
- package/dist/components/importButton.svelte +12 -9
- package/dist/components/landing.svelte +7 -0
- package/dist/components/landing.svelte.d.ts +6 -14
- package/dist/components/miniSidebar.svelte +90 -19
- package/dist/components/miniSidebar.svelte.d.ts +2 -17
- package/dist/components/polymorphicInput.svelte +1 -1
- package/dist/components/rangeCalendarButton.svelte +13 -13
- package/dist/components/richTextEditor.svelte +1 -1
- package/dist/components/routes/collections/collection.svelte +3 -3
- package/dist/components/routes/collections/collections.svelte +34 -12
- package/dist/components/routes/data_model/dataModel.svelte +6 -28
- package/dist/components/routes/data_model/dataModel.svelte.d.ts +17 -2
- package/dist/components/routes/extensions/extension.svelte +1 -1
- package/dist/components/routes/extensions/publicExtension.svelte +19 -0
- package/dist/components/routes/extensions/publicExtension.svelte.d.ts +13 -0
- package/dist/components/routes/home.svelte +3 -3
- package/dist/components/routes/workflows/workflows.svelte +9 -9
- package/dist/components/selectRecord.svelte +2 -21
- package/dist/components/setServerPage.svelte +1 -1
- package/dist/components/sidebar/sidebar.svelte +1 -1
- package/dist/components/sidebar/sidebarElements.svelte +4 -4
- package/dist/components/singletone.svelte +4 -6
- package/dist/components/ui/alert-dialog/alert-dialog-action.svelte +1 -1
- package/dist/components/ui/alert-dialog/alert-dialog-cancel.svelte +1 -1
- package/dist/components/ui/button/button.svelte +2 -3
- package/dist/components/ui/command/command-dialog.svelte +1 -1
- package/dist/components/ui/range-calendar/range-calendar-day.svelte +1 -1
- package/dist/components/ui/range-calendar/range-calendar-next-button.svelte +1 -1
- package/dist/components/ui/range-calendar/range-calendar-prev-button.svelte +1 -1
- package/dist/components/ui/select/select-separator.svelte +1 -1
- package/dist/components/workflowEditor.svelte +5 -5
- package/dist/eventSystem.d.ts +1 -1
- package/dist/eventSystem.js +7 -5
- package/dist/extensions/extension.types.d.ts +38 -14
- package/dist/extensions/extensionUtils.js +4 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/store.types.d.ts +2 -2
- package/dist/studioLifecycle.svelte.d.ts +2 -0
- package/dist/studioLifecycle.svelte.js +15 -0
- package/package.json +3 -4
- package/src/app.css +3 -0
- package/src/lib/actions.ts +2 -0
- package/src/lib/components/Studio.svelte +46 -47
- package/src/lib/components/StudioRoot.svelte +19 -0
- package/src/lib/components/breadCrumbs.svelte +5 -4
- package/src/lib/components/codeEditor.svelte +1 -1
- package/src/lib/components/combobox.svelte +3 -3
- package/src/lib/components/confirmationDialog/confirmationDialog.svelte +1 -1
- package/src/lib/components/dataTable/dataTable.svelte +108 -101
- package/src/lib/components/dataTable/dataTableTabs.svelte +4 -2
- package/src/lib/components/dataTable/filter.svelte +1 -1
- package/src/lib/components/dataTable/filterButton.svelte +1 -1
- package/src/lib/components/dataTable/header.svelte +30 -47
- package/src/lib/components/dataTable/listViewChildren.svelte +4 -6
- package/src/lib/components/dataTable/sort.svelte +1 -1
- package/src/lib/components/dataTable/sortButton.svelte +2 -2
- package/src/lib/components/dataTable/table.svelte +8 -10
- package/src/lib/components/dataTableDrawer/dataTableDrawer.svelte +4 -1
- package/src/lib/components/detailView/create/children.svelte +2 -2
- package/src/lib/components/detailView/create/createDetailView.svelte +81 -88
- package/src/lib/components/detailView/create/createDetailViewButton.svelte +2 -2
- package/src/lib/components/detailView/create/createManyView.svelte +12 -10
- package/src/lib/components/detailView/detailView.svelte +81 -0
- package/src/lib/components/detailView/fieldInput.svelte +11 -11
- package/src/lib/components/detailView/fieldInputReplacement.svelte +8 -8
- package/src/lib/components/detailView/passwordInput.svelte +1 -1
- package/src/lib/components/detailView/update/detailViewChildren.svelte +15 -26
- package/src/lib/components/detailView/update/updateDetailView.svelte +90 -69
- package/src/lib/components/detailView/update/updateDetailViewButton.svelte +3 -2
- package/src/lib/components/detailView/utils.ts +13 -0
- package/src/lib/components/diffViewer.svelte +1 -1
- package/src/lib/components/extensionsComponents.svelte +3 -1
- package/src/lib/components/foreingKeyInput.svelte +2 -2
- package/src/lib/components/importButton.svelte +12 -9
- package/src/lib/components/landing.svelte +7 -0
- package/src/lib/components/miniSidebar.svelte +90 -19
- package/src/lib/components/polymorphicInput.svelte +1 -1
- package/src/lib/components/rangeCalendarButton.svelte +13 -13
- package/src/lib/components/richTextEditor.svelte +1 -1
- package/src/lib/components/routes/collections/collection.svelte +3 -3
- package/src/lib/components/routes/collections/collections.svelte +34 -12
- package/src/lib/components/routes/data_model/dataModel.svelte +6 -28
- package/src/lib/components/routes/extensions/extension.svelte +1 -1
- package/src/lib/components/routes/extensions/publicExtension.svelte +19 -0
- package/src/lib/components/routes/home.svelte +3 -3
- package/src/lib/components/routes/workflows/workflows.svelte +9 -9
- package/src/lib/components/selectRecord.svelte +2 -21
- package/src/lib/components/setServerPage.svelte +1 -1
- package/src/lib/components/sidebar/sidebar.svelte +1 -1
- package/src/lib/components/sidebar/sidebarElements.svelte +4 -4
- package/src/lib/components/singletone.svelte +4 -6
- package/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte +1 -1
- package/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte +1 -1
- package/src/lib/components/ui/button/button.svelte +2 -3
- package/src/lib/components/ui/command/command-dialog.svelte +1 -1
- package/src/lib/components/ui/range-calendar/range-calendar-day.svelte +1 -1
- package/src/lib/components/ui/range-calendar/range-calendar-next-button.svelte +1 -1
- package/src/lib/components/ui/range-calendar/range-calendar-prev-button.svelte +1 -1
- package/src/lib/components/ui/select/select-separator.svelte +1 -1
- package/src/lib/components/workflowEditor.svelte +5 -5
- package/src/lib/eventSystem.ts +8 -7
- package/src/lib/extensions/extension.types.ts +39 -6
- package/src/lib/extensions/extensionUtils.ts +4 -2
- package/src/lib/index.ts +3 -1
- package/src/lib/store.types.ts +2 -2
- package/src/lib/studioLifecycle.svelte.ts +17 -0
- package/vite-plugins/index.js +2 -4
- package/vite-plugins/utils.js +15 -0
- package/vite-plugins/{workspace-optimize.js → workspace-fs-allow.js} +4 -18
- package/dist/components/routes/data_model/syncManager.svelte +0 -94
- package/dist/components/routes/data_model/syncManager.svelte.d.ts +0 -3
- package/src/lib/components/routes/data_model/syncManager.svelte +0 -94
- package/vite-plugins/contextual-lib-alias.js +0 -67
|
@@ -1,43 +1,32 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import DataTable
|
|
2
|
+
import DataTable from "../../dataTable/dataTable.svelte";
|
|
3
3
|
import { getStudioContext } from "../../../context";
|
|
4
4
|
import { Table, Link } from "lucide-svelte";
|
|
5
5
|
|
|
6
6
|
const { ctx } = getStudioContext();
|
|
7
7
|
|
|
8
|
-
type
|
|
9
|
-
link?: (string | number)[];
|
|
10
|
-
unlink?: (string | number)[];
|
|
11
|
-
delete?: (string | number)[];
|
|
12
|
-
create?: any[];
|
|
13
|
-
};
|
|
8
|
+
import type { Changes, ChildrenChanges } from "../utils";
|
|
14
9
|
|
|
15
10
|
interface LocalProp {
|
|
16
11
|
collectionName: string;
|
|
17
12
|
entry: any;
|
|
18
|
-
|
|
13
|
+
activeChanges?: Changes;
|
|
19
14
|
}
|
|
20
15
|
|
|
21
|
-
let { collectionName, entry,
|
|
22
|
-
|
|
23
|
-
function makeHandler(collection: string) {
|
|
24
|
-
return (op: RecordOperation) => {
|
|
25
|
-
if (!pendingChildren[collection]) pendingChildren[collection] = {};
|
|
26
|
-
const c = pendingChildren[collection];
|
|
27
|
-
if (op.type === "link") {
|
|
28
|
-
(c.link ??= []).push(op.record.id);
|
|
29
|
-
} else if (op.type === "unlink") {
|
|
30
|
-
(c.unlink ??= []).push(op.id);
|
|
31
|
-
} else if (op.type === "delete") {
|
|
32
|
-
(c.delete ??= []).push(op.id);
|
|
33
|
-
} else if (op.type === "create") {
|
|
34
|
-
(c.create ??= []).push(op.record);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
}
|
|
16
|
+
let { collectionName, entry, activeChanges }: LocalProp = $props();
|
|
38
17
|
|
|
39
18
|
const children = (ctx.meta.collections[collectionName]?.children ?? [])
|
|
40
19
|
.filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
|
|
20
|
+
|
|
21
|
+
// Ensure all child collection slots exist before the template renders so bind:changes works
|
|
22
|
+
$effect.pre(() => {
|
|
23
|
+
if (!activeChanges) return;
|
|
24
|
+
for (const child of children) {
|
|
25
|
+
if (!activeChanges.children[child.collection]) {
|
|
26
|
+
activeChanges.children[child.collection] = { created: [], updated: [], deleted: [], linked: [], unlinked: [] };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
41
30
|
</script>
|
|
42
31
|
|
|
43
32
|
{#if children.length}
|
|
@@ -52,7 +41,7 @@
|
|
|
52
41
|
collectionName={child.collection}
|
|
53
42
|
searchParams={{ children_of: collectionName, parent_id: entry.id }}
|
|
54
43
|
parentContext={{ collectionName, recordId: entry.id }}
|
|
55
|
-
|
|
44
|
+
bind:changes={activeChanges!.children[child.collection]}
|
|
56
45
|
showImport={false}
|
|
57
46
|
showHeader={true}
|
|
58
47
|
showFooter={true}
|
|
@@ -9,64 +9,112 @@
|
|
|
9
9
|
recordId: string;
|
|
10
10
|
values?: Record<string, any>;
|
|
11
11
|
showRelatedRecords?: boolean;
|
|
12
|
-
rollback?: boolean;
|
|
13
12
|
submitButton?: SubmitButton;
|
|
14
13
|
title?: Snippet<[string]>;
|
|
15
14
|
onSuccessfullSave?: (entry: any) => Promise<void>;
|
|
16
15
|
onCancel?: () => Promise<void>;
|
|
16
|
+
changes?: import("../utils").Changes | undefined;
|
|
17
17
|
}
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
20
|
<script lang="ts">
|
|
21
21
|
import { ArrowLeft, Pencil, X } from "lucide-svelte";
|
|
22
|
-
import Button from "
|
|
23
|
-
import { fade, fly } from "svelte/transition";
|
|
22
|
+
import Button from "../../ui/button/button.svelte";
|
|
24
23
|
import { getStudioContext } from "../../../context";
|
|
25
24
|
import { toast } from "svelte-sonner";
|
|
26
|
-
import
|
|
27
|
-
import { getExtensionUtils } from "../../../extensions/extensionUtils";
|
|
25
|
+
import { untrack } from "svelte";
|
|
28
26
|
|
|
29
27
|
const { lobb, ctx } = getStudioContext();
|
|
30
|
-
import {
|
|
31
|
-
import
|
|
32
|
-
import DetailViewChildren from "../update/detailViewChildren.svelte";
|
|
28
|
+
import { getChangedProperties } from "../../../utils";
|
|
29
|
+
import DetailViewChildren from "./detailViewChildren.svelte";
|
|
33
30
|
import type { Snippet } from "svelte";
|
|
34
31
|
import { getDefaultEntry } from "../utils";
|
|
35
|
-
import
|
|
36
|
-
|
|
32
|
+
import type { Changes, ChildrenChanges } from "../utils";
|
|
33
|
+
import DetailView from "../detailView.svelte";
|
|
34
|
+
import Drawer from "../../drawer.svelte";
|
|
37
35
|
|
|
38
36
|
let {
|
|
39
37
|
collectionName,
|
|
40
|
-
values = {},
|
|
38
|
+
values: passedValues = {} as Record<string, any>,
|
|
41
39
|
showRelatedRecords = true,
|
|
42
40
|
onCancel,
|
|
43
41
|
onSuccessfullSave,
|
|
44
42
|
title,
|
|
45
43
|
submitButton,
|
|
46
44
|
recordId,
|
|
45
|
+
changes: passedChanges = $bindable<Changes | undefined>(undefined),
|
|
47
46
|
}: UpdateDetailViewProp = $props();
|
|
48
47
|
|
|
48
|
+
// Recording mode = changes was passed from a parent component
|
|
49
|
+
const isRecordingMode = passedChanges !== undefined;
|
|
50
|
+
if (!isRecordingMode) passedChanges = { data: {}, children: {} };
|
|
51
|
+
const changes = passedChanges as Changes;
|
|
52
|
+
|
|
49
53
|
const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
);
|
|
53
|
-
const initialEntry = $state.snapshot(entry);
|
|
54
|
-
let localEntry = $derived(
|
|
55
|
-
getChangedProperties(initialEntry, $state.snapshot(entry)),
|
|
56
|
-
);
|
|
54
|
+
let values = $state(getDefaultEntry(ctx, fieldNames, collectionName, passedValues));
|
|
55
|
+
const initialValues = $state.snapshot(values);
|
|
57
56
|
let fieldsErrors: Record<string, any> = $state({});
|
|
58
|
-
|
|
57
|
+
|
|
58
|
+
const hasChanges = $derived(
|
|
59
|
+
Object.keys(changes.data).length > 0 ||
|
|
60
|
+
Object.values(changes.children).some(
|
|
61
|
+
(ch: ChildrenChanges) => ch.created.length || ch.updated.length || ch.deleted.length || ch.linked.length || ch.unlinked.length,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
|
|
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
|
+
$effect(() => {
|
|
68
|
+
const currentEntrySnap = $state.snapshot(values);
|
|
69
|
+
|
|
70
|
+
untrack(() => {
|
|
71
|
+
changes.data = getChangedProperties(initialValues, currentEntrySnap);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Separate logging effect — needs its own $effect so it tracks mutations to changes.children.
|
|
76
|
+
$effect(() => {
|
|
77
|
+
if (!isRecordingMode) {
|
|
78
|
+
console.log(`[${collectionName}] changes:`, $state.snapshot(changes));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function buildApiChildren(children: Record<string, ChildrenChanges>): Record<string, any> | undefined {
|
|
83
|
+
const result: Record<string, any> = {};
|
|
84
|
+
for (const [collection, ops] of Object.entries(children)) {
|
|
85
|
+
const hasOps = ops.created.length || ops.deleted.length || ops.linked.length || ops.unlinked.length;
|
|
86
|
+
if (!hasOps) continue;
|
|
87
|
+
result[collection] = {
|
|
88
|
+
...(ops.created.length ? { create: ops.created.map((op) => op.data) } : {}),
|
|
89
|
+
...(ops.deleted.length ? { delete: ops.deleted.map((r) => r.id) } : {}),
|
|
90
|
+
...(ops.linked.length ? { link: ops.linked.map((r) => r.id) } : {}),
|
|
91
|
+
...(ops.unlinked.length ? { unlink: ops.unlinked.map((r) => r.id) } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return Object.keys(result).length ? result : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleCancel() {
|
|
98
|
+
if (isRecordingMode) {
|
|
99
|
+
changes.data = {};
|
|
100
|
+
changes.children = {};
|
|
101
|
+
}
|
|
102
|
+
onCancel?.();
|
|
103
|
+
}
|
|
59
104
|
|
|
60
105
|
async function handleSave() {
|
|
61
|
-
|
|
106
|
+
const snap = $state.snapshot(changes);
|
|
107
|
+
const { id: _id, ...data } = snap.data;
|
|
108
|
+
const children = buildApiChildren(snap.children);
|
|
62
109
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
110
|
+
const response = await lobb.updateOne(collectionName, recordId, data, children, isRecordingMode);
|
|
111
|
+
|
|
112
|
+
if (response.status === 204) {
|
|
113
|
+
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
114
|
+
toast.success(`The record was successfully updated`);
|
|
115
|
+
onCancel?.();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
70
118
|
|
|
71
119
|
if (!response.bodyUsed) {
|
|
72
120
|
const result = await response.json();
|
|
@@ -75,27 +123,32 @@
|
|
|
75
123
|
fieldsErrors = result.details;
|
|
76
124
|
return;
|
|
77
125
|
} else if (result.message) {
|
|
126
|
+
toast.error(result.message);
|
|
78
127
|
return;
|
|
79
128
|
}
|
|
80
129
|
}
|
|
81
130
|
}
|
|
82
131
|
|
|
83
|
-
//
|
|
84
|
-
if (
|
|
85
|
-
|
|
132
|
+
// Real mode: also fire separate update requests for edited children
|
|
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
|
+
}
|
|
86
139
|
}
|
|
87
140
|
|
|
141
|
+
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
88
142
|
toast.success(`The record was successfully updated`);
|
|
89
|
-
|
|
90
143
|
onCancel?.();
|
|
91
144
|
}
|
|
92
145
|
</script>
|
|
93
146
|
|
|
94
|
-
<Drawer onHide={
|
|
147
|
+
<Drawer onHide={handleCancel}>
|
|
95
148
|
<div class="flex h-12 items-center gap-4 border-b px-4">
|
|
96
149
|
<Button
|
|
97
150
|
variant="outline"
|
|
98
|
-
onclick={
|
|
151
|
+
onclick={handleCancel}
|
|
99
152
|
class=" h-8 w-8 rounded-full text-xs font-normal"
|
|
100
153
|
Icon={ArrowLeft}
|
|
101
154
|
></Button>
|
|
@@ -111,48 +164,16 @@
|
|
|
111
164
|
</div>
|
|
112
165
|
</div>
|
|
113
166
|
<div class="flex-1 overflow-y-auto">
|
|
114
|
-
<
|
|
115
|
-
{#each fieldNames as fieldName}
|
|
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
|
-
/>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
<FieldInput
|
|
137
|
-
{collectionName}
|
|
138
|
-
{fieldName}
|
|
139
|
-
bind:value={entry[fieldName]}
|
|
140
|
-
bind:entry
|
|
141
|
-
errorMessages={fieldsErrors[fieldName]}
|
|
142
|
-
/>
|
|
143
|
-
</div>
|
|
144
|
-
{/if}
|
|
145
|
-
{/each}
|
|
146
|
-
</div>
|
|
167
|
+
<DetailView {collectionName} bind:entry={values} {fieldsErrors} />
|
|
147
168
|
{#if showRelatedRecords}
|
|
148
|
-
<DetailViewChildren {collectionName} {
|
|
169
|
+
<DetailViewChildren {collectionName} entry={values} activeChanges={changes} />
|
|
149
170
|
{/if}
|
|
150
171
|
</div>
|
|
151
172
|
<div class="flex h-12 items-center justify-end gap-2 border-t px-4">
|
|
152
173
|
<div class="flex gap-3">
|
|
153
174
|
<Button
|
|
154
175
|
variant="outline"
|
|
155
|
-
onclick={
|
|
176
|
+
onclick={handleCancel}
|
|
156
177
|
class="h-7 px-3 text-xs font-normal"
|
|
157
178
|
Icon={X}
|
|
158
179
|
>
|
|
@@ -163,7 +184,7 @@
|
|
|
163
184
|
class="h-7 px-3 text-xs font-normal"
|
|
164
185
|
Icon={submitButton?.icon ? submitButton.icon : Pencil}
|
|
165
186
|
onclick={handleSave}
|
|
166
|
-
disabled={!
|
|
187
|
+
disabled={!hasChanges}
|
|
167
188
|
>
|
|
168
189
|
{submitButton?.text ? submitButton.text : "Update"}
|
|
169
190
|
</Button>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { UpdateDetailViewProp } from "./updateDetailView.svelte";
|
|
3
|
-
import type { ButtonProps } from "
|
|
4
|
-
import Button from "
|
|
3
|
+
import type { ButtonProps } from "../../ui/button/button.svelte";
|
|
4
|
+
import Button from "../../ui/button/button.svelte";
|
|
5
5
|
import UpdateDetailView from "./updateDetailView.svelte";
|
|
6
6
|
import { getStudioContext } from "../../../context";
|
|
7
7
|
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
<UpdateDetailView
|
|
47
47
|
{...props}
|
|
48
48
|
{values}
|
|
49
|
+
changes={props.changes}
|
|
49
50
|
onCancel={async () => {
|
|
50
51
|
open = false;
|
|
51
52
|
values = undefined;
|
|
@@ -3,6 +3,19 @@ import type { CTX } from "../../store.types";
|
|
|
3
3
|
import { getField } from "../dataTable/utils";
|
|
4
4
|
import type { DetailFormField } from "./detailViewForm.svelte";
|
|
5
5
|
|
|
6
|
+
export type ChildrenChanges = {
|
|
7
|
+
created: Array<{ data: Record<string, any> }>;
|
|
8
|
+
updated: Array<{ id: string | number; data: Record<string, any>; children: Record<string, ChildrenChanges> }>;
|
|
9
|
+
deleted: Array<Record<string, any>>;
|
|
10
|
+
linked: Array<Record<string, any>>;
|
|
11
|
+
unlinked: Array<Record<string, any>>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type Changes = {
|
|
15
|
+
data: Record<string, any>;
|
|
16
|
+
children: Record<string, ChildrenChanges>;
|
|
17
|
+
};
|
|
18
|
+
|
|
6
19
|
export function getDefaultEntry(ctx: CTX, fieldNames: string[], collectionName: string, values?: Record<string, any>) {
|
|
7
20
|
return Object.fromEntries(
|
|
8
21
|
fieldNames.map((fieldName) => {
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
});
|
|
97
97
|
</script>
|
|
98
98
|
|
|
99
|
-
<div class={cn("w-full resize-y rounded-md border bg-muted
|
|
99
|
+
<div class={cn("w-full resize-y rounded-md border bg-muted-soft shadow-sm", className)}>
|
|
100
100
|
<div
|
|
101
101
|
bind:this={editorContainer}
|
|
102
102
|
class="editor pl-2"
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
placeholder={"NULL"}
|
|
86
86
|
type="number"
|
|
87
87
|
class="
|
|
88
|
-
bg-muted
|
|
88
|
+
bg-muted-soft text-xs
|
|
89
89
|
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
90
90
|
"
|
|
91
91
|
bind:value={
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
<div class="relative z-10">
|
|
99
99
|
<Input
|
|
100
100
|
placeholder={"PARENT ID"}
|
|
101
|
-
class="bg-muted
|
|
101
|
+
class="bg-muted-soft text-xs"
|
|
102
102
|
disabled={true}
|
|
103
103
|
/>
|
|
104
104
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { AlertCircle, Check, FileText, LoaderCircle, Upload, X } from "lucide-svelte";
|
|
2
|
+
import { AlertCircle, Check, Download, FileText, LoaderCircle, Upload, X } from "lucide-svelte";
|
|
3
3
|
import Button, { type ButtonProps } from "./ui/button/button.svelte";
|
|
4
4
|
import { toast } from "svelte-sonner";
|
|
5
5
|
import { getStudioContext } from "../context";
|
|
@@ -170,10 +170,15 @@
|
|
|
170
170
|
}
|
|
171
171
|
</script>
|
|
172
172
|
|
|
173
|
-
<Button variant={rest.variant} class={rest.class}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
<Button variant={rest.variant} class={rest.class} onclick={showDrawer}>
|
|
174
|
+
<ExtensionsComponents
|
|
175
|
+
name="collections.import.button.content"
|
|
176
|
+
utils={getExtensionUtils(lobb, ctx)}
|
|
177
|
+
{collectionName}
|
|
178
|
+
>
|
|
179
|
+
<rest.Icon />
|
|
180
|
+
Import
|
|
181
|
+
</ExtensionsComponents>
|
|
177
182
|
</Button>
|
|
178
183
|
|
|
179
184
|
<Dialog.Root
|
|
@@ -231,7 +236,7 @@
|
|
|
231
236
|
tabindex="0"
|
|
232
237
|
onkeydown={(e) => e.key === "Enter" && fileInput.click()}
|
|
233
238
|
>
|
|
234
|
-
<
|
|
239
|
+
<Download class="mb-3 h-8 w-8 text-muted-foreground" />
|
|
235
240
|
<p class="text-sm font-medium">Drop a file here or click to browse</p>
|
|
236
241
|
<p class="mt-1 text-xs text-muted-foreground">Supports .csv and .json</p>
|
|
237
242
|
</div>
|
|
@@ -244,7 +249,7 @@
|
|
|
244
249
|
/>
|
|
245
250
|
{:else}
|
|
246
251
|
<textarea
|
|
247
|
-
class="block h-56 w-full resize-none rounded-md border bg-muted
|
|
252
|
+
class="block h-56 w-full resize-none rounded-md border bg-muted-soft p-3 font-mono text-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
|
248
253
|
placeholder="Paste CSV or JSON here..."
|
|
249
254
|
bind:value={pasteContent}
|
|
250
255
|
></textarea>
|
|
@@ -279,7 +284,6 @@
|
|
|
279
284
|
data={transformedRows}
|
|
280
285
|
columns={collectionColumns.filter((c) => c.id !== "id")}
|
|
281
286
|
showCheckboxes={false}
|
|
282
|
-
unifiedBgColor="bg-background"
|
|
283
287
|
headerBorderTop={true}
|
|
284
288
|
/>
|
|
285
289
|
</div>
|
|
@@ -328,7 +332,6 @@
|
|
|
328
332
|
data={failedData}
|
|
329
333
|
columns={[{ id: "__error", icon: AlertCircle }, ...collectionColumns.filter((c) => c.id !== "id")]}
|
|
330
334
|
showCheckboxes={false}
|
|
331
|
-
unifiedBgColor="bg-background"
|
|
332
335
|
headerBorderTop={true}
|
|
333
336
|
>
|
|
334
337
|
{#snippet overrideCell(value, column)}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
onMount(() => {
|
|
4
|
+
document.getElementById("app-loading")?.remove();
|
|
5
|
+
});
|
|
6
|
+
</script>
|
|
7
|
+
|
|
1
8
|
<div class="min-h-screen flex items-center justify-center bg-background text-foreground p-6">
|
|
2
9
|
<div class="max-w-md text-center space-y-4">
|
|
3
10
|
<h1 class="text-2xl font-semibold">Lobb Studio</h1>
|
|
@@ -12,20 +12,23 @@
|
|
|
12
12
|
|
|
13
13
|
<script lang="ts">
|
|
14
14
|
import { House, Layers, Library, Workflow, X } from "lucide-svelte";
|
|
15
|
-
import Button from "
|
|
16
|
-
import Separator from "
|
|
17
|
-
import * as Tooltip from "
|
|
18
|
-
import * as Accordion from "
|
|
15
|
+
import Button from "./ui/button/button.svelte";
|
|
16
|
+
import Separator from "./ui/separator/separator.svelte";
|
|
17
|
+
import * as Tooltip from "./ui/tooltip";
|
|
18
|
+
import * as Accordion from "./ui/accordion/index.js";
|
|
19
|
+
import { onMount } from "svelte";
|
|
19
20
|
|
|
20
21
|
import { getStudioContext } from "../context";
|
|
21
22
|
import { getDashboardNavs } from "../extensions/extensionUtils";
|
|
23
|
+
import { emitEvent } from "../eventSystem";
|
|
22
24
|
|
|
23
|
-
const { ctx } = getStudioContext();
|
|
25
|
+
const { lobb, ctx } = getStudioContext();
|
|
24
26
|
import { mediaQueries } from "../utils";
|
|
25
27
|
import * as Popover from "./ui/popover";
|
|
26
|
-
import {
|
|
28
|
+
import { page } from "$app/state";
|
|
29
|
+
import { goto } from "$app/navigation";
|
|
27
30
|
|
|
28
|
-
const
|
|
31
|
+
const rawSections: any[][] = [
|
|
29
32
|
[
|
|
30
33
|
{
|
|
31
34
|
label: "Home",
|
|
@@ -39,13 +42,15 @@
|
|
|
39
42
|
},
|
|
40
43
|
{
|
|
41
44
|
label: "Data Model",
|
|
42
|
-
href: "/studio/datamodel
|
|
45
|
+
href: "/studio/datamodel",
|
|
43
46
|
icon: Layers,
|
|
47
|
+
represents: "core_data_model",
|
|
44
48
|
},
|
|
45
49
|
{
|
|
46
50
|
label: "Workflows",
|
|
47
51
|
href: "/studio/workflows",
|
|
48
52
|
icon: Workflow,
|
|
53
|
+
represents: "core_workflows",
|
|
49
54
|
},
|
|
50
55
|
],
|
|
51
56
|
[],
|
|
@@ -55,20 +60,72 @@
|
|
|
55
60
|
const navs = getDashboardNavs(ctx);
|
|
56
61
|
|
|
57
62
|
if (navs.top) {
|
|
58
|
-
|
|
63
|
+
rawSections[0] = [...rawSections[0], ...navs.top];
|
|
59
64
|
}
|
|
60
65
|
if (navs.middle) {
|
|
61
|
-
|
|
66
|
+
rawSections[1] = [...rawSections[1], ...navs.middle];
|
|
62
67
|
}
|
|
63
68
|
if (navs.bottom) {
|
|
64
|
-
|
|
69
|
+
rawSections[2] = [...rawSections[2], ...navs.bottom];
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
// Items without a `represents` are always visible. Items with one are
|
|
73
|
+
// gated by emitting auth.canAccess — the auth extension (or any
|
|
74
|
+
// drop-in replacement) decides based on the current user's session.
|
|
75
|
+
// Start empty so nothing flashes before the answers come back.
|
|
76
|
+
let sections: any[][] = $state([[], [], []]);
|
|
77
|
+
|
|
78
|
+
async function isItemVisible(item: any): Promise<boolean> {
|
|
79
|
+
if (!item.represents) return true;
|
|
80
|
+
const res = await emitEvent(
|
|
81
|
+
{ lobb, ctx },
|
|
82
|
+
"auth.canAccess",
|
|
83
|
+
{ collection: item.represents, action: "read" },
|
|
84
|
+
);
|
|
85
|
+
return res === true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Highlight the nav item matching the current URL. "/studio" requires an
|
|
89
|
+
// exact match (otherwise it would light up on every sub-route since it's a
|
|
90
|
+
// prefix of everything); other items use startsWith so sub-paths
|
|
91
|
+
// (e.g. /studio/collections/risks) still highlight their parent.
|
|
92
|
+
// Popover items with children are active when any of their children match.
|
|
93
|
+
const currentPath = $derived(page.url.pathname);
|
|
94
|
+
function isItemActive(item: any): boolean {
|
|
95
|
+
if (item.navs) return item.navs.some((c: any) => isItemActive(c));
|
|
96
|
+
if (!item.href) return false;
|
|
97
|
+
if (item.href === "/studio") return currentPath === "/studio";
|
|
98
|
+
return currentPath === item.href || currentPath.startsWith(item.href + "/");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// onMount is enough — Studio gets remounted on login/logout (see
|
|
102
|
+
// remountStudio in @lobb-js/studio), so by the time this component
|
|
103
|
+
// mounts, ctx.extensions.auth.user / permissions are already populated.
|
|
104
|
+
onMount(async () => {
|
|
105
|
+
const result: any[][] = [[], [], []];
|
|
106
|
+
for (let i = 0; i < rawSections.length; i++) {
|
|
107
|
+
for (const item of rawSections[i]) {
|
|
108
|
+
if (item.navs) {
|
|
109
|
+
const visibleChildren: any[] = [];
|
|
110
|
+
for (const child of item.navs) {
|
|
111
|
+
if (await isItemVisible(child)) visibleChildren.push(child);
|
|
112
|
+
}
|
|
113
|
+
if (visibleChildren.length && (await isItemVisible(item))) {
|
|
114
|
+
result[i].push({ ...item, navs: visibleChildren });
|
|
115
|
+
}
|
|
116
|
+
} else if (await isItemVisible(item)) {
|
|
117
|
+
result[i].push(item);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
sections = result;
|
|
122
|
+
});
|
|
67
123
|
</script>
|
|
68
124
|
|
|
69
125
|
{#snippet section(section: any)}
|
|
70
126
|
<div class="flex flex-col {isSmallScreen ? 'gap-0' : 'gap-2'}">
|
|
71
127
|
{#each section as item}
|
|
128
|
+
{@const active = isItemActive(item)}
|
|
72
129
|
{#if isSmallScreen}
|
|
73
130
|
{#if !item.navs}
|
|
74
131
|
<Button
|
|
@@ -76,11 +133,13 @@
|
|
|
76
133
|
if (item.onclick) {
|
|
77
134
|
item.onclick();
|
|
78
135
|
} else {
|
|
79
|
-
|
|
136
|
+
goto(item.href);
|
|
80
137
|
}
|
|
81
138
|
isCollapsed = true;
|
|
82
139
|
}}
|
|
83
|
-
class="flex items-center justify-start flex-nowrap text-
|
|
140
|
+
class="flex items-center justify-start flex-nowrap text-nowrap h-10 w-full {active
|
|
141
|
+
? 'bg-accent text-accent-foreground'
|
|
142
|
+
: 'text-muted-foreground'}"
|
|
84
143
|
variant="ghost"
|
|
85
144
|
size="icon"
|
|
86
145
|
Icon={item.icon}
|
|
@@ -92,7 +151,9 @@
|
|
|
92
151
|
<Accordion.Item class="border-b-0">
|
|
93
152
|
<Accordion.Trigger class="justify-between p-0 h-10">
|
|
94
153
|
<div
|
|
95
|
-
class="flex items-center gap-2
|
|
154
|
+
class="flex items-center gap-2 {active
|
|
155
|
+
? 'text-accent-foreground'
|
|
156
|
+
: 'text-muted-foreground'}"
|
|
96
157
|
>
|
|
97
158
|
<item.icon size="18" />
|
|
98
159
|
<div class="text-nowrap">{item.label}</div>
|
|
@@ -100,16 +161,19 @@
|
|
|
100
161
|
</Accordion.Trigger>
|
|
101
162
|
<Accordion.Content class="pl-2 border-l">
|
|
102
163
|
{#each item.navs as childItem}
|
|
164
|
+
{@const childActive = isItemActive(childItem)}
|
|
103
165
|
<Button
|
|
104
166
|
onclick={() => {
|
|
105
167
|
if (childItem.onclick) {
|
|
106
168
|
childItem.onclick();
|
|
107
169
|
} else {
|
|
108
|
-
|
|
170
|
+
goto(item.href);
|
|
109
171
|
}
|
|
110
172
|
isCollapsed = true;
|
|
111
173
|
}}
|
|
112
|
-
class="flex items-center justify-start flex-nowrap text-
|
|
174
|
+
class="flex items-center justify-start flex-nowrap text-nowrap h-8 w-full {childActive
|
|
175
|
+
? 'bg-accent text-accent-foreground'
|
|
176
|
+
: 'text-muted-foreground'}"
|
|
113
177
|
variant="ghost"
|
|
114
178
|
size="icon"
|
|
115
179
|
Icon={childItem.icon}
|
|
@@ -133,7 +197,9 @@
|
|
|
133
197
|
isCollapsed = true;
|
|
134
198
|
}}
|
|
135
199
|
href={item.href}
|
|
136
|
-
class=
|
|
200
|
+
class={active
|
|
201
|
+
? 'bg-accent text-accent-foreground'
|
|
202
|
+
: 'text-muted-foreground'}
|
|
137
203
|
variant="ghost"
|
|
138
204
|
size="icon"
|
|
139
205
|
Icon={item.icon}
|
|
@@ -142,7 +208,9 @@
|
|
|
142
208
|
<Popover.Root>
|
|
143
209
|
<Popover.Trigger>
|
|
144
210
|
<Button
|
|
145
|
-
class=
|
|
211
|
+
class={active
|
|
212
|
+
? 'bg-accent text-accent-foreground'
|
|
213
|
+
: 'text-muted-foreground'}
|
|
146
214
|
variant="ghost"
|
|
147
215
|
size="icon"
|
|
148
216
|
Icon={item.icon}
|
|
@@ -155,12 +223,15 @@
|
|
|
155
223
|
>
|
|
156
224
|
<div class="py-1">
|
|
157
225
|
{#each item.navs as childItem}
|
|
226
|
+
{@const childActive = isItemActive(childItem)}
|
|
158
227
|
<div
|
|
159
228
|
class="px-1 text-xs text-muted-foreground"
|
|
160
229
|
>
|
|
161
230
|
<Button
|
|
162
231
|
variant="ghost"
|
|
163
|
-
class="flex h-7 w-full justify-start p-2 text-xs font-normal
|
|
232
|
+
class="flex h-7 w-full justify-start p-2 text-xs font-normal {childActive
|
|
233
|
+
? 'bg-accent text-accent-foreground'
|
|
234
|
+
: 'text-muted-foreground'}"
|
|
164
235
|
Icon={childItem.icon}
|
|
165
236
|
onclick={() => {
|
|
166
237
|
if (childItem.onclick) {
|