@lobb-js/studio 0.25.0 → 0.27.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 (39) hide show
  1. package/dist/components/dataTable/dataTable.svelte +77 -14
  2. package/dist/components/dataTable/dataTable.svelte.d.ts +25 -0
  3. package/dist/components/dataTable/header.svelte +88 -24
  4. package/dist/components/dataTable/header.svelte.d.ts +4 -0
  5. package/dist/components/dataTable/listViewChildren.svelte +106 -0
  6. package/dist/components/dataTable/listViewChildren.svelte.d.ts +9 -0
  7. package/dist/components/dataTable/table.svelte +1 -1
  8. package/dist/components/detailView/create/createManyView.svelte +2 -2
  9. package/dist/components/detailView/update/detailViewChildren.svelte +72 -0
  10. package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +14 -0
  11. package/dist/components/detailView/update/updateDetailView.svelte +6 -3
  12. package/dist/components/routes/collections/collections.svelte +44 -23
  13. package/dist/components/routes/data_model/dataModel.svelte +2 -8
  14. package/dist/components/routes/workflows/workflows.svelte +24 -11
  15. package/dist/components/sidebar/sidebar.svelte +12 -5
  16. package/dist/components/sidebar/sidebar.svelte.d.ts +1 -2
  17. package/dist/components/sidebar/sidebarElements.svelte +50 -75
  18. package/dist/components/sidebar/sidebarElements.svelte.d.ts +10 -3
  19. package/dist/utils.js +2 -1
  20. package/package.json +2 -2
  21. package/src/lib/components/dataTable/dataTable.svelte +77 -14
  22. package/src/lib/components/dataTable/header.svelte +88 -24
  23. package/src/lib/components/dataTable/listViewChildren.svelte +106 -0
  24. package/src/lib/components/dataTable/table.svelte +1 -1
  25. package/src/lib/components/detailView/create/createManyView.svelte +2 -2
  26. package/src/lib/components/detailView/update/detailViewChildren.svelte +72 -0
  27. package/src/lib/components/detailView/update/updateDetailView.svelte +6 -3
  28. package/src/lib/components/routes/collections/collections.svelte +44 -23
  29. package/src/lib/components/routes/data_model/dataModel.svelte +2 -8
  30. package/src/lib/components/routes/workflows/workflows.svelte +24 -11
  31. package/src/lib/components/sidebar/sidebar.svelte +12 -5
  32. package/src/lib/components/sidebar/sidebarElements.svelte +50 -75
  33. package/src/lib/utils.ts +2 -1
  34. package/dist/components/dataTable/childRecords.svelte +0 -142
  35. package/dist/components/dataTable/childRecords.svelte.d.ts +0 -9
  36. package/dist/components/detailView/update/children.svelte +0 -96
  37. package/dist/components/detailView/update/children.svelte.d.ts +0 -7
  38. package/src/lib/components/dataTable/childRecords.svelte +0 -142
  39. package/src/lib/components/detailView/update/children.svelte +0 -96
@@ -1,40 +1,61 @@
1
1
  <script lang="ts">
2
- import type { SideBarData } from "../../../components/sidebar/sidebarElements.svelte";
2
+ import type { SideBarData, SideBarNode } from "../../../components/sidebar/sidebarElements.svelte";
3
3
  import Sidebar from "../../../components/sidebar/sidebar.svelte";
4
4
  import { getStudioContext } from "../../../context";
5
5
  import Collection from "./collection.svelte";
6
6
 
7
7
  const { ctx } = getStudioContext();
8
- import { Table } from "lucide-svelte";
8
+ import { Table, Cpu, LibraryBig } from "lucide-svelte";
9
+ import * as Icons from "lucide-svelte";
10
+
11
+ const directoryIcons: Record<string, any> = {
12
+ project: LibraryBig,
13
+ core: Cpu,
14
+ };
9
15
 
10
16
  let { collectionName } = $props();
11
17
 
12
18
  const collectionsList = $state(getCollectionsList());
