@lobb-js/studio 0.28.5 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/components/Studio.svelte +7 -4
  2. package/dist/components/combobox.svelte +3 -3
  3. package/dist/components/confirmationDialog/confirmationDialog.svelte +1 -1
  4. package/dist/components/dataTable/dataTable.svelte +74 -82
  5. package/dist/components/dataTable/dataTable.svelte.d.ts +3 -19
  6. package/dist/components/dataTable/filter.svelte +1 -1
  7. package/dist/components/dataTable/filterButton.svelte +1 -1
  8. package/dist/components/dataTable/header.svelte +33 -54
  9. package/dist/components/dataTable/header.svelte.d.ts +3 -2
  10. package/dist/components/dataTable/sort.svelte +1 -1
  11. package/dist/components/dataTable/sortButton.svelte +2 -2
  12. package/dist/components/detailView/create/children.svelte +1 -1
  13. package/dist/components/detailView/create/createDetailView.svelte +77 -42
  14. package/dist/components/detailView/create/createDetailView.svelte.d.ts +2 -2
  15. package/dist/components/detailView/create/createDetailViewButton.svelte +2 -2
  16. package/dist/components/detailView/create/createDetailViewButton.svelte.d.ts +1 -1
  17. package/dist/components/detailView/create/createManyView.svelte +10 -6
  18. package/dist/components/detailView/fieldInput.svelte +1 -1
  19. package/dist/components/detailView/fieldInputReplacement.svelte +1 -1
  20. package/dist/components/detailView/update/detailViewChildren.svelte +15 -26
  21. package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +3 -8
  22. package/dist/components/detailView/update/updateDetailView.svelte +85 -27
  23. package/dist/components/detailView/update/updateDetailView.svelte.d.ts +2 -2
  24. package/dist/components/detailView/update/updateDetailViewButton.svelte +3 -2
  25. package/dist/components/detailView/update/updateDetailViewButton.svelte.d.ts +1 -1
  26. package/dist/components/detailView/utils.d.ts +17 -0
  27. package/dist/components/miniSidebar.svelte +4 -4
  28. package/dist/components/rangeCalendarButton.svelte +3 -3
  29. package/dist/components/routes/collections/collection.svelte +3 -3
  30. package/dist/components/routes/collections/collections.svelte +2 -2
  31. package/dist/components/routes/data_model/dataModel.svelte +2 -2
  32. package/dist/components/routes/data_model/syncManager.svelte +4 -4
  33. package/dist/components/routes/extensions/extension.svelte +1 -1
  34. package/dist/components/routes/home.svelte +1 -1
  35. package/dist/components/routes/workflows/workflows.svelte +5 -5
  36. package/dist/components/selectRecord.svelte +2 -21
  37. package/dist/components/setServerPage.svelte +1 -1
  38. package/dist/components/ui/alert-dialog/alert-dialog-action.svelte +1 -1
  39. package/dist/components/ui/alert-dialog/alert-dialog-cancel.svelte +1 -1
  40. package/dist/components/ui/command/command-dialog.svelte +1 -1
  41. package/dist/components/ui/range-calendar/range-calendar-day.svelte +1 -1
  42. package/dist/components/ui/range-calendar/range-calendar-next-button.svelte +1 -1
  43. package/dist/components/ui/range-calendar/range-calendar-prev-button.svelte +1 -1
  44. package/dist/components/ui/select/select-separator.svelte +1 -1
  45. package/dist/components/workflowEditor.svelte +3 -3
  46. package/dist/store.types.d.ts +1 -1
  47. package/package.json +2 -2
  48. package/src/lib/components/Studio.svelte +7 -4
  49. package/src/lib/components/combobox.svelte +3 -3
  50. package/src/lib/components/confirmationDialog/confirmationDialog.svelte +1 -1
  51. package/src/lib/components/dataTable/dataTable.svelte +74 -82
  52. package/src/lib/components/dataTable/filter.svelte +1 -1
  53. package/src/lib/components/dataTable/filterButton.svelte +1 -1
  54. package/src/lib/components/dataTable/header.svelte +33 -54
  55. package/src/lib/components/dataTable/sort.svelte +1 -1
  56. package/src/lib/components/dataTable/sortButton.svelte +2 -2
  57. package/src/lib/components/detailView/create/children.svelte +1 -1
  58. package/src/lib/components/detailView/create/createDetailView.svelte +77 -42
  59. package/src/lib/components/detailView/create/createDetailViewButton.svelte +2 -2
  60. package/src/lib/components/detailView/create/createManyView.svelte +10 -6
  61. package/src/lib/components/detailView/fieldInput.svelte +1 -1
  62. package/src/lib/components/detailView/fieldInputReplacement.svelte +1 -1
  63. package/src/lib/components/detailView/update/detailViewChildren.svelte +15 -26
  64. package/src/lib/components/detailView/update/updateDetailView.svelte +85 -27
  65. package/src/lib/components/detailView/update/updateDetailViewButton.svelte +3 -2
  66. package/src/lib/components/detailView/utils.ts +13 -0
  67. package/src/lib/components/miniSidebar.svelte +4 -4
  68. package/src/lib/components/rangeCalendarButton.svelte +3 -3
  69. package/src/lib/components/routes/collections/collection.svelte +3 -3
  70. package/src/lib/components/routes/collections/collections.svelte +2 -2
  71. package/src/lib/components/routes/data_model/dataModel.svelte +2 -2
  72. package/src/lib/components/routes/data_model/syncManager.svelte +4 -4
  73. package/src/lib/components/routes/extensions/extension.svelte +1 -1
  74. package/src/lib/components/routes/home.svelte +1 -1
  75. package/src/lib/components/routes/workflows/workflows.svelte +5 -5
  76. package/src/lib/components/selectRecord.svelte +2 -21
  77. package/src/lib/components/setServerPage.svelte +1 -1
  78. package/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte +1 -1
  79. package/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte +1 -1
  80. package/src/lib/components/ui/command/command-dialog.svelte +1 -1
  81. package/src/lib/components/ui/range-calendar/range-calendar-day.svelte +1 -1
  82. package/src/lib/components/ui/range-calendar/range-calendar-next-button.svelte +1 -1
  83. package/src/lib/components/ui/range-calendar/range-calendar-prev-button.svelte +1 -1
  84. package/src/lib/components/ui/select/select-separator.svelte +1 -1
  85. package/src/lib/components/workflowEditor.svelte +3 -3
  86. package/src/lib/store.types.ts +1 -1
  87. package/vite-plugins/index.js +2 -4
  88. package/vite-plugins/lobb-extensions.js +12 -0
  89. package/vite-plugins/utils.js +15 -0
  90. package/vite-plugins/workspace-fs-allow.js +33 -0
  91. package/vite-plugins/contextual-lib-alias.js +0 -67
  92. package/vite-plugins/workspace-optimize.js +0 -106
