@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.
Files changed (160) hide show
  1. package/README.md +1 -0
  2. package/dist/actions.d.ts +2 -0
  3. package/dist/components/Studio.svelte +46 -47
  4. package/dist/components/StudioRoot.svelte +19 -0
  5. package/dist/components/StudioRoot.svelte.d.ts +6 -0
  6. package/dist/components/breadCrumbs.svelte +5 -4
  7. package/dist/components/codeEditor.svelte +1 -1
  8. package/dist/components/combobox.svelte +3 -3
  9. package/dist/components/confirmationDialog/confirmationDialog.svelte +1 -1
  10. package/dist/components/dataTable/dataTable.svelte +108 -101
  11. package/dist/components/dataTable/dataTable.svelte.d.ts +5 -20
  12. package/dist/components/dataTable/dataTableTabs.svelte +4 -2
  13. package/dist/components/dataTable/dataTableTabs.svelte.d.ts +2 -0
  14. package/dist/components/dataTable/filter.svelte +1 -1
  15. package/dist/components/dataTable/filterButton.svelte +1 -1
  16. package/dist/components/dataTable/header.svelte +30 -47
  17. package/dist/components/dataTable/header.svelte.d.ts +4 -2
  18. package/dist/components/dataTable/listViewChildren.svelte +4 -6
  19. package/dist/components/dataTable/listViewChildren.svelte.d.ts +0 -1
  20. package/dist/components/dataTable/sort.svelte +1 -1
  21. package/dist/components/dataTable/sortButton.svelte +2 -2
  22. package/dist/components/dataTable/table.svelte +8 -10
  23. package/dist/components/dataTable/table.svelte.d.ts +0 -1
  24. package/dist/components/dataTableDrawer/dataTableDrawer.svelte +4 -1
  25. package/dist/components/dataTableDrawer/dataTableDrawer.svelte.d.ts +2 -0
  26. package/dist/components/detailView/create/children.svelte +2 -2
  27. package/dist/components/detailView/create/createDetailView.svelte +81 -88
  28. package/dist/components/detailView/create/createDetailView.svelte.d.ts +2 -2
  29. package/dist/components/detailView/create/createDetailViewButton.svelte +2 -2
  30. package/dist/components/detailView/create/createDetailViewButton.svelte.d.ts +1 -1
  31. package/dist/components/detailView/create/createManyView.svelte +12 -10
  32. package/dist/components/detailView/detailView.svelte +81 -0
  33. package/dist/components/detailView/detailView.svelte.d.ts +8 -0
  34. package/dist/components/detailView/fieldInput.svelte +11 -11
  35. package/dist/components/detailView/fieldInputReplacement.svelte +8 -8
  36. package/dist/components/detailView/passwordInput.svelte +1 -1
  37. package/dist/components/detailView/update/detailViewChildren.svelte +15 -26
  38. package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +3 -8
  39. package/dist/components/detailView/update/updateDetailView.svelte +90 -69
  40. package/dist/components/detailView/update/updateDetailView.svelte.d.ts +2 -2
  41. package/dist/components/detailView/update/updateDetailViewButton.svelte +3 -2
  42. package/dist/components/detailView/update/updateDetailViewButton.svelte.d.ts +1 -1
  43. package/dist/components/detailView/utils.d.ts +17 -0
  44. package/dist/components/diffViewer.svelte +1 -1
  45. package/dist/components/extensionsComponents.svelte +3 -1
  46. package/dist/components/foreingKeyInput.svelte +2 -2
  47. package/dist/components/importButton.svelte +12 -9
  48. package/dist/components/landing.svelte +7 -0
  49. package/dist/components/landing.svelte.d.ts +6 -14
  50. package/dist/components/miniSidebar.svelte +90 -19
  51. package/dist/components/miniSidebar.svelte.d.ts +2 -17
  52. package/dist/components/polymorphicInput.svelte +1 -1
  53. package/dist/components/rangeCalendarButton.svelte +13 -13
  54. package/dist/components/richTextEditor.svelte +1 -1
  55. package/dist/components/routes/collections/collection.svelte +3 -3
  56. package/dist/components/routes/collections/collections.svelte +34 -12
  57. package/dist/components/routes/data_model/dataModel.svelte +6 -28
  58. package/dist/components/routes/data_model/dataModel.svelte.d.ts +17 -2
  59. package/dist/components/routes/extensions/extension.svelte +1 -1
  60. package/dist/components/routes/extensions/publicExtension.svelte +19 -0
  61. package/dist/components/routes/extensions/publicExtension.svelte.d.ts +13 -0
  62. package/dist/components/routes/home.svelte +3 -3
  63. package/dist/components/routes/workflows/workflows.svelte +9 -9
  64. package/dist/components/selectRecord.svelte +2 -21
  65. package/dist/components/setServerPage.svelte +1 -1
  66. package/dist/components/sidebar/sidebar.svelte +1 -1
  67. package/dist/components/sidebar/sidebarElements.svelte +4 -4
  68. package/dist/components/singletone.svelte +4 -6
  69. package/dist/components/ui/alert-dialog/alert-dialog-action.svelte +1 -1
  70. package/dist/components/ui/alert-dialog/alert-dialog-cancel.svelte +1 -1
  71. package/dist/components/ui/button/button.svelte +2 -3
  72. package/dist/components/ui/command/command-dialog.svelte +1 -1
  73. package/dist/components/ui/range-calendar/range-calendar-day.svelte +1 -1
  74. package/dist/components/ui/range-calendar/range-calendar-next-button.svelte +1 -1
  75. package/dist/components/ui/range-calendar/range-calendar-prev-button.svelte +1 -1
  76. package/dist/components/ui/select/select-separator.svelte +1 -1
  77. package/dist/components/workflowEditor.svelte +5 -5
  78. package/dist/eventSystem.d.ts +1 -1
  79. package/dist/eventSystem.js +7 -5
  80. package/dist/extensions/extension.types.d.ts +38 -14
  81. package/dist/extensions/extensionUtils.js +4 -2
  82. package/dist/index.d.ts +3 -1
  83. package/dist/index.js +3 -1
  84. package/dist/store.types.d.ts +2 -2
  85. package/dist/studioLifecycle.svelte.d.ts +2 -0
  86. package/dist/studioLifecycle.svelte.js +15 -0
  87. package/package.json +3 -4
  88. package/src/app.css +3 -0
  89. package/src/lib/actions.ts +2 -0
  90. package/src/lib/components/Studio.svelte +46 -47
  91. package/src/lib/components/StudioRoot.svelte +19 -0
  92. package/src/lib/components/breadCrumbs.svelte +5 -4
  93. package/src/lib/components/codeEditor.svelte +1 -1
  94. package/src/lib/components/combobox.svelte +3 -3
  95. package/src/lib/components/confirmationDialog/confirmationDialog.svelte +1 -1
  96. package/src/lib/components/dataTable/dataTable.svelte +108 -101
  97. package/src/lib/components/dataTable/dataTableTabs.svelte +4 -2
  98. package/src/lib/components/dataTable/filter.svelte +1 -1
  99. package/src/lib/components/dataTable/filterButton.svelte +1 -1
  100. package/src/lib/components/dataTable/header.svelte +30 -47
  101. package/src/lib/components/dataTable/listViewChildren.svelte +4 -6
  102. package/src/lib/components/dataTable/sort.svelte +1 -1
  103. package/src/lib/components/dataTable/sortButton.svelte +2 -2
  104. package/src/lib/components/dataTable/table.svelte +8 -10
  105. package/src/lib/components/dataTableDrawer/dataTableDrawer.svelte +4 -1
  106. package/src/lib/components/detailView/create/children.svelte +2 -2
  107. package/src/lib/components/detailView/create/createDetailView.svelte +81 -88
  108. package/src/lib/components/detailView/create/createDetailViewButton.svelte +2 -2
  109. package/src/lib/components/detailView/create/createManyView.svelte +12 -10
  110. package/src/lib/components/detailView/detailView.svelte +81 -0
  111. package/src/lib/components/detailView/fieldInput.svelte +11 -11
  112. package/src/lib/components/detailView/fieldInputReplacement.svelte +8 -8
  113. package/src/lib/components/detailView/passwordInput.svelte +1 -1
  114. package/src/lib/components/detailView/update/detailViewChildren.svelte +15 -26
  115. package/src/lib/components/detailView/update/updateDetailView.svelte +90 -69
  116. package/src/lib/components/detailView/update/updateDetailViewButton.svelte +3 -2
  117. package/src/lib/components/detailView/utils.ts +13 -0
  118. package/src/lib/components/diffViewer.svelte +1 -1
  119. package/src/lib/components/extensionsComponents.svelte +3 -1
  120. package/src/lib/components/foreingKeyInput.svelte +2 -2
  121. package/src/lib/components/importButton.svelte +12 -9
  122. package/src/lib/components/landing.svelte +7 -0
  123. package/src/lib/components/miniSidebar.svelte +90 -19
  124. package/src/lib/components/polymorphicInput.svelte +1 -1
  125. package/src/lib/components/rangeCalendarButton.svelte +13 -13
  126. package/src/lib/components/richTextEditor.svelte +1 -1
  127. package/src/lib/components/routes/collections/collection.svelte +3 -3
  128. package/src/lib/components/routes/collections/collections.svelte +34 -12
  129. package/src/lib/components/routes/data_model/dataModel.svelte +6 -28
  130. package/src/lib/components/routes/extensions/extension.svelte +1 -1
  131. package/src/lib/components/routes/extensions/publicExtension.svelte +19 -0
  132. package/src/lib/components/routes/home.svelte +3 -3
  133. package/src/lib/components/routes/workflows/workflows.svelte +9 -9
  134. package/src/lib/components/selectRecord.svelte +2 -21
  135. package/src/lib/components/setServerPage.svelte +1 -1
  136. package/src/lib/components/sidebar/sidebar.svelte +1 -1
  137. package/src/lib/components/sidebar/sidebarElements.svelte +4 -4
  138. package/src/lib/components/singletone.svelte +4 -6
  139. package/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte +1 -1
  140. package/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte +1 -1
  141. package/src/lib/components/ui/button/button.svelte +2 -3
  142. package/src/lib/components/ui/command/command-dialog.svelte +1 -1
  143. package/src/lib/components/ui/range-calendar/range-calendar-day.svelte +1 -1
  144. package/src/lib/components/ui/range-calendar/range-calendar-next-button.svelte +1 -1
  145. package/src/lib/components/ui/range-calendar/range-calendar-prev-button.svelte +1 -1
  146. package/src/lib/components/ui/select/select-separator.svelte +1 -1
  147. package/src/lib/components/workflowEditor.svelte +5 -5
  148. package/src/lib/eventSystem.ts +8 -7
  149. package/src/lib/extensions/extension.types.ts +39 -6
  150. package/src/lib/extensions/extensionUtils.ts +4 -2
  151. package/src/lib/index.ts +3 -1
  152. package/src/lib/store.types.ts +2 -2
  153. package/src/lib/studioLifecycle.svelte.ts +17 -0
  154. package/vite-plugins/index.js +2 -4
  155. package/vite-plugins/utils.js +15 -0
  156. package/vite-plugins/{workspace-optimize.js → workspace-fs-allow.js} +4 -18
  157. package/dist/components/routes/data_model/syncManager.svelte +0 -94
  158. package/dist/components/routes/data_model/syncManager.svelte.d.ts +0 -3
  159. package/src/lib/components/routes/data_model/syncManager.svelte +0 -94
  160. package/vite-plugins/contextual-lib-alias.js +0 -67