13
19
 
14
- function getCollectionsList() {
20
+ function getCollectionsList(): SideBarData {
15
21
  const collections = ctx.meta.collections;
16
- let collectionsOwners: SideBarData = Object.entries(collections).map(
17
- ([collectionName, collectionValue]) => {
18
- return {
19
- name: collectionName,
20
- path: collectionValue.category ?? collectionValue.owner,
21
- icon: Table,
22
- href: `/studio/collections/${collectionName}`,
23
- };
24
- },
25
- );
26
-
27
- // updating the path from '__project' and '__core' to a more readable names
28
- collectionsOwners = collectionsOwners.map((item) => {
29
- if (item.path === "__project") {
30
- item.path = "project";
31
- } else if (item.path === "__core") {
32
- item.path = "core";
22
+
23
+ const groups = new Map<string, SideBarNode[]>();
24
+
25
+ for (const [name, value] of Object.entries(collections)) {
26
+ let groupKey: string = (value as any).category ?? (value as any).owner;
27
+ if (groupKey === "__project") groupKey = "project";
28
+ else if (groupKey === "__core") groupKey = "core";
29
+
30
+ if (!groups.has(groupKey)) {
31
+ groups.set(groupKey, []);
33
32
  }
34
- return item;
35
- });
33
+ groups.get(groupKey)!.push({
34
+ type: "element",
35
+ name,
36
+ icon: Table,
37
+ href: `/studio/collections/${name}`,
38
+ });
39
+ }
40
+
41
+ const result: SideBarData = [];
42
+ for (const [groupKey, children] of groups) {
43
+ const extensionIconName = ctx.meta.extensions?.[groupKey]?.icon;
44
+ const isProject = groupKey === "project";
45
+ result.push({
46
+ type: "directory",
47
+ name: isProject ? "Collections" : groupKey,
48
+ icon: directoryIcons[groupKey] ?? (extensionIconName ? (Icons as any)[extensionIconName] : undefined),
49
+ collapsed: isProject ? false : true,
50
+ children,
51
+ });
52
+ }
36
53
 
37
- return collectionsOwners;
54
+ return result.sort((a, b) => {
55
+ if ((a as any).name === "core") return 1;
56
+ if ((b as any).name === "core") return -1;
57
+ return 0;
58
+ });
38
59
  }
39
60
  </script>
40
61
 
@@ -13,14 +13,8 @@
13
13
  title="Data Model"
14
14
  showSearch={false}
15
15
  data={[
16
- {
17
- name: "graph",
18
- href: "/studio/datamodel/graph",
19
- },
20
- {
21
- name: "query_editor",
22
- href: "/studio/datamodel/query_editor",
23
- },
16
+ { type: "element", name: "graph", href: "/studio/datamodel/graph" },
17
+ { type: "element", name: "query_editor", href: "/studio/datamodel/query_editor" },
24
18
  ]}
25
19
  >
26
20
  <div class="relative h-full w-full">
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { SideBarData } from "../../../components/sidebar/sidebarElements.svelte";
2
+ import type { SideBarData, SideBarNode } from "../../../components/sidebar/sidebarElements.svelte";
3
3
  import WorkflowEditor, {
4
4
  type WorkflowEntry,
5
5
  } from "../../../components/workflowEditor.svelte";
@@ -31,18 +31,31 @@
31
31
  const response = await lobb.findAll("core_workflows", {});
32
32
  const result = await response.json();
33
33
  const workflows: any[] = result.data;
34
- sidebarData = workflows.map((workflow) => {
35
- return {
34
+
35
+ const groups = new Map<string, SideBarNode[]>();
36
+ const nodes: SideBarData = [];
37
+
38
+ for (const workflow of workflows) {
39
+ const item: SideBarNode = {
40
+ type: "element",
36
41
  name: workflow.name,
37
- path: workflow.directory,
38
- onclick: () => {
39
- location.navigate(`/studio/workflows/${workflow.name}`);
40
- },
41
- meta: {
42
- id: workflow.id,
43
- },
42
+ onclick: () => location.navigate(`/studio/workflows/${workflow.name}`),
43
+ meta: { id: workflow.id },
44
44
  };
45
- });
45
+
46
+ if (workflow.directory) {
47
+ if (!groups.has(workflow.directory)) {
48
+ const children: SideBarNode[] = [];
49
+ groups.set(workflow.directory, children);
50
+ nodes.push({ type: "directory", name: workflow.directory, children });
51
+ }
52
+ groups.get(workflow.directory)!.push(item);
53
+ } else {
54
+ nodes.push(item);
55
+ }
56
+ }
57
+
58
+ sidebarData = nodes;
46
59
  }
47
60
 
48
61
  async function fetchWorkflowData(workflowName: string) {
@@ -13,13 +13,12 @@
13
13
  import SidebarElements, {
14
14
  type SideBarData,
15
15
  type SideBarElement,
16
- type SidebarElementsProps,
16
+ type SideBarNode,
17
17
  } from "./sidebarElements.svelte";
18
18
 
19
19
  interface Props {
20
20
  title: string;
21
21
  data: SideBarData | null;
22
- sidebarElementsProps?: Partial<SidebarElementsProps>;
23
22
  showSearch?: boolean;
24
23
  children: Snippet<[]>;
25
24
  belowSearch?: Snippet;
@@ -29,7 +28,6 @@
29
28
  let {
30
29
  title,
31
30
  data,
32
- sidebarElementsProps,
33
31
  showSearch = true,
34
32
  children,
35
33
  belowSearch,
@@ -67,7 +65,17 @@
67
65
  }
68
66
 
69
67
  function filterSidebarData(items: SideBarData, term: string): SideBarData {
70
- return items.filter((item) => item.name.includes(term));
68
+ return items.reduce<SideBarData>((acc, node) => {
69
+ if (node.type === "directory") {
70
+ const filteredChildren = filterSidebarData(node.children, term);
71
+ if (filteredChildren.length > 0) {
72
+ acc.push({ ...node, collapsed: false, children: filteredChildren });
73
+ }
74
+ } else if (node.name.toLowerCase().includes(term)) {
75
+ acc.push(node);
76
+ }
77
+ return acc;
78
+ }, []);
71
79
  }
72
80
  </script>
73
81
 
@@ -124,7 +132,6 @@
124
132
  <SidebarElements
125
133
  bind:data={visibleData}
126
134
  {elementRightSide}
127
- {...sidebarElementsProps}
128
135
  />
129
136
  {/key}
130
137
  </div>
@@ -2,11 +2,10 @@ export interface SidebarProperties {
2
2
  collapsed: boolean;
3
3
  }
4
4
  import { type Snippet } from "svelte";
5
- import { type SideBarData, type SideBarElement, type SidebarElementsProps } from "./sidebarElements.svelte";
5
+ import { type SideBarData, type SideBarElement } from "./sidebarElements.svelte";
6
6
  interface Props {
7
7
  title: string;
8
8
  data: SideBarData | null;
9
- sidebarElementsProps?: Partial<SidebarElementsProps>;
10
9
  showSearch?: boolean;
11
10
  children: Snippet<[]>;
12
11
  belowSearch?: Snippet;
@@ -1,24 +1,32 @@
1
1
  <script lang="ts" module>
2
2
  export interface SideBarElement {
3
+ type: "element";
3
4
  name: string;
4
5
  onclick?: () => Promise<void> | void;
5
6
  href?: string;
6
7
  icon?: any;
7
- path?: string;
8
8
  meta?: Record<string, any>;
9
9
  }
10
10
 
11
- export type SideBarData = Array<SideBarElement>;
11
+ export interface SideBarDirectory {
12
+ type: "directory";
13
+ name: string;
14
+ icon?: any;
15
+ collapsed?: boolean;
16
+ children: SideBarNode[];
17
+ }
18
+
19
+ export type SideBarNode = SideBarElement | SideBarDirectory;
20
+ export type SideBarData = SideBarNode[];
12
21
 
13
22
  export interface SidebarElementsProps {
14
23
  data: SideBarData;
15
- path?: string[];
16
24
  elementRightSide?: Snippet<[SideBarElement]>;
17
25
  }
18
26
  </script>
19
27
 
20
28
  <script lang="ts">
21
- import { Ban } from "lucide-svelte";
29
+ import { Ban, ChevronDown, Folder } from "lucide-svelte";
22
30
  import type { Snippet } from "svelte";
23
31
  import SidebarElements from "./sidebarElements.svelte";
24
32
  import Button from "../ui/button/button.svelte";
@@ -26,117 +34,84 @@
26
34
 
27
35
  let {
28
36
  data = $bindable(),
29
- path = [],
30
37
  elementRightSide,
31
38
  }: SidebarElementsProps = $props();
32
39
 
33
- const elementsToShow = data.reduce<Array<string | SideBarElement>>(
34
- (acc, item, index) => {
35
- const firstPath = item.path?.split("/")[0];
36
- if (firstPath && item.path && !acc.includes(firstPath)) {
37
- acc.push(firstPath);
38
- } else if (!item.path) {
39
- acc.push(item);
40
- }
41
- return acc;
42
- },
43
- [],
40
+ let expandedHeights: number[] = $state(Array(data.length).fill(0));
41
+ let collapsedDirs: boolean[] = $state(
42
+ data.map((node) => node.type === "directory" ? (node.collapsed ?? false) : false)
44
43
  );
45
44
 
46
- let expandedHeights: number[] = $state(Array(data?.length).fill(0));
45
+ function toggleDir(index: number) {
46
+ collapsedDirs[index] = !collapsedDirs[index];
47
+ }
47
48
 
48
49
  async function handleElementClick(element: SideBarElement) {
49
50
  if (element.onclick) {
50
51
  await element.onclick();
51
52
  }
52
53
  }
53
-
54
- function getDirElements(dirName: string) {
55
- let elements = data.filter((item) => item.path?.startsWith(dirName));
56
- elements = elements.map((item) => {
57
- return {
58
- ...item,
59
- path: item.path?.split("/").slice(1).join("/"),
60
- };
61
- });
62
- return elements;
63
- }
64
54
  </script>
65
55
 
66
56
  <div class="flex flex-col">
67
- {#if elementsToShow.length}
68
- {#each elementsToShow as element, index}
69
- {#if typeof element === "string"}
70
- {@const directoryName = element.split("/")[0]}
57
+ {#if data.length}
58
+ {#each data as node, index}
59
+ {#if node.type === "directory"}
71
60
  <button
72
- class="
73
- flex items-center justify-between p-2 gap-2 text-muted-foreground
74
- rounded-md cursor-default
75
- "
61
+ class="flex items-center justify-between p-2 gap-2 text-muted-foreground rounded-md hover:bg-muted/30 cursor-pointer"
62
+ onclick={() => toggleDir(index)}
76
63
  >
77
64
  <div class="flex items-center gap-2">
78
- <div class="text-xs">
79
- {directoryName}
80
- </div>
65
+ {#if node.icon}
66
+ <node.icon size="17.5" />
67
+ {:else}
68
+ <Folder size="17.5" />
69
+ {/if}
70
+ <div class="text-xs">{node.name}</div>
81
71
  </div>
72
+ <ChevronDown
73
+ size="14"
74
+ class="transition-transform duration-200 {collapsedDirs[index] ? '-rotate-90' : ''}"
75
+ />
82
76
  </button>
83
- {#if getDirElements(directoryName)}
84
- <div
85
- class="overflow-hidden"
86
- style="
87
- height: {true ? expandedHeights[index] : 0}px;
88
- "
89
- >
90
- <div
91
- bind:clientHeight={expandedHeights[index]}
92
- class="border-l ml-4 pl-2"
93
- >
94
- <SidebarElements
95
- data={getDirElements(element)}
96
- path={[...path, element]}
97
- {elementRightSide}
98
- />
99
- </div>
77
+ <div
78
+ class="overflow-hidden transition-[height] duration-200"
79
+ style="height: {collapsedDirs[index] ? 0 : expandedHeights[index]}px;"
80
+ >
81
+ <div bind:clientHeight={expandedHeights[index]} class="border-l ml-4 pl-2">
82
+ <SidebarElements data={node.children} {elementRightSide} />
100
83
  </div>
101
- {/if}
84
+ </div>
102
85
  {:else}
103
- {@const elementPath = [...path, element]}
104
- {@const isselected = location.url.pathname === element.href}
86
+ {@const isselected = location.url.pathname === node.href}
105
87
  <Button
106
- onclick={() => handleElementClick(element)}
107
- href={element.href}
88
+ onclick={() => handleElementClick(node)}
89
+ href={node.href}
108
90
  variant="ghost"
109
91
  class="
110
92
  flex items-center justify-between p-2 gap-2 hover:bg-muted/30 text-muted-foreground
111
93
  rounded-md {isselected ? 'bg-muted' : ''}
112
94
  "
113
- title={element.name}
95
+ title={node.name}
114
96
  >
115
97
  <div class="flex items-center gap-2 truncate">
116
- {#if element.icon}
117
- <element.icon size="17.5" />
98
+ {#if node.icon}
99
+ <node.icon size="17.5" />
118
100
  {/if}
119
- <div
120
- class="
121
- text-xs
122
- {isselected ? 'text-primary font-medium' : ''}
123
- "
124
- >
125
- {element.name}
101
+ <div class="text-xs {isselected ? 'text-primary font-medium' : ''}">
102
+ {node.name}
126
103
  </div>
127
104
  </div>
128
105
  <div class="flex gap-2 items-center">
129
106
  {#if elementRightSide}
130
- {@render elementRightSide(element)}
107
+ {@render elementRightSide(node)}
131
108
  {/if}
132
109
  </div>
133
110
  </Button>
134
111
  {/if}
135
112
  {/each}
136
113
  {:else}
137
- <div
138
- class="flex justify-center items-center gap-2 text-muted-foreground"
139
- >
114
+ <div class="flex justify-center items-center gap-2 text-muted-foreground">
140
115
  <Ban size="17.5" />
141
116
  <div class="text-xs text-center">No result</div>
142
117
  </div>
@@ -1,15 +1,22 @@
1
1
  export interface SideBarElement {
2
+ type: "element";
2
3
  name: string;
3
4
  onclick?: () => Promise<void> | void;
4
5
  href?: string;
5
6
  icon?: any;
6
- path?: string;
7
7
  meta?: Record<string, any>;
8
8
  }
9
- export type SideBarData = Array<SideBarElement>;
9
+ export interface SideBarDirectory {
10
+ type: "directory";
11
+ name: string;
12
+ icon?: any;
13
+ collapsed?: boolean;
14
+ children: SideBarNode[];
15
+ }
16
+ export type SideBarNode = SideBarElement | SideBarDirectory;
17
+ export type SideBarData = SideBarNode[];
10
18
  export interface SidebarElementsProps {
11
19
  data: SideBarData;
12
- path?: string[];
13
20
  elementRightSide?: Snippet<[SideBarElement]>;
14
21
  }
15
22
  import type { Snippet } from "svelte";
package/dist/utils.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { clsx } from "clsx";
2
2
  import { twMerge } from "tailwind-merge";
3
+ import { isEqual } from "lodash";
3
4
  import { MediaQuery } from 'svelte/reactivity';
4
5
  export function cn() {
5
6
  var inputs = [];
@@ -26,7 +27,7 @@ export function getChangedProperties(oldObj, newObj) {
26
27
  var changes = {};
27
28
  for (var _i = 0, _a = Object.keys(newObj); _i < _a.length; _i++) {
28
29
  var key = _a[_i];
29
- if (oldObj[key] !== newObj[key]) {
30
+ if (!isEqual(oldObj[key], newObj[key])) {
30
31
  changes[key] = newObj[key];
31
32
  }
32
33
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lobb-js/studio",
3
3
  "license": "UNLICENSED",
4
- "version": "0.25.0",
4
+ "version": "0.27.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -42,7 +42,7 @@
42
42
  "postpublish": "./scripts/postpublish.sh"
43
43
  },
44
44
  "devDependencies": {
45
- "@lobb-js/core": "^0.28.0",
45
+ "@lobb-js/core": "^0.30.0",
46
46
  "@chromatic-com/storybook": "^4.1.2",
47
47
  "@storybook/addon-a11y": "^10.0.1",
48
48
  "@storybook/addon-docs": "^10.0.1",
@@ -1,3 +1,17 @@
1
+ <script lang="ts" module>
2
+ export interface ParentContext {
3
+ collectionName: string;
4
+ recordId: string | number;
5
+ }
6
+
7
+ export type RecordOperation =
8
+ | { type: "link"; record: any }
9
+ | { type: "unlink"; id: string | number }
10
+ | { type: "delete"; id: string | number }
11
+ | { type: "create"; record: any }
12
+ | { type: "update"; id: string | number; data: any };
13
+ </script>
14
+
1
15
  <script lang="ts">
2
16
  import _ from "lodash";
3
17
  import { getStudioContext } from "../../context";
@@ -5,9 +19,9 @@
5
19
  import Header from "./header.svelte";
6
20
  import Table, { type TableProps } from "./table.svelte";
7
21
  import { getCollectionColumns, getCollectionParamsFields } from "./utils";
8
- import { Pencil, Trash } from "lucide-svelte";
22
+ import { Pencil, Trash, Unlink } from "lucide-svelte";
9
23
  import * as icons from "lucide-svelte";
10
- import ChildRecords from "./childRecords.svelte";
24
+ import ListViewChildren from "./listViewChildren.svelte";
11
25
  import FieldCell from "./fieldCell.svelte";
12
26
  import Skeleton from "../ui/skeleton/skeleton.svelte";
13
27
  import Button from "../ui/button/button.svelte";
@@ -25,8 +39,12 @@
25
39
  interface Props {
26
40
  collectionName: string;
27
41
  filter?: any;
42
+ searchParams?: Record<string, any>;
43
+ parentContext?: ParentContext;
44
+ onOperation?: (op: RecordOperation) => void;
28
45
  showHeader?: boolean;
29
46
  showFooter?: boolean;
47
+ showImport?: boolean;
30
48
  unifiedBgColor?: "bg-muted/30" | "bg-background";
31
49
  showDelete?: boolean;
32
50
  tableProps?: Partial<TableProps>;
@@ -36,8 +54,12 @@
36
54
  let {
37
55
  collectionName,
38
56
  filter,
57
+ searchParams,
58
+ parentContext,
59
+ onOperation,
39
60
  showHeader = true,
40
61
  showFooter = true,
62
+ showImport = true,
41
63
  unifiedBgColor,
42
64
  showDelete = false,
43
65
  tableProps,
@@ -57,6 +79,7 @@
57
79
  sort: {},
58
80
  limit: "100",
59
81
  page: 1,
82
+ ...searchParams,
60
83
  });
61
84
 
62
85
  $effect(() => {
@@ -73,10 +96,9 @@
73
96
  );
74
97
  let dataTableContainerWidth: number = $state(0);
75
98
  let dataTableWidth: number = $state(0);
76
- const doesCollectionHasChildren = Boolean(
77
- ctx.meta.relations.find(
78
- (relation) => relation.to.collection === collectionName,
79
- ),
99
+ const doesCollectionHasChildren = $derived(
100
+ (ctx.meta.collections[collectionName]?.children ?? [])
101
+ .some((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic")
80
102
  );
81
103
 
82
104
  // requests the data from the server when the params is changed
@@ -107,14 +129,44 @@
107
129
  loading = false;
108
130
  }
109
131
 
132
+ // Internal handler: updates data optimistically then calls onOperation
133
+ function applyOperation(op: RecordOperation) {
134
+ if (op.type === "link") {
135
+ data = [...data, op.record];
136
+ } else if (op.type === "unlink" || op.type === "delete") {
137
+ data = data.filter((r: any) => String(r.id) !== String(op.id));
138
+ } else if (op.type === "create") {
139
+ data = [...data, { ...op.record, _pending: true }];
140
+ } else if (op.type === "update") {
141
+ data = data.map((r: any) => String(r.id) === String(op.id) ? { ...r, ...op.data } : r);
142
+ }
143
+ onOperation?.(op);
144
+ }
145
+
110
146
  async function handleDelete(entryId: string) {
111
- const result = await showDialog(
112
- "Are you sure?",
113
- "This will delete the record you selected.",
114
- );
147
+ const result = await showDialog("Are you sure?", "This will permanently delete the record.");
115
148
  if (result) {
116
- await lobb.deleteOne(collectionName, entryId);
117
- params = { ...params };
149
+ if (onOperation) {
150
+ applyOperation({ type: "delete", id: entryId });
151
+ } else if (parentContext) {
152
+ await lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), {}, { [collectionName]: { delete: [entryId] } });
153
+ params = { ...params };
154
+ } else {
155
+ await lobb.deleteOne(collectionName, entryId);
156
+ params = { ...params };
157
+ }
158
+ }
159
+ }
160
+
161
+ async function handleUnlink(entryId: string) {
162
+ const result = await showDialog("Are you sure?", "This will unlink the record without deleting it.");
163
+ if (result) {
164
+ if (onOperation) {
165
+ applyOperation({ type: "unlink", id: entryId });
166
+ } else {
167
+ await lobb.updateOne(parentContext!.collectionName, String(parentContext!.recordId), {}, { [collectionName]: { unlink: [entryId] } });
168
+ params = { ...params };
169
+ }
118
170
  }
119
171
  }
120
172
 
@@ -157,7 +209,7 @@
157
209
  {/snippet}
158
210
 
159
211
  {#if showHeader}
160
- <Header bind:params {collectionName} bind:selectedRecords>
212
+ <Header bind:params {collectionName} bind:selectedRecords {parentContext} {showImport} onOperation={onOperation ? applyOperation : undefined}>
161
213
  {#snippet left()}
162
214
  {@render headerLeft?.()}
163
215
  {/snippet}
@@ -198,6 +250,16 @@
198
250
  params = { ...params };
199
251
  }}
200
252
  ></UpdateDetailViewButton>
253
+ {#if parentContext}
254
+ <Button
255
+ class="h-6 w-6 text-muted-foreground hover:bg-transparent"
256
+ variant="ghost"
257
+ size="icon"
258
+ onclick={() => handleUnlink(entry.id)}
259
+ Icon={Unlink}
260
+ title="Remove from this entry"
261
+ ></Button>
262
+ {/if}
201
263
  {#if showDelete}
202
264
  <Button
203
265
  class="h-6 w-6 text-muted-foreground hover:bg-transparent"
@@ -205,6 +267,7 @@
205
267
  size="icon"
206
268
  onclick={() => handleDelete(entry.id)}
207
269
  Icon={Trash}
270
+ title="Delete permanently"
208
271
  ></Button>
209
272
  {/if}
210
273
  {#await getWorkflowTools($state.snapshot(entry))}
@@ -232,7 +295,7 @@
232
295
  />
233
296
  {/snippet}
234
297
  {#snippet collapsible(entry)}
235
- <ChildRecords
298
+ <ListViewChildren
236
299
  {collectionName}
237
300
  recordId={entry.id}
238
301
  width={dataTableWidth > dataTableContainerWidth