@@ -8,93 +8,128 @@
8
8
  collectionName: string;
9
9
  values?: Record<string, any>;
10
10
  showRelatedRecords?: boolean;
11
- rollback?: boolean;
12
11
  submitButton?: SubmitButton;
13
12
  title?: Snippet<[string]>;
14
13
  onSuccessfullSave?: (entry: any) => Promise<void>;
15
14
  onCancel?: () => Promise<void>;
15
+ changes?: import("../utils").Changes | undefined;
16
16
  }
17
17
  </script>
18
18
 
19
19
  <script lang="ts">
20
20
  import { ArrowLeft, Plus, X } from "lucide-svelte";
21
- import Button from "../../../components/ui/button/button.svelte";
21
+ import Button from "../../ui/button/button.svelte";
22
22
  import { getStudioContext } from "../../../context";
23
23
  import { toast } from "svelte-sonner";
24
+ import { untrack } from "svelte";
24
25
 
25
26
  const { lobb, ctx } = getStudioContext();
26
27
  import ExtensionsComponents from "../../extensionsComponents.svelte";
27
28
  import { getExtensionUtils } from "../../../extensions/extensionUtils";
28
29
  import { getField, getFieldIcon } from "../../dataTable/utils";
29
30
  import Children from "./children.svelte";