package/README.md CHANGED
@@ -1 +1,2 @@
1
1
  # Lobb Studio
2
+
package/dist/actions.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { CreateDetailViewProp } from "./components/detailView/create/createDetailView.svelte";
2
2
  import type { UpdateDetailViewProp } from "./components/detailView/update/updateDetailView.svelte";
3
3
  import type { StudioContext } from "./context";
4
+ import type { CollectionTab } from "./store.types";
4
5
  export interface OpenDataTableDrawerProps {
5
6
  collectionName: string;
6
7
  filter?: Record<string, any>;
@@ -8,6 +9,7 @@ export interface OpenDataTableDrawerProps {
8
9
  showHeader?: boolean;
9
10
  showFooter?: boolean;
10
11
  position?: "side" | "bottom";
12
+ tabs?: CollectionTab[];
11
13
  }
12
14
  export declare function showDialog(title: string, description: string): Promise<boolean>;
13
15
  export declare function openCreateDetailView(studioContext: StudioContext, props: CreateDetailViewProp): void;
@@ -1,14 +1,15 @@
1
1
  <script lang="ts">
2
- import { Toaster } from "../components/ui/sonner";
3
- import { onMount, onDestroy } from "svelte";
2
+ import { Toaster } from "./ui/sonner";
3
+ import { onMount } from "svelte";
4
4
  import { ModeWatcher } from "mode-watcher";
5
5
  import { createLobb } from "../store.svelte";
6
6
  import { setStudioContext } from "../context";
7
- import Header from "../components/header.svelte";
7
+ import Header from "./header.svelte";
8
8
  import { LoaderCircle, ServerOff } from "lucide-svelte";
9
- import MiniSidebar from "../components/miniSidebar.svelte";
10
- import * as Tooltip from "../components/ui/tooltip";
11
- import { Router, Route, Fallback, init as initRouter } from "@wjfe/n-savant";
9
+ import MiniSidebar from "./miniSidebar.svelte";
10
+ import * as Tooltip from "./ui/tooltip";
11
+ import { page } from "$app/state";
12
+ import { afterNavigate } from "$app/navigation";
12
13
  import {
13
14
  executeExtensionsOnStartup,
14
15
  executeExtensionsOnRouteChange,
@@ -22,6 +23,7 @@
22
23
  import Collections from "./routes/collections/collections.svelte";
23
24
  import Workflows from "./routes/workflows/workflows.svelte";
24
25
  import Extension from "./routes/extensions/extension.svelte";
26
+ import PublicExtension from "./routes/extensions/publicExtension.svelte";
25
27
 
26
28
  interface StudioProps {
27
29
  lobbUrl?: string;
@@ -42,10 +44,11 @@
42
44
 
43
45
  let status: "loading" | "error" | "ready" = $state("loading");
44
46
  let isSmallScreen = $derived(!mediaQueries.sm.current);
45
- let cleanupRouter: (() => void) | undefined;
46
47
 
47
48
  onMount(async () => {
48
- cleanupRouter = initRouter();
49
+ // Remove the static loading screen defined in app.html — it shows instantly
50
+ // before JS loads to avoid a blank page, and is replaced by the Studio UI.
51
+ document.getElementById("app-loading")?.remove();
49
52
  try {
50
53
  ctx.meta = await lobb.getMeta();
51
54
  ctx.extensions = await loadExtensions(lobb, ctx, extensionMap);
@@ -56,19 +59,13 @@
56
59
  console.error(err);
57
60
  status = "error";
58
61
  }
59
-
60
- // Fire onRouteChange hooks on every navigation
61
- const onRouteChange = () => executeExtensionsOnRouteChange(lobb, ctx as any, window.location.pathname);
62
- const originalPushState = history.pushState.bind(history);
63
- history.pushState = function (...args) {
64
- originalPushState(...args);
65
- onRouteChange();
66
- };
67
- window.addEventListener("popstate", onRouteChange);
68
62
  });
69
63
 
70
- onDestroy(() => {
71
- if (cleanupRouter) cleanupRouter();
64
+ // Fire onRouteChange hooks via SvelteKit's afterNavigate lifecycle instead
65
+ // of monkey-patching history.pushState. Runs both on initial mount and on
66
+ // every client-side navigation.
67
+ afterNavigate(() => {
68
+ executeExtensionsOnRouteChange(lobb, ctx as any, page.url.pathname);
72
69
  });
73
70
  </script>
74
71
 
@@ -91,6 +88,18 @@
91
88
  <div class="text-xs">Could not connect to the lobb server at this endpoint ({ctx.lobbUrl})</div>
92
89
  </div>
93
90
  </div>
91
+ {:else if page.url.pathname.startsWith("/studio/public/")}
92
+ <!-- Public extension pages skip the dashboard chrome (no sidebar, no
93
+ header) since the viewer is unauthenticated and shouldn't see any
94
+ navigation to gated areas. -->
95
+ <Tooltip.Provider delayDuration={0} disableHoverableContent={true}>
96
+ <main class="bg-background h-screen w-screen">
97
+ <PublicExtension
98
+ extension={page.url.pathname.split("/")[3]}
99
+ page={page.url.pathname.split("/")[4]}
100
+ />
101
+ </main>
102
+ </Tooltip.Provider>
94
103
  {:else}
95
104
  <Tooltip.Provider delayDuration={0} disableHoverableContent={true}>
96
105
  <main
@@ -100,34 +109,24 @@
100
109
  <MiniSidebar />
101
110
  <div class="second_grid">
102
111
  <Header />
103
- <Router id="root-router" basePath="/studio">
104
- <Route key="home" path="/">
105
- {#snippet children(params)}
106
- <Home />
107
- {/snippet}
108
- </Route>
109
- <Route key="collections" path="/collections/:collection?">
110
- {#snippet children(params)}
111
- <Collections collectionName={params?.collection} />
112
- {/snippet}
113
- </Route>
114
- <Route key="datamodel" path="/datamodel/*">
115
- {#snippet children(params)}
116
- <DataModel />
117
- {/snippet}
118
- </Route>
119
- <Route key="workflows" path="/workflows/:workflow?">
120
- {#snippet children(params)}
121
- <Workflows workflowName={params?.workflow} />
122
- {/snippet}
123
- </Route>
124
- <Route key="extensions" path="/extensions/:extension?/:page?/*">
125
- {#snippet children(params)}
126
- <Extension extension={params?.extension} page={params?.page} />
127
- {/snippet}
128
- </Route>
129
- <Fallback>Not Found</Fallback>
130
- </Router>
112
+ {#if page.url.pathname.replace(/\/$/, "") === "/studio"}
113
+ <Home />
114
+ {:else if page.url.pathname.startsWith("/studio/collections")}
115
+ <Collections collectionName={page.url.pathname.split("/")[3]} />
116
+ {:else if page.url.pathname.startsWith("/studio/datamodel")}
117
+ <DataModel />
118
+ {:else if page.url.pathname.startsWith("/studio/workflows")}
119
+ <Workflows workflowName={page.url.pathname.split("/")[3]} />
120
+ {:else if page.url.pathname.startsWith("/studio/extensions")}
121
+ <Extension
122
+ extension={page.url.pathname.split("/")[3]}
123
+ page={page.url.pathname.split("/")[4]}
124
+ />
125
+ {:else}
126
+ <div class="flex h-full w-full items-center justify-center text-muted-foreground">
127
+ Not Found
128
+ </div>
129
+ {/if}
131
130
  </div>
132
131
  </main>
133
132
  </Tooltip.Provider>
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import Studio from "./Studio.svelte";
3
+ import { getStudioMountKey } from "../studioLifecycle.svelte";
4
+
5
+ interface StudioRootProps {
6
+ lobbUrl?: string;
7
+ }
8
+
9
+ let { lobbUrl }: StudioRootProps = $props();
10
+
11
+ // Tracked so any change to the mount key tears down the Studio tree and
12
+ // mounts a fresh one — fresh ctx, fresh extension load, fresh /me, etc.
13
+ // Bumped by remountStudio() on login/logout.
14
+ const mountKey = $derived(getStudioMountKey());
15
+ </script>
16
+
17
+ {#key mountKey}
18
+ <Studio {lobbUrl} />
19
+ {/key}
@@ -0,0 +1,6 @@
1
+ interface StudioRootProps {
2
+ lobbUrl?: string;
3
+ }
4
+ declare const StudioRoot: import("svelte").Component<StudioRootProps, {}, "">;
5
+ type StudioRoot = ReturnType<typeof StudioRoot>;
6
+ export default StudioRoot;
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
2
  import * as Breadcrumb from "./ui/breadcrumb";
3
3
  import { mediaQueries } from "../utils";
4
- import { location } from "@wjfe/n-savant";
4
+ import { page } from "$app/state";
5
+ import { goto } from "$app/navigation";
5
6
 
6
7
  const isSmall = $derived(!mediaQueries.sm.current);
7
8
  const pathNames = $derived(
8
- location.url.pathname
9
+ page.url.pathname
9
10
  .replace("/studio", "")
10
11
  .split("/")
11
12
  .filter((el: any) => el !== "")
@@ -26,7 +27,7 @@
26
27
  {:else}
27
28
  <Breadcrumb.Link
28
29
  class="cursor-pointer"
29
- onclick={() => location.navigate("/studio")}
30
+ onclick={() => goto("/studio")}
30
31
  >
31
32
  Home
32
33
  </Breadcrumb.Link>
@@ -45,7 +46,7 @@
45
46
  <Breadcrumb.Link
46
47
  class="cursor-pointer"
47
48
  onclick={() =>
48
- location.navigate(`/studio/${currentFullPaths}`)}
49
+ goto(`/studio/${currentFullPaths}`)}
49
50
  >
50
51
  {path}
51
52
  </Breadcrumb.Link>
@@ -132,7 +132,7 @@
132
132
  });
133
133
  </script>
134
134
 
135
- <div class={cn('resize-y rounded-md border bg-muted/30 h-60', className)}>
135
+ <div class={cn('resize-y rounded-md border bg-muted-soft h-60', className)}>
136
136
  <div bind:this={editorContainer} class="h-full w-full pl-2" />
137
137
  </div>
138
138
 
@@ -2,9 +2,9 @@
2
2
  import CheckIcon from "@lucide/svelte/icons/check";
3
3
  import ChevronsUpDownIcon from "@lucide/svelte/icons/chevrons-up-down";
4
4
  import { tick } from "svelte";
5
- import * as Command from "../components/ui/command/index.js";
6
- import * as Popover from "../components/ui/popover/index.js";
7
- import { Button } from "../components/ui/button/index.js";
5
+ import * as Command from "./ui/command/index.js";
6
+ import * as Popover from "./ui/popover/index.js";
7
+ import { Button } from "./ui/button/index.js";
8
8
  import { cn } from "../utils.js";
9
9
  import type { HTMLButtonAttributes } from "svelte/elements";
10
10
 
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import * as AlertDialog from "../../components/ui/alert-dialog/index";
2
+ import * as AlertDialog from "../ui/alert-dialog/index";
3
3
  import { Check, X } from "lucide-svelte";
4
4
  import Button from "../ui/button/button.svelte";
5
5
 
@@ -4,12 +4,6 @@
4
4
  recordId: string | number;
5
5
  }
6
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
7
  </script>
14
8
 
15
9
  <script lang="ts">
@@ -20,19 +14,21 @@
20
14
  import Table, { type TableProps } from "./table.svelte";
21
15
  import { getCollectionColumns, getCollectionParamsFields } from "./utils";
22
16
  import { Pencil, Trash, Unlink } from "lucide-svelte";
23
- import * as icons from "lucide-svelte";
24
17
  import ListViewChildren from "./listViewChildren.svelte";
25
18
  import FieldCell from "./fieldCell.svelte";
26
19
  import Skeleton from "../ui/skeleton/skeleton.svelte";
27
20
  import Button from "../ui/button/button.svelte";
28
21
  import { showDialog } from "../../actions";
29
22
  import UpdateDetailViewButton from "../detailView/update/updateDetailViewButton.svelte";
30
- import { emitEvent } from "../../eventSystem";
31
23
  import type { Snippet } from "svelte";
24
+ import type { Changes, ChildrenChanges } from "../detailView/utils";
32
25
  import ExtensionsComponents from "../extensionsComponents.svelte";
33
26
  import { getExtensionUtils, loadExtensionComponents } from "../../extensions/extensionUtils";
27
+ import { emitEvent } from "../../eventSystem";
28
+ import { onMount } from "svelte";
34
29
  import Tabs from "./dataTableTabs.svelte";
35
30
  import { fade } from "svelte/transition";
31
+ import type { CollectionTab } from "../../store.types";
36
32
 
37
33
  const { lobb, ctx } = getStudioContext();
38
34
 
@@ -41,13 +37,13 @@
41
37
  filter?: any;
42
38
  searchParams?: Record<string, any>;
43
39
  parentContext?: ParentContext;
44
- onOperation?: (op: RecordOperation) => void;
40
+ changes?: ChildrenChanges;
45
41
  showHeader?: boolean;
46
42
  showFooter?: boolean;
47
43
  showImport?: boolean;
48
- unifiedBgColor?: "bg-muted/30" | "bg-background";
49
44
  showDelete?: boolean;
50
45
  tableProps?: Partial<TableProps>;
46
+ tabs?: CollectionTab[];
51
47
  headerLeft?: Snippet<[]>;
52
48
  }
53
49
 
@@ -56,16 +52,70 @@
56
52
  filter,
57
53
  searchParams,
58
54
  parentContext,
59
- onOperation,
55
+ changes = $bindable<ChildrenChanges | undefined>(undefined),
60
56
  showHeader = true,
61
57
  showFooter = true,
62
58
  showImport = true,
63
- unifiedBgColor,
64
59
  showDelete = false,
65
60
  tableProps,
61
+ tabs,
66
62
  headerLeft,
67
63
  }: Props = $props();
68
64
 
65
+ // Gate row/header buttons by the current user's permissions:
66
+ // - showUpdate → per-row edit button
67
+ // - showCreate → header's Create + Import buttons (passed to Header)
68
+ let showUpdate = $state(false);
69
+ let showCreate = $state(false);
70
+ onMount(async () => {
71
+ const [update, create] = await Promise.all([
72
+ emitEvent({ lobb, ctx }, "auth.canAccess", { collection: collectionName, action: "update" }),
73
+ emitEvent({ lobb, ctx }, "auth.canAccess", { collection: collectionName, action: "create" }),
74
+ ]);
75
+ showUpdate = update === true;
76
+ showCreate = create === true;
77
+ });
78
+
79
+ function getOrCreateUpdatedSlot(recordId: string): Changes | undefined {
80
+ if (!changes) return undefined;
81
+ let slot = changes.updated.find((u) => String(u.id) === String(recordId));
82
+ if (!slot) {
83
+ slot = { id: recordId, data: {}, children: {} };
84
+ changes.updated.push(slot);
85
+ }
86
+ return slot;
87
+ }
88
+
89
+ // Derives the displayed rows by applying changes on top of server data.
90
+ // This is the single place responsible for optimistic UI — no handler touches data directly.
91
+ const data = $derived.by(() => {
92
+ if (!changes) return serverData;
93
+
94
+ const removedIds = new Set([
95
+ ...changes.deleted.map((r) => String(r.id)),
96
+ ...changes.unlinked.map((r) => String(r.id)),
97
+ ]);
98
+
99
+ let result = serverData.filter((r: any) => !removedIds.has(String(r.id)));
100
+
101
+ result = result.map((r: any) => {
102
+ const update = changes.updated.find((u) => String(u.id) === String(r.id));
103
+ return update && Object.keys(update.data).length ? { ...r, ...update.data } : r;
104
+ });
105
+
106
+ for (const record of changes.linked) {
107
+ if (!result.some((r: any) => String(r.id) === String(record.id))) {
108
+ result = [...result, record];
109
+ }
110
+ }
111
+
112
+ for (const item of changes.created) {
113
+ result = [...result, { ...item.data, _pending: true }];
114
+ }
115
+
116
+ return result;
117
+ });
118
+
69
119
  const hasRowActions = $derived(
70
120
  loadExtensionComponents(ctx, "listView.entry.actions", undefined, { collectionName }).length > 0
71
121
  );
@@ -89,7 +139,7 @@
89
139
 
90
140
  let selectedRecords = $state([]);
91
141
  let totalCount = $state(0);
92
- let data: TableProps["data"] = $state([]);
142
+ let serverData: TableProps["data"] = $state([]);
93
143
  let loading = $state(true);
94
144
  const columns: TableProps["columns"] = $state(
95
145
  getCollectionColumns(ctx, collectionName),
@@ -108,7 +158,6 @@
108
158
 
109
159
  async function loadData(params: any) {
110
160
  loading = true;
111
- // parsing sort before sending the request
112
161
  const paramsCopy = $state.snapshot(params);
113
162
  const sort: TableProps["sort"] = paramsCopy.sort;
114
163
  const sortStrings: string[] = [];
@@ -118,85 +167,48 @@
118
167
  }
119
168
  }
120
169
  paramsCopy.sort = sortStrings.join(",");
121
-
122
- // sending the request
123
170
  const response = await lobb.findAll(collectionName, paramsCopy);
124
171
  const res = await response.json();
125
-
126
- data = res.data;
172
+ serverData = res.data;
127
173
  totalCount = res.meta.totalCount;
128
-
129
174
  loading = false;
130
175
  }
131
176
 
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
-
146
177
  async function handleDelete(entryId: string) {
147
178
  const result = await showDialog("Are you sure?", "This will permanently delete the record.");
148
- if (result) {
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
- }
179
+ if (!result) return;
180
+ if (changes) {
181
+ const record = data.find((r: any) => String(r.id) === String(entryId));
182
+ if (record) changes.deleted.push($state.snapshot(record));
183
+ } else if (parentContext) {
184
+ serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
185
+ await lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), {}, { [collectionName]: { delete: [entryId] } });
186
+ params = { ...params };
187
+ } else {
188
+ serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
189
+ await lobb.deleteOne(collectionName, entryId);
190
+ params = { ...params };
158
191
  }
159
192
  }
160
193
 
161
194
  async function handleUnlink(entryId: string) {
162
195
  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
- }
196
+ if (!result) return;
197
+ if (changes) {
198
+ const record = data.find((r: any) => String(r.id) === String(entryId));
199
+ if (record) changes.unlinked.push($state.snapshot(record));
200
+ } else {
201
+ serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
202
+ await lobb.updateOne(parentContext!.collectionName, String(parentContext!.recordId), {}, { [collectionName]: { unlink: [entryId] } });
203
+ params = { ...params };
170
204
  }
171
205
  }
172
206
 
173
- async function getWorkflowTools(
174
- entry: Record<string, any>,
175
- ): Promise<any[]> {
176
- // TODO: instead of firing the events like this. get them all the fire them one by one to get their results
177
- const eventResult = await emitEvent(
178
- { lobb, ctx },
179
- "studio.collections.listView.tools",
180
- {
181
- collectionName,
182
- entry,
183
- },
184
- );
185
-
186
- if (eventResult) {
187
- return eventResult.tools;
188
- }
189
-
190
- return [];
191
- }
192
207
  </script>
193
208
 
194
209
  <div
195
210
  bind:clientWidth={dataTableContainerWidth}
196
- class="
197
- flex flex-col overflow-auto h-full w-full
198
- {unifiedBgColor ? unifiedBgColor : ''}
199
- "
211
+ class="flex flex-col overflow-auto h-full w-full"
200
212
  >
201
213
  {#snippet rowActionsSnippet(entry: Record<string, any>)}
202
214
  <ExtensionsComponents
@@ -209,13 +221,21 @@
209
221
  {/snippet}
210
222
 
211
223
  {#if showHeader}
212
- <Header bind:params {collectionName} bind:selectedRecords {parentContext} {showImport} onOperation={onOperation ? applyOperation : undefined}>
224
+ <Header
225
+ bind:params
226
+ {collectionName}
227
+ bind:selectedRecords
228
+ {showImport}
229
+ {showCreate}
230
+ {parentContext}
231
+ {changes}
232
+ >
213
233
  {#snippet left()}
214
234
  {@render headerLeft?.()}
215
235
  {/snippet}
216
236
  </Header>
217
237
  {/if}
218
- <Tabs {collectionName} {filter} bind:activeTabFilter />
238
+ <Tabs {collectionName} {filter} {tabs} bind:activeTabFilter />
219
239
  <div class="relative flex-1 overflow-auto w-full">
220
240
  {#key activeTabFilter}
221
241
  <div class="h-full w-full" in:fade={{ duration: 120 }}>
@@ -235,21 +255,23 @@
235
255
  showLastColumnBorder={true}
236
256
  bind:sort={params.sort}
237
257
  bind:selectedRecords
238
- {unifiedBgColor}
239
258
  bind:tableWidth={dataTableWidth}
240
259
  {...tableProps}
241
260
  rowActions={hasRowActions ? rowActionsSnippet : undefined}>
242
261
  {#snippet tools(entry)}
243
- <UpdateDetailViewButton
244
- {collectionName}
245
- recordId={entry.id}
246
- variant="ghost"
247
- class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
248
- Icon={Pencil}
249
- onSuccessfullSave={async () => {
250
- params = { ...params };
251
- }}
252
- ></UpdateDetailViewButton>
262
+ {#if showUpdate}
263
+ <UpdateDetailViewButton
264
+ {collectionName}
265
+ recordId={entry.id}
266
+ variant="ghost"
267
+ class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
268
+ Icon={Pencil}
269
+ changes={getOrCreateUpdatedSlot(String(entry.id))}
270
+ onSuccessfullSave={async () => {
271
+ params = { ...params };
272
+ }}
273
+ ></UpdateDetailViewButton>
274
+ {/if}
253
275
  {#if parentContext}
254
276
  <Button
255
277
  class="h-6 w-6 text-muted-foreground hover:bg-transparent"
@@ -270,20 +292,6 @@
270
292
  title="Delete permanently"
271
293
  ></Button>
272
294
  {/if}
273
- {#await getWorkflowTools($state.snapshot(entry))}
274
- <div></div>
275
- {:then workflowTools}
276
- {#each workflowTools as workflowTool}
277
- <Button
278
- variant="ghost"
279
- class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
280
- Icon={icons[
281
- workflowTool.icon as keyof typeof icons
282
- ]}
283
- onclick={workflowTool.onclick}
284
- ></Button>
285
- {/each}
286
- {/await}
287
295
  {/snippet}
288
296
  {#snippet overrideCell(value, column, entry)}
289
297
  <FieldCell
@@ -301,7 +309,6 @@
301
309
  width={dataTableWidth > dataTableContainerWidth
302
310
  ? dataTableContainerWidth
303
311
  : dataTableWidth}
304
- unifiedBgColor={unifiedBgColor ?? "bg-background"}
305
312
  />
306
313
  {/snippet}
307
314
  </Table>
@@ -2,39 +2,24 @@ export interface ParentContext {
2
2
  collectionName: string;
3
3
  recordId: string | number;
4
4
  }
5
- export type RecordOperation = {
6
- type: "link";
7
- record: any;
8
- } | {
9
- type: "unlink";
10
- id: string | number;
11
- } | {
12
- type: "delete";
13
- id: string | number;
14
- } | {
15
- type: "create";
16
- record: any;
17
- } | {
18
- type: "update";
19
- id: string | number;
20
- data: any;
21
- };
22
5
  import { type TableProps } from "./table.svelte";
23
6
  import type { Snippet } from "svelte";
7
+ import type { ChildrenChanges } from "../detailView/utils";
8
+ import type { CollectionTab } from "../../store.types";
24
9
  interface Props {
25
10
  collectionName: string;
26
11
  filter?: any;
27
12
  searchParams?: Record<string, any>;
28
13
  parentContext?: ParentContext;
29
- onOperation?: (op: RecordOperation) => void;
14
+ changes?: ChildrenChanges;
30
15
  showHeader?: boolean;
31
16
  showFooter?: boolean;
32
17
  showImport?: boolean;
33
- unifiedBgColor?: "bg-muted/30" | "bg-background";
34
18
  showDelete?: boolean;
35
19
  tableProps?: Partial<TableProps>;
20
+ tabs?: CollectionTab[];
36
21
  headerLeft?: Snippet<[]>;
37
22
  }
38
- declare const DataTable: import("svelte").Component<Props, {}, "">;
23
+ declare const DataTable: import("svelte").Component<Props, {}, "changes">;
39
24
  type DataTable = ReturnType<typeof DataTable>;
40
25
  export default DataTable;
@@ -1,17 +1,19 @@
1
1
  <script lang="ts">
2
2
  import { getStudioContext } from "../../context";
3
+ import type { CollectionTab } from "../../store.types";
3
4
 
4
5
  const { lobb, ctx } = getStudioContext();
5
6
 
6
7
  interface Props {
7
8
  collectionName: string;
8
9
  filter?: any;
10
+ tabs?: CollectionTab[];
9
11
  activeTabFilter?: any;
10
12
  }
11
13
 
12
- let { collectionName, filter, activeTabFilter = $bindable() }: Props = $props();
14
+ let { collectionName, filter, tabs: tabsProp, activeTabFilter = $bindable() }: Props = $props();
13
15
 
14
- const tabs = ctx.meta.collections[collectionName].ui?.tabs;
16
+ const tabs: CollectionTab[] | undefined = $derived(tabsProp ?? ctx.meta.collections[collectionName].ui?.tabs);
15
17
  let activeTab = $state<string | null>(null);
16
18
  let tabCounts = $state<Record<string, number>>({});
17
19