@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.
- package/dist/components/dataTable/dataTable.svelte +10 -1
- package/dist/components/dataTable/dataTableTabs.svelte +65 -0
- package/dist/components/dataTable/dataTableTabs.svelte.d.ts +8 -0
- package/dist/components/routes/collections/collection.svelte +16 -13
- package/dist/components/routes/collections/collection.svelte.d.ts +2 -8
- package/dist/store.types.d.ts +10 -0
- package/package.json +2 -2
- package/src/lib/components/dataTable/dataTable.svelte +10 -1
- package/src/lib/components/dataTable/dataTableTabs.svelte +65 -0
- package/src/lib/components/routes/collections/collection.svelte +16 -13
- package/src/lib/store.types.ts +11 -0
|
@@ -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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
};
|
|
4
|
+
type Collection = ReturnType<typeof Collection>;
|
|
5
|
+
export default Collection;
|
package/dist/store.types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
package/src/lib/store.types.ts
CHANGED
|
@@ -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
|
|