30
- import {
31
- buildChildren,
32
- getDefaultEntry,
33
- } from "../utils";
31
+ import { buildChildren, getDefaultEntry } from "../utils";
32
+ import type { Changes } from "../utils";
34
33
  import { getChangedProperties } from "../../../utils";
35
34
  import type { Snippet } from "svelte";
36
35
  import FieldInput from "../fieldInput.svelte";
37
- import { emitEvent } from "../../../eventSystem";
38
- import Drawer from "../../../components/drawer.svelte";
36
+ import Drawer from "../../drawer.svelte";
39
37
 
40
38
  let {
41
39
  collectionName,
42
40
  values = {},
43
41
  showRelatedRecords = true,
44
- rollback = false,
45
42
  onCancel,
46
43
  onSuccessfullSave,
47
44
  title,
48
45
  submitButton,
46
+ changes = $bindable<Changes | undefined>(undefined),
49
47
  }: CreateDetailViewProp = $props();
50
48
 
49
+ let _changes = $state<Changes>({ data: {}, children: {} });
50
+ const isRecordingMode = $derived(changes !== undefined);
51
+
51
52
  const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
52
53
  let entry: Record<string, any> = $state(
53
54
  getDefaultEntry(ctx, fieldNames, collectionName, values),
54
55
  );
55
56
  const initialEntry = $state.snapshot(entry);
56
57
  let fieldsErrors: Record<string, any> = $state({});
57
- const subCollections = ctx.meta.relations.filter((relation) => relation.to.collection === collectionName).map((relation) => relation.from.collection);
58
+
59
+ const childCollections = ctx.meta.relations
60
+ .filter((r) => r.to.collection === collectionName)
61
+ .map((r) => (r as any).from.collection);
62
+
63
+ const subCollections = childCollections;
58
64
  const subCollectionsValues: Record<string, any> = {};
