@lobb-js/studio 0.12.0 → 0.13.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.
@@ -17,6 +17,7 @@
17
17
  import type { Snippet } from "svelte";
18
18
  import ExtensionsComponents from "../extensionsComponents.svelte";
19
19
  import { getExtensionUtils, loadExtensionComponents } from "../../extensions/extensionUtils";
20
+ import Tabs from "./dataTableTabs.svelte";
20
21
 
21
22
  const { lobb, ctx } = getStudioContext();
22
23
 
@@ -46,15 +47,22 @@
46
47
  loadExtensionComponents(ctx, "listView.entry.actions", undefined, { collectionName }).length > 0
47
48
  );
48
49
 
50
+ let activeTabFilter = $state<any>(undefined);
51
+
49
52
  const fields = getCollectionParamsFields(ctx, collectionName);
50
53
  let params = $state({
51
54
  fields: fields,
52
- filter: filter ?? {},
55
+ filter: { ...filter, ...activeTabFilter },
53
56
  sort: {},
54
57
  limit: "100",
55
58
  page: 1,
56
59
  });
57
60
 
61
+ $effect(() => {
62
+ const tabFilter = activeTabFilter;
63
+ params.filter = { ...filter, ...tabFilter };
64
+ });
65
+
58
66
  let selectedRecords = $state([]);
59
67
  let totalCount = $state(0);
60
68
  let data: TableProps["data"] = $state([]);
@@ -146,6 +154,7 @@
146
154
  />
147
155
  {/snippet}
148
156
 