59
- for (let index = 0; index < subCollections.length; index++) {
60
- const subCollection = subCollections[index];
61
- if (values[subCollection]) {
62
- subCollectionsValues[subCollection] = values[subCollection];
63
- }
65
+ for (const col of subCollections) {
66
+ if (values[col]) subCollectionsValues[col] = values[col];
64
67
  }
65
68
 
66
- async function handleSave() {
67
- let localEntry = getChangedProperties(initialEntry, $state.snapshot(entry));
69
+ $effect(() => {
70
+ const snap = $state.snapshot(entry);
68
71
 
69
- await emitEvent({ lobb, ctx }, "studio.collections.preCreate", {
70
- collectionName,
71
- entry: localEntry,
72
- });
72
+ untrack(() => {
73
+ const target = changes ?? _changes;
74
+ const data: Record<string, any> = {};
75
+ const children: Record<string, any> = {};
73
76
 
74
- // remove empty children records data
75
- for (const [key, value] of Object.entries(localEntry)) {
76
- if (Array.isArray(value) && !value.length) {
77
- delete localEntry[key];
77
+ for (const [key, value] of Object.entries(snap)) {
78
+ if (childCollections.includes(key) && Array.isArray(value)) {
79
+ children[key] = {
80
+ created: (value as any[]).filter((r) => !r.id).map((r) => ({ data: r })),
81
+ updated: [],
82
+ deleted: [],
83
+ linked: (value as any[]).filter((r) => r.id).map((r) => r.id),
84
+ unlinked: [],
85
+ };
86
+ } else if (value !== null && value !== undefined && value !== '') {
87
+ data[key] = value;
88
+ }
78
89
  }
79
- }
80
90
 
81
- if (rollback) {
82
- if (onSuccessfullSave) await onSuccessfullSave(localEntry);
83
- onCancel?.();
84
- return;
91
+ target.data = data;
92
+ target.children = children;
93
+
94
+ if (!isRecordingMode) {
95
+ console.log(`[${collectionName}] changes:`, $state.snapshot(target));
96
+ }
97
+ });
98
+ });
99
+
100
+ function handleCancel() {
101
+ if (changes !== undefined) {
102
+ changes.data = {};
103
+ changes.children = {};
85
104
  }
105
+ onCancel?.();
106
+ }
107
+
108
+ async function handleSave() {
109
+ const target = changes ?? _changes;
110
+ const snap = $state.snapshot(target);
86
111
 
87
- const children = buildChildren(ctx, collectionName, localEntry);
88
- const response = await lobb.createOne(collectionName, localEntry, children);
112
+ const children = buildChildren(ctx, collectionName, { ...snap.data, ...Object.fromEntries(
113
+ Object.entries(snap.children).map(([col, ops]) => [
114
+ col,
115
+ [
116
+ ...(ops.created.map((c) => c.data)),
117
+ ...(ops.linked.map((id) => ({ id }))),
118
+ ]
119
+ ])
120
+ )});
89
121
 
90
- await emitEvent({ lobb, ctx }, "studio.collections.create", {
91
- collectionName,
92
- entry: localEntry,
93
- response: response,
94
- });
122
+ const response = await lobb.createOne(collectionName, snap.data, children, undefined, isRecordingMode);
123
+
124
+ if (response.status === 204) {
125
+ if (onSuccessfullSave) await onSuccessfullSave(snap);
126
+ toast.success(`The record was successfully created`);
127
+ handleCancel();
128
+ return;
129
+ }
95
130
 
96
131
  if (!response.bodyUsed) {
97
- let result = await response.json();
132
+ const result = await response.json();
98
133
  if (response.status >= 400) {
99
134
  if (result.message && result.details) {
100
135
  fieldsErrors = result.details;
@@ -105,17 +140,17 @@
105
140
  }
106
141
  }
107
142
 
108
- if (onSuccessfullSave) await onSuccessfullSave(localEntry);
143
+ if (onSuccessfullSave) await onSuccessfullSave(snap);
109
144
  toast.success(`The record was successfully created`);
110
145
  onCancel?.();
111
146
  }
112
147
  </script>
113
148
 
114
- <Drawer onHide={onCancel}>
149
+ <Drawer onHide={handleCancel}>
115
150
  <div class="flex h-12 items-center gap-4 border-b px-4">
116
151
  <Button
117
152
  variant="outline"
118
- onclick={onCancel}
153
+ onclick={handleCancel}
119
154
  class=" h-8 w-8 rounded-full text-xs font-normal"
120
155
  Icon={ArrowLeft}
121
156
  ></Button>
@@ -175,7 +210,7 @@
175
210
  <div class="flex gap-3">
176
211
  <Button
177
212
  variant="outline"
178
- onclick={onCancel}
213
+ onclick={handleCancel}
179
214
  class="h-7 px-3 text-xs font-normal"
180
215
  Icon={X}
181
216
  >
@@ -6,13 +6,13 @@ export interface CreateDetailViewProp {
6
6
  collectionName: string;
7
7
  values?: Record<string, any>;
8
8
  showRelatedRecords?: boolean;
9
- rollback?: boolean;
10
9
  submitButton?: SubmitButton;
11
10
  title?: Snippet<[string]>;
12
11
  onSuccessfullSave?: (entry: any) => Promise<void>;
13
12
  onCancel?: () => Promise<void>;
13
+ changes?: import("../utils").Changes | undefined;
14
14
  }
15
15
  import type { Snippet } from "svelte";
16
- declare const CreateDetailView: import("svelte").Component<CreateDetailViewProp, {}, "">;
16
+ declare const CreateDetailView: import("svelte").Component<CreateDetailViewProp, {}, "changes">;
17
17
  type CreateDetailView = ReturnType<typeof CreateDetailView>;
18
18
  export default CreateDetailView;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { CreateDetailViewProp } from "./createDetailView.svelte";
3
- import type { ButtonProps } from "../../../components/ui/button/button.svelte";
4
- import Button from "../../../components/ui/button/button.svelte";
3
+ import type { ButtonProps } from "../../ui/button/button.svelte";
4
+ import Button from "../../ui/button/button.svelte";
5
5
  import CreateDetailView from "./createDetailView.svelte";
6
6
 
7
7
  interface LocalProp extends CreateDetailViewProp {
@@ -1,5 +1,5 @@
1
1
  import type { CreateDetailViewProp } from "./createDetailView.svelte";
2
- import type { ButtonProps } from "../../../components/ui/button/button.svelte";
2
+ import type { ButtonProps } from "../../ui/button/button.svelte";
3
3
  interface LocalProp extends CreateDetailViewProp {
4
4
  variant?: ButtonProps["variant"];
5
5
  class?: ButtonProps["class"];
@@ -11,11 +11,15 @@
11
11
  import Table, { type TableProps } from "../../dataTable/table.svelte";
12
12
  import Button from "../../ui/button/button.svelte";
13
13
  import CreateDetailViewButton from "./createDetailViewButton.svelte";
14
- import { getCollectionColumns } from "../../../components/dataTable/utils";
15
- import SelectRecord from "../../../components/selectRecord.svelte";
16
- import FieldCell from "../../../components/dataTable/fieldCell.svelte";
14
+ import type { Changes } from "../utils";
15
+
16
+ let addChanges = $state<Changes>({ data: {}, children: {} });
17
+ let editChanges = $state<Changes>({ data: {}, children: {} });
18
+ import { getCollectionColumns } from "../../dataTable/utils";
19
+ import SelectRecord from "../../selectRecord.svelte";
20
+ import FieldCell from "../../dataTable/fieldCell.svelte";
17
21
  import SubRecords from "./subRecords.svelte";
18
- import ListViewChildren from "../../../components/dataTable/listViewChildren.svelte";
22
+ import ListViewChildren from "../../dataTable/listViewChildren.svelte";
19
23
  import { getStudioContext } from "../../../context";
20
24
 
21
25
  const { ctx } = getStudioContext();
@@ -141,7 +145,7 @@
141
145
  class="h-7 px-2 font-normal text-xs"
142
146
  Icon={Plus}
143
147
  {collectionName}
144
- rollback={true}
148
+ changes={addChanges}
145
149
  showRelatedRecords={true}
146
150
  onSuccessfullSave={onRecordAdd}
147
151
  values={createValues}
@@ -197,7 +201,7 @@
197
201
  class="h-6 w-6 text-muted-foreground hover:bg-transparent p-0"
198
202
  Icon={Pencil}
199
203
  {collectionName}
200
- rollback={true}
204
+ changes={editChanges}
201
205
  showRelatedRecords={true}
202
206
  onSuccessfullSave={(entry) =>
203
207
  onRecordOverride(entry, index)}
@@ -6,7 +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 * as Select from "../../components/ui/select/index";
9
+ import * as Select from "../ui/select/index";
10
10
  import EnumBadge from "../dataTable/enumBadge.svelte";
11
11
  import type { EnumOption } from "@lobb-js/core";
12
12
  import Textarea from "../ui/textarea/textarea.svelte";
@@ -2,7 +2,7 @@
2
2
  import { Ban, Check, CircleAlert, X } from "lucide-svelte";
3
3
  import Button from "../ui/button/button.svelte";
4
4
  import Input from "../ui/input/input.svelte";
5
- import * as Select from "../../components/ui/select/index";
5
+ import * as Select from "../ui/select/index";
6
6
  import Textarea from "../ui/textarea/textarea.svelte";
7
7
  import type { EntryField } from "./detailViewForm.svelte";
8
8
  import type { Snippet } from "svelte";
@@ -1,43 +1,32 @@
1
1
  <script lang="ts">
2
- import DataTable, { type RecordOperation } from "../../../components/dataTable/dataTable.svelte";
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 PendingOps = {
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
- pendingChildren?: Record<string, PendingOps>;
13
+ activeChanges?: Changes;
19
14
  }
20
15
 
21
- let { collectionName, entry, pendingChildren = $bindable({}) }: LocalProp = $props();
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
- onOperation={makeHandler(child.collection)}
44
+ bind:changes={activeChanges!.children[child.collection]}
56
45
  showImport={false}
57
46
  showHeader={true}
58
47
  showFooter={true}
@@ -1,14 +1,9 @@
1
- type PendingOps = {
2
- link?: (string | number)[];
3
- unlink?: (string | number)[];
4
- delete?: (string | number)[];
5
- create?: any[];
6
- };
1
+ import type { Changes } from "../utils";
7
2
  interface LocalProp {
8
3
  collectionName: string;
9
4
  entry: any;
10
- pendingChildren?: Record<string, PendingOps>;
5
+ activeChanges?: Changes;
11
6
  }
12
- declare const DetailViewChildren: import("svelte").Component<LocalProp, {}, "pendingChildren">;
7
+ declare const DetailViewChildren: import("svelte").Component<LocalProp, {}, "">;
13
8
  type DetailViewChildren = ReturnType<typeof DetailViewChildren>;
14
9
  export default DetailViewChildren;
@@ -9,31 +9,32 @@
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 "../../../components/ui/button/button.svelte";
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
25
  import ExtensionsComponents from "../../extensionsComponents.svelte";
27
26
  import { getExtensionUtils } from "../../../extensions/extensionUtils";
27
+ import { untrack } from "svelte";
28
28
 
29
29
  const { lobb, ctx } = getStudioContext();
30
- import { calculateDrawerWidth, getChangedProperties } from "../../../utils";
30
+ import { getChangedProperties } from "../../../utils";
31
31
  import { getField, getFieldIcon } from "../../dataTable/utils";
32
- import DetailViewChildren from "../update/detailViewChildren.svelte";
32
+ import DetailViewChildren from "./detailViewChildren.svelte";
33
33
  import type { Snippet } from "svelte";
34
34
  import { getDefaultEntry } from "../utils";
35
+ import type { Changes, ChildrenChanges } from "../utils";
35
36
  import FieldInput from "../fieldInput.svelte";
36
- import Drawer from "../../../components/drawer.svelte";
37
+ import Drawer from "../../drawer.svelte";
37
38
 
38
39
  let {
39
40
  collectionName,
@@ -44,29 +45,74 @@
44
45
  title,
45
46
  submitButton,
46
47
  recordId,
48
+ changes = $bindable<Changes | undefined>(undefined),
47
49
  }: UpdateDetailViewProp = $props();
48
50
 
51
+ // Internal changes — used when not in recording mode, passed down to children
52
+ let _changes = $state<Changes>({ data: {}, children: {} });
53
+
54
+ // Recording mode = changes was passed from a parent component
55
+ const isRecordingMode = $derived(changes !== undefined);
56
+
49
57
  const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
50
58
  let entry: Record<string, any> = $state(
51
59
  getDefaultEntry(ctx, fieldNames, collectionName, values),
52
60
  );
53
61
  const initialEntry = $state.snapshot(entry);
54
- let localEntry = $derived(
55
- getChangedProperties(initialEntry, $state.snapshot(entry)),
56
- );
57
62
  let fieldsErrors: Record<string, any> = $state({});
58
- let pendingChildren = $state<Record<string, any>>({});
63
+
64
+ // Tracks field edits into the active changes object.
65
+ // Child ops (create/link/unlink/delete) are written directly by DataTable into changes.children.
66
+ $effect(() => {
67
+ const currentEntrySnap = $state.snapshot(entry);
68
+
69
+ untrack(() => {
70
+ const target = changes ?? _changes;
71
+ target.data = getChangedProperties(initialEntry, currentEntrySnap);
72
+
73
+ if (!isRecordingMode) {
74
+ console.log(`[${collectionName}] changes:`, $state.snapshot(target));
75
+ }
76
+ });
77
+ });
78
+
79
+ function buildApiChildren(children: Record<string, ChildrenChanges>): Record<string, any> | undefined {
80
+ const result: Record<string, any> = {};
81
+ for (const [collection, ops] of Object.entries(children)) {
82
+ const hasOps = ops.created.length || ops.deleted.length || ops.linked.length || ops.unlinked.length;
83
+ if (!hasOps) continue;
84
+ result[collection] = {
85
+ ...(ops.created.length ? { create: ops.created.map((c) => c.data) } : {}),
86
+ ...(ops.deleted.length ? { delete: ops.deleted.map((r) => r.id) } : {}),
87
+ ...(ops.linked.length ? { link: ops.linked.map((r) => r.id) } : {}),
88
+ ...(ops.unlinked.length ? { unlink: ops.unlinked.map((r) => r.id) } : {}),
89
+ };
90
+ }
91
+ return Object.keys(result).length ? result : undefined;
92
+ }
93
+
94
+ function handleCancel() {
95
+ if (changes !== undefined) {
96
+ changes.data = {};
97
+ changes.children = {};
98
+ }
99
+ onCancel?.();
100
+ }
59
101
 
60
102
  async function handleSave() {
61
- delete localEntry.id;
103
+ const target = changes ?? _changes;
104
+ const snap = $state.snapshot(target);
105
+ const { id: _id, ...data } = snap.data;
106
+ const children = buildApiChildren(snap.children);
62
107
 
63
- const children = Object.keys(pendingChildren).length ? pendingChildren : undefined;
64
- const response = await lobb.updateOne(
65
- collectionName,
66
- recordId,
67
- localEntry,
68
- children,
69
- );
108
+ const response = await lobb.updateOne(collectionName, recordId, data, children, isRecordingMode);
109
+
110
+ if (response.status === 204) {
111
+ if (onSuccessfullSave) await onSuccessfullSave(snap);
112
+ toast.success(`The record was successfully updated`);
113
+ onCancel?.();
114
+ return;
115
+ }
70
116
 
71
117
  if (!response.bodyUsed) {
72
118
  const result = await response.json();
@@ -80,22 +126,34 @@
80
126
  }
81
127
  }
82
128
 
83
- // close detailView side bar
84
- if (onSuccessfullSave) {
85
- await onSuccessfullSave(localEntry);
129
+ // Real mode: also fire separate update requests for edited children
130
+ if (!isRecordingMode) {
131
+ for (const [collection, ops] of Object.entries(snap.children)) {
132
+ for (const updated of ops.updated) {
133
+ await lobb.updateOne(collection, String(updated.id), updated.data);
134
+ }
135
+ }
86
136
  }
87
137
 
138
+ if (onSuccessfullSave) await onSuccessfullSave(snap);
88
139
  toast.success(`The record was successfully updated`);
89
-
90
140
  onCancel?.();
91
141
  }
142
+
143
+ const activeChanges = $derived(changes ?? _changes);
144
+ const hasChanges = $derived(
145
+ Object.keys(activeChanges.data).length > 0 ||
146
+ Object.values(activeChanges.children).some(
147
+ (c) => c.created.length || c.updated.length || c.deleted.length || c.linked.length || c.unlinked.length,
148
+ ),
149
+ );
92
150
  </script>
93
151
 
94
- <Drawer onHide={onCancel}>
152
+ <Drawer onHide={handleCancel}>
95
153
  <div class="flex h-12 items-center gap-4 border-b px-4">
96
154
  <Button
97
155
  variant="outline"
98
- onclick={onCancel}
156
+ onclick={handleCancel}
99
157
  class=" h-8 w-8 rounded-full text-xs font-normal"
100
158
  Icon={ArrowLeft}
101
159
  ></Button>
@@ -145,14 +203,14 @@
145
203
  {/each}
146
204
  </div>
147
205
  {#if showRelatedRecords}
148
- <DetailViewChildren {collectionName} {entry} bind:pendingChildren />
206
+ <DetailViewChildren {collectionName} {entry} {activeChanges} />
149
207
  {/if}
150
208
  </div>
151
209
  <div class="flex h-12 items-center justify-end gap-2 border-t px-4">
152
210
  <div class="flex gap-3">
153
211
  <Button
154
212
  variant="outline"
155
- onclick={onCancel}
213
+ onclick={handleCancel}
156
214
  class="h-7 px-3 text-xs font-normal"
157
215
  Icon={X}
158
216
  >
@@ -163,7 +221,7 @@
163
221
  class="h-7 px-3 text-xs font-normal"
164
222
  Icon={submitButton?.icon ? submitButton.icon : Pencil}
165
223
  onclick={handleSave}
166
- disabled={!Object.keys(localEntry).length && !Object.keys(pendingChildren).length}
224
+ disabled={!hasChanges}
167
225
  >
168
226
  {submitButton?.text ? submitButton.text : "Update"}
169
227
  </Button>
@@ -7,13 +7,13 @@ export interface UpdateDetailViewProp {
7
7
  recordId: string;
8
8
  values?: Record<string, any>;
9
9
  showRelatedRecords?: boolean;
10
- rollback?: boolean;
11
10
  submitButton?: SubmitButton;
12
11
  title?: Snippet<[string]>;
13
12
  onSuccessfullSave?: (entry: any) => Promise<void>;
14
13
  onCancel?: () => Promise<void>;
14
+ changes?: import("../utils").Changes | undefined;
15
15
  }
16
16
  import type { Snippet } from "svelte";
17
- declare const UpdateDetailView: import("svelte").Component<UpdateDetailViewProp, {}, "">;
17
+ declare const UpdateDetailView: import("svelte").Component<UpdateDetailViewProp, {}, "changes">;
18
18
  type UpdateDetailView = ReturnType<typeof UpdateDetailView>;
19
19
  export default UpdateDetailView;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { UpdateDetailViewProp } from "./updateDetailView.svelte";
3
- import type { ButtonProps } from "../../../components/ui/button/button.svelte";
4
- import Button from "../../../components/ui/button/button.svelte";
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;
@@ -1,5 +1,5 @@
1
1
  import type { UpdateDetailViewProp } from "./updateDetailView.svelte";
2
- import type { ButtonProps } from "../../../components/ui/button/button.svelte";
2
+ import type { ButtonProps } from "../../ui/button/button.svelte";
3
3
  interface LocalProp extends UpdateDetailViewProp {
4
4
  variant?: ButtonProps["variant"];
5
5
  class?: ButtonProps["class"];
@@ -1,5 +1,22 @@
1
1
  import type { CTX } from "../../store.types";
2
2
  import type { DetailFormField } from "./detailViewForm.svelte";
3
+ export type ChildrenChanges = {
4
+ created: Array<{
5
+ data: Record<string, any>;
6
+ }>;
7
+ updated: Array<{
8
+ id: string | number;
9
+ data: Record<string, any>;
10
+ children: Record<string, ChildrenChanges>;
11
+ }>;
12
+ deleted: Array<Record<string, any>>;
13
+ linked: Array<Record<string, any>>;
14
+ unlinked: Array<Record<string, any>>;
15
+ };
16
+ export type Changes = {
17
+ data: Record<string, any>;
18
+ children: Record<string, ChildrenChanges>;
19
+ };
3
20
  export declare function getDefaultEntry(ctx: CTX, fieldNames: string[], collectionName: string, values?: Record<string, any>): {
4
21
  [k: string]: any;
5
22
  };
@@ -12,10 +12,10 @@
12
12
 
13
13
  <script lang="ts">
14
14
  import { House, Layers, Library, Workflow, X } from "lucide-svelte";
15
- import Button from "../components/ui/button/button.svelte";
16
- import Separator from "../components/ui/separator/separator.svelte";
17
- import * as Tooltip from "../components/ui/tooltip";
18
- import * as Accordion from "../components/ui/accordion/index.js";
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
19
 
20
20
  import { getStudioContext } from "../context";
21
21
  import { getDashboardNavs } from "../extensions/extensionUtils";