157
+ <Tabs {collectionName} {filter} bind:activeTabFilter />
149
158
  {#if showHeader}
150
159
  <Header bind:params {collectionName} bind:selectedRecords>
151
160
  {#snippet left()}
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import { getStudioContext } from "../../context";
3
+
4
+ const { lobb, ctx } = getStudioContext();
5
+
6
+ interface Props {
7
+ collectionName: string;
8
+ filter?: any;
9
+ activeTabFilter?: any;
10
+ }
11
+
12
+ let { collectionName, filter, activeTabFilter = $bindable() }: Props = $props();
13
+
14
+ const tabs = ctx.meta.collections[collectionName].ui?.tabs;
15
+ let activeTab = $state<string | null>(null);
16
+ let tabCounts = $state<Record<string, number>>({});
17
+
18
+ async function loadTabCounts() {
19
+ if (!tabs) return;
20
+ const results = await Promise.all(
21
+ tabs.map(async (tab) => {
22
+ const res = await lobb.findAll(collectionName, { filter: { ...filter, ...tab.filter }, limit: 1 });
23
+ const json = await res.json();
24
+ return { key: tab.id ?? tab.label, count: json.meta.totalCount };
25
+ })
26
+ );
27
+ tabCounts = Object.fromEntries(results.map(({ key, count }) => [key, count]));
28
+ }
29
+
30
+ $effect(() => {
31
+ loadTabCounts();
32
+ });
33
+
34
+ $effect(() => {
35
+ if (!tabs) return;
36
+ const key = (t: typeof tabs[0]) => t.id ?? t.label;
37
+ const tab = activeTab
38
+ ? tabs.find((t) => key(t) === activeTab)
39
+ : tabs.find((t) => t.default) ?? tabs[0];
40
+ activeTabFilter = tab?.filter;
41
+ });
42
+ </script>
43
+
44
+ {#if tabs}
45
+ <div class="flex items-center gap-1 px-3 py-1.5 border-b shrink-0 bg-background">
46
+ {#each tabs as tab}
47
+ {@const key = tab.id ?? tab.label}
48
+ {@const isActive = activeTab ? activeTab === key : (tab.default ?? tabs[0] === tab)}
49
+ <button
50
+ onclick={() => activeTab = key}
51
+ class="inline-flex items-center px-2.5 py-1 text-[11px] rounded-md transition-colors
52
+ {isActive
53
+ ? 'bg-muted text-foreground font-medium'
54
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted/50'}"
55
+ >
56
+ {tab.label}
57
+ {#if tabCounts[key] !== undefined}
58
+ <span class="ml-1.5 px-1.5 py-0.5 rounded-full text-[10px] {isActive ? 'bg-muted-foreground/20 text-foreground' : 'bg-muted text-muted-foreground'}">
59
+ {tabCounts[key]}
60
+ </span>
61
+ {/if}
62
+ </button>
63
+ {/each}
64
+ </div>
65
+ {/if}
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ collectionName: string;
3
+ filter?: any;
4
+ activeTabFilter?: any;
5
+ }
6
+ declare const DataTableTabs: import("svelte").Component<Props, {}, "activeTabFilter">;
7
+ type DataTableTabs = ReturnType<typeof DataTableTabs>;
8
+ export default DataTableTabs;
@@ -1,30 +1,33 @@
1
- <script>
2
- import { CircleSlash2 } from "lucide-svelte";
1
+ <script lang="ts">
2
+ import { CircleSlash2, Zap } from "lucide-svelte";
3
3
  import DataTable from "../../../components/dataTable/dataTable.svelte";
4
4
  import SidebarTrigger from "../../../components/sidebar/sidebarTrigger.svelte";
5
5
  import { getStudioContext } from "../../../context";
6
6
  import Singletone from "../../../components/singletone.svelte";
7
- import { getExtensionUtils } from "../../../extensions/extensionUtils";
8
- import ExtensionsComponents from "../../../components/extensionsComponents.svelte";
9
7
 
10
- const { ctx, lobb } = getStudioContext();
8
+ const { ctx } = getStudioContext();
11
9
 
12
10
  let { collectionName } = $props();
13
11
  let isSingletonCollection = $derived(ctx.meta.collections[collectionName].singleton);
12
+ let isVirtualCollection = $derived(ctx.meta.collections[collectionName].virtual);
14
13
 
15
14
  let containerWidth = $state();
16
15
  </script>
17
16
 
18
17
  <div bind:clientWidth={containerWidth} class="h-full">
19
18
  {#if collectionName}
20
- <!-- TODO: add support in here for the views for each collection view -->
21
- <!-- {#if true}
22
- <ExtensionsComponents
23
- name="studio.listView"
24
- utils={getExtensionUtils(lobb, ctx)}
25
- ></ExtensionsComponents>
26
- {:else if isSingletonCollection} -->
27
- {#if isSingletonCollection}
19
+ {#if isVirtualCollection}
20
+ <div class="relative flex h-full w-full flex-col items-center justify-center gap-4 text-muted-foreground">
21
+ <Zap class="opacity-50" size="50" />
22
+ <div class="flex flex-col items-center justify-center">
23
+ <div>Virtual collection</div>
24
+ <div class="text-xs">This collection has no database table. It exists only as an API endpoint for workflows to intercept.</div>
25
+ </div>
26
+ <div class="absolute top-0 left-0 p-2.5">
27
+ <SidebarTrigger />
28
+ </div>
29
+ </div>
30
+ {:else if isSingletonCollection}
28
31
  <Singletone collectionName={collectionName} />
29
32
  {:else}
30
33
  <DataTable
@@ -1,11 +1,5 @@
1
- export default Collection;
2
- type Collection = {
3
- $on?(type: string, callback: (e: any) => void): () => void;
4
- $set?(props: Partial<$$ComponentProps>): void;
5
- };
6
1
  declare const Collection: import("svelte").Component<{
7
2
  collectionName: any;
8
3
  }, {}, "">;
9
- type $$ComponentProps = {
10
- collectionName: any;
11
- };
4
+ type Collection = ReturnType<typeof Collection>;
5
+ export default Collection;
@@ -1,9 +1,19 @@
1
1
  import type { Extension } from "../extensions/extension.types";
2
+ interface CollectionTab {
3
+ id?: string;
4
+ label: string;
5
+ filter?: Record<string, any>;
6
+ default?: boolean;
7
+ }
2
8
  interface Collection {
3
9
  category: string;
4
10
  owner: string;
5
11
  fields: Record<string, any>;
6
12
  singleton: boolean;
13
+ virtual?: boolean;
14
+ ui?: {
15
+ tabs?: CollectionTab[];
16
+ };
7
17
  }
8
18
  type Collections = Record<string, Collection>;
9
19
  interface Meta {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lobb-js/studio",
3
3
  "license": "UNLICENSED",
4
- "version": "0.12.0",
4
+ "version": "0.13.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.18.0",
45
+ "@lobb-js/core": "^0.19.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",
@@ -17,6 +17,7 @@
17
17
  import type { Snippet } from "svelte";
18
18
  import ExtensionsComponents from "../extensionsComponents.svelte";
19
19
  import { getExtensionUtils, loadExtensionComponents } from "../../extensions/extensionUtils";
20
+ import Tabs from "./dataTableTabs.svelte";
20
21
 
21
22
  const { lobb, ctx } = getStudioContext();
22
23
 
@@ -46,15 +47,22 @@
46
47
  loadExtensionComponents(ctx, "listView.entry.actions", undefined, { collectionName }).length > 0
47
48
  );
48
49
 
50
+ let activeTabFilter = $state<any>(undefined);
51
+
49
52
  const fields = getCollectionParamsFields(ctx, collectionName);
50
53
  let params = $state({
51
54
  fields: fields,
52
- filter: filter ?? {},
55
+ filter: { ...filter, ...activeTabFilter },
53
56
  sort: {},
54
57
  limit: "100",
55
58
  page: 1,
56
59
  });
57
60
 
61
+ $effect(() => {
62
+ const tabFilter = activeTabFilter;
63
+ params.filter = { ...filter, ...tabFilter };
64
+ });
65
+
58
66
  let selectedRecords = $state([]);
59
67
  let totalCount = $state(0);
60
68
  let data: TableProps["data"] = $state([]);
@@ -146,6 +154,7 @@
146
154
  />
147
155
  {/snippet}
148
156
 
157
+ <Tabs {collectionName} {filter} bind:activeTabFilter />
149
158
  {#if showHeader}
150
159
  <Header bind:params {collectionName} bind:selectedRecords>
151
160
  {#snippet left()}
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import { getStudioContext } from "../../context";
3
+
4
+ const { lobb, ctx } = getStudioContext();
5
+
6
+ interface Props {
7
+ collectionName: string;
8
+ filter?: any;
9
+ activeTabFilter?: any;
10
+ }
11
+
12
+ let { collectionName, filter, activeTabFilter = $bindable() }: Props = $props();
13
+
14
+ const tabs = ctx.meta.collections[collectionName].ui?.tabs;
15
+ let activeTab = $state<string | null>(null);
16
+ let tabCounts = $state<Record<string, number>>({});
17
+
18
+ async function loadTabCounts() {
19
+ if (!tabs) return;
20
+ const results = await Promise.all(
21
+ tabs.map(async (tab) => {
22
+ const res = await lobb.findAll(collectionName, { filter: { ...filter, ...tab.filter }, limit: 1 });
23
+ const json = await res.json();
24
+ return { key: tab.id ?? tab.label, count: json.meta.totalCount };
25
+ })
26
+ );
27
+ tabCounts = Object.fromEntries(results.map(({ key, count }) => [key, count]));
28
+ }
29
+
30
+ $effect(() => {
31
+ loadTabCounts();
32
+ });
33
+
34
+ $effect(() => {
35
+ if (!tabs) return;
36
+ const key = (t: typeof tabs[0]) => t.id ?? t.label;
37
+ const tab = activeTab
38
+ ? tabs.find((t) => key(t) === activeTab)
39
+ : tabs.find((t) => t.default) ?? tabs[0];
40
+ activeTabFilter = tab?.filter;
41
+ });
42
+ </script>
43
+
44
+ {#if tabs}
45
+ <div class="flex items-center gap-1 px-3 py-1.5 border-b shrink-0 bg-background">
46
+ {#each tabs as tab}
47
+ {@const key = tab.id ?? tab.label}
48
+ {@const isActive = activeTab ? activeTab === key : (tab.default ?? tabs[0] === tab)}
49
+ <button
50
+ onclick={() => activeTab = key}
51
+ class="inline-flex items-center px-2.5 py-1 text-[11px] rounded-md transition-colors
52
+ {isActive
53
+ ? 'bg-muted text-foreground font-medium'
54
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted/50'}"
55
+ >
56
+ {tab.label}
57
+ {#if tabCounts[key] !== undefined}
58
+ <span class="ml-1.5 px-1.5 py-0.5 rounded-full text-[10px] {isActive ? 'bg-muted-foreground/20 text-foreground' : 'bg-muted text-muted-foreground'}">
59
+ {tabCounts[key]}
60
+ </span>
61
+ {/if}
62
+ </button>
63
+ {/each}
64
+ </div>
65
+ {/if}
@@ -1,30 +1,33 @@
1
- <script>
2
- import { CircleSlash2 } from "lucide-svelte";
1
+ <script lang="ts">
2
+ import { CircleSlash2, Zap } from "lucide-svelte";
3
3
  import DataTable from "../../../components/dataTable/dataTable.svelte";
4
4
  import SidebarTrigger from "../../../components/sidebar/sidebarTrigger.svelte";
5
5
  import { getStudioContext } from "../../../context";
6
6
  import Singletone from "../../../components/singletone.svelte";
7
- import { getExtensionUtils } from "../../../extensions/extensionUtils";
8
- import ExtensionsComponents from "../../../components/extensionsComponents.svelte";
9
7
 
10
- const { ctx, lobb } = getStudioContext();
8
+ const { ctx } = getStudioContext();
11
9
 
12
10
  let { collectionName } = $props();
13
11
  let isSingletonCollection = $derived(ctx.meta.collections[collectionName].singleton);
12
+ let isVirtualCollection = $derived(ctx.meta.collections[collectionName].virtual);
14
13
 
15
14
  let containerWidth = $state();
16
15
  </script>
17
16
 
18
17
  <div bind:clientWidth={containerWidth} class="h-full">
19
18
  {#if collectionName}
20
- <!-- TODO: add support in here for the views for each collection view -->
21
- <!-- {#if true}
22
- <ExtensionsComponents
23
- name="studio.listView"
24
- utils={getExtensionUtils(lobb, ctx)}
25
- ></ExtensionsComponents>
26
- {:else if isSingletonCollection} -->
27
- {#if isSingletonCollection}
19
+ {#if isVirtualCollection}
20
+ <div class="relative flex h-full w-full flex-col items-center justify-center gap-4 text-muted-foreground">
21
+ <Zap class="opacity-50" size="50" />
22
+ <div class="flex flex-col items-center justify-center">
23
+ <div>Virtual collection</div>
24
+ <div class="text-xs">This collection has no database table. It exists only as an API endpoint for workflows to intercept.</div>
25
+ </div>
26
+ <div class="absolute top-0 left-0 p-2.5">
27
+ <SidebarTrigger />
28
+ </div>
29
+ </div>
30
+ {:else if isSingletonCollection}
28
31
  <Singletone collectionName={collectionName} />
29
32
  {:else}
30
33
  <DataTable
@@ -1,10 +1,21 @@
1
1
  import type { Extension } from "../extensions/extension.types";
2
2
 
3
+ interface CollectionTab {
4
+ id?: string;
5
+ label: string;
6
+ filter?: Record<string, any>;
7
+ default?: boolean;
8
+ }
9
+
3
10
  interface Collection {
4
11
  category: string;
5
12
  owner: string;
6
13
  fields: Record<string, any>;
7
14
  singleton: boolean;
15
+ virtual?: boolean;
16
+ ui?: {
17
+ tabs?: CollectionTab[];
18
+ };
8
19
  }
9
20
  type Collections = Record<string, Collection>;
10
21