@lobb-js/studio 0.11.1 → 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 +27 -2
- package/dist/components/dataTable/dataTableTabs.svelte +65 -0
- package/dist/components/dataTable/dataTableTabs.svelte.d.ts +8 -0
- package/dist/components/dataTable/table.svelte +28 -4
- package/dist/components/dataTable/table.svelte.d.ts +1 -0
- package/dist/components/extensionsComponents.svelte +1 -1
- package/dist/components/routes/collections/collection.svelte +16 -13
- package/dist/components/routes/collections/collection.svelte.d.ts +2 -8
- package/dist/components/ui/button/button.svelte +1 -0
- package/dist/extensions/extension.types.d.ts +6 -1
- package/dist/extensions/extensionUtils.d.ts +1 -1
- package/dist/extensions/extensionUtils.js +9 -2
- package/dist/store.types.d.ts +10 -0
- package/package.json +2 -2
- package/src/lib/components/dataTable/dataTable.svelte +27 -2
- package/src/lib/components/dataTable/dataTableTabs.svelte +65 -0
- package/src/lib/components/dataTable/table.svelte +28 -4
- package/src/lib/components/extensionsComponents.svelte +1 -1
- package/src/lib/components/routes/collections/collection.svelte +16 -13
- package/src/lib/components/ui/button/button.svelte +1 -0
- package/src/lib/extensions/extension.types.ts +17 -1
- package/src/lib/extensions/extensionUtils.ts +8 -1
- package/src/lib/store.types.ts +11 -0
- package/dist/components/loadingTypesForMonacoEditor.d.ts +0 -7
- package/dist/components/loadingTypesForMonacoEditor.js +0 -72
- package/src/lib/components/loadingTypesForMonacoEditor.ts +0 -36
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
import UpdateDetailViewButton from "../detailView/update/updateDetailViewButton.svelte";
|
|
16
16
|
import { emitEvent } from "../../eventSystem";
|
|
17
17
|
import type { Snippet } from "svelte";
|
|
18
|
+
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
19
|
+
import { getExtensionUtils, loadExtensionComponents } from "../../extensions/extensionUtils";
|
|
20
|
+
import Tabs from "./dataTableTabs.svelte";
|
|
18
21
|
|
|
19
22
|
const { lobb, ctx } = getStudioContext();
|
|
20
23
|
|
|
@@ -40,15 +43,26 @@
|
|
|
40
43
|
headerLeft,
|
|
41
44
|
}: Props = $props();
|
|
42
45
|
|
|
46
|
+
const hasRowActions = $derived(
|
|
47
|
+
loadExtensionComponents(ctx, "listView.entry.actions", undefined, { collectionName }).length > 0
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
let activeTabFilter = $state<any>(undefined);
|
|
51
|
+
|
|
43
52
|
const fields = getCollectionParamsFields(ctx, collectionName);
|
|
44
53
|
let params = $state({
|
|
45
54
|
fields: fields,
|
|
46
|
-
filter: filter
|
|
55
|
+
filter: { ...filter, ...activeTabFilter },
|
|
47
56
|
sort: {},
|
|
48
57
|
limit: "100",
|
|
49
58
|
page: 1,
|
|
50
59
|
});
|
|
51
60
|
|
|
61
|
+
$effect(() => {
|
|
62
|
+
const tabFilter = activeTabFilter;
|
|
63
|
+
params.filter = { ...filter, ...tabFilter };
|
|
64
|
+
});
|
|
65
|
+
|
|
52
66
|
let selectedRecords = $state([]);
|
|
53
67
|
let totalCount = $state(0);
|
|
54
68
|
let data: TableProps["data"] = $state([]);
|
|
@@ -130,6 +144,17 @@
|
|
|
130
144
|
{unifiedBgColor ? unifiedBgColor : ''}
|
|
131
145
|
"
|
|
132
146
|
>
|
|
147
|
+
{#snippet rowActionsSnippet(entry: Record<string, any>)}
|
|
148
|
+
<ExtensionsComponents
|
|
149
|
+
name="listView.entry.actions"
|
|
150
|
+
utils={getExtensionUtils(lobb, ctx)}
|
|
151
|
+
{entry}
|
|
152
|
+
{collectionName}
|
|
153
|
+
refresh={() => { params = { ...params }; }}
|
|
154
|
+
/>
|
|
155
|
+
{/snippet}
|
|
156
|
+
|
|
157
|
+
<Tabs {collectionName} {filter} bind:activeTabFilter />
|
|
133
158
|
{#if showHeader}
|
|
134
159
|
<Header bind:params {collectionName} bind:selectedRecords>
|
|
135
160
|
{#snippet left()}
|
|
@@ -157,7 +182,7 @@
|
|
|
157
182
|
{unifiedBgColor}
|
|
158
183
|
bind:tableWidth={dataTableWidth}
|
|
159
184
|
{...tableProps}
|
|
160
|
-
|
|
185
|
+
rowActions={hasRowActions ? rowActionsSnippet : undefined}>
|
|
161
186
|
{#snippet tools(entry)}
|
|
162
187
|
<UpdateDetailViewButton
|
|
163
188
|
{collectionName}
|
|
@@ -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;
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
// snippets
|
|
33
33
|
overrideCell?: Snippet<[any, Column, Entry]>;
|
|
34
34
|
tools?: Snippet<[Entry, number]>;
|
|
35
|
+
rowActions?: Snippet<[Entry, number]>;
|
|
35
36
|
collapsible?: Snippet<[Entry, number]>;
|
|
36
37
|
|
|
37
38
|
// other
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
parentWidth,
|
|
73
74
|
overrideCell,
|
|
74
75
|
tools,
|
|
76
|
+
rowActions,
|
|
75
77
|
collapsible,
|
|
76
78
|
unifiedBgColor,
|
|
77
79
|
select,
|
|
@@ -82,6 +84,7 @@
|
|
|
82
84
|
|
|
83
85
|
// calculate columns count
|
|
84
86
|
const toolsExists = selectedRecords || tools ? 1 : 0;
|
|
87
|
+
const rowActionsExists = $derived(rowActions ? 1 : 0);
|
|
85
88
|
const columnsLength = columns.length + toolsExists;
|
|
86
89
|
|
|
87
90
|
// set table width
|
|
@@ -167,8 +170,7 @@
|
|
|
167
170
|
<div
|
|
168
171
|
style="
|
|
169
172
|
display: grid;
|
|
170
|
-
grid-template-columns: minmax(auto, 7.5rem) repeat({columnsLength -
|
|
171
|
-
1}, minmax(auto, 15rem));
|
|
173
|
+
grid-template-columns: minmax(auto, 7.5rem) repeat({columnsLength - 1}, minmax(auto, 15rem)){rowActionsExists ? ' minmax(auto, 7.5rem)' : ''};
|
|
172
174
|
grid-template-rows: 2.5rem;
|
|
173
175
|
"
|
|
174
176
|
>
|
|
@@ -179,7 +181,7 @@
|
|
|
179
181
|
sticky left-0 top-0 z-20
|
|
180
182
|
flex items-center p-2.5 text-xs h-10
|
|
181
183
|
border-r border-b gap-2
|
|
182
|
-
{unifiedBgColor ? unifiedBgColor : 'bg-muted
|
|
184
|
+
{unifiedBgColor ? unifiedBgColor : 'bg-muted'}
|
|
183
185
|
"
|
|
184
186
|
>
|
|
185
187
|
<!-- collapsable toggle -->
|
|
@@ -225,6 +227,16 @@
|
|
|
225
227
|
</div>
|
|
226
228
|
</button>
|
|
227
229
|
{/each}
|
|
230
|
+
{#if rowActions}
|
|
231
|
+
<div
|
|
232
|
+
class="
|
|
233
|
+
sticky top-0 right-0 z-20
|
|
234
|
+
flex items-center p-2.5 h-10
|
|
235
|
+
{unifiedBgColor ? unifiedBgColor : 'bg-muted'}
|
|
236
|
+
border-l border-b
|
|
237
|
+
"
|
|
238
|
+
></div>
|
|
239
|
+
{/if}
|
|
228
240
|
{#if Object.keys(data).length}
|
|
229
241
|
{#each data as entry, index}
|
|
230
242
|
{@const isDisabled = Boolean(entry.__disabled)}
|
|
@@ -292,9 +304,21 @@
|
|
|
292
304
|
{/if}
|
|
293
305
|
</div>
|
|
294
306
|
{/each}
|
|
307
|
+
{#if rowActions}
|
|
308
|
+
<div
|
|
309
|
+
class="
|
|
310
|
+
sticky right-0 z-10
|
|
311
|
+
flex items-center p-2.5 text-xs h-10
|
|
312
|
+
border-l gap-2
|
|
313
|
+
{unifiedBgColor ? unifiedBgColor : 'bg-background'}
|
|
314
|
+
"
|
|
315
|
+
>
|
|
316
|
+
{@render rowActions?.(entry, index)}
|
|
317
|
+
</div>
|
|
318
|
+
{/if}
|
|
295
319
|
<!-- nested data -->
|
|
296
320
|
<div
|
|
297
|
-
style="grid-column: span {columnsLength};"
|
|
321
|
+
style="grid-column: span {columnsLength + rowActionsExists};"
|
|
298
322
|
class="
|
|
299
323
|
{!showLastColumnBorder ? '' : 'border-r'}
|
|
300
324
|
{lastRow && !showLastRowBorder ? '' : 'border-b'}
|
|
@@ -20,6 +20,7 @@ export interface TableProps {
|
|
|
20
20
|
showLastColumnBorder?: boolean;
|
|
21
21
|
overrideCell?: Snippet<[any, Column, Entry]>;
|
|
22
22
|
tools?: Snippet<[Entry, number]>;
|
|
23
|
+
rowActions?: Snippet<[Entry, number]>;
|
|
23
24
|
collapsible?: Snippet<[Entry, number]>;
|
|
24
25
|
parentWidth?: number;
|
|
25
26
|
unifiedBgColor?: "bg-muted/30" | "bg-background";
|
|
@@ -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;
|
|
@@ -73,10 +73,15 @@ export interface ExtensionProps {
|
|
|
73
73
|
utils: ExtensionUtils;
|
|
74
74
|
[key: string]: any;
|
|
75
75
|
}
|
|
76
|
+
export type ExtensionComponentKey = `pages.${string}` | "studio.listView" | `dvFields.topRight.${string}.${string}` | `detailView.update.subRecords.${string}` | `detailView.create.subRecords.${string}` | `detailView.fields.topRight.${string}.${string}` | `detailView.fields.foreignKey.${string}` | `listView.entry.children.${string}` | `listView.entry.actions`;
|
|
77
|
+
export type ExtensionComponent = any | {
|
|
78
|
+
component: any;
|
|
79
|
+
when: (props: Record<string, any>) => boolean;
|
|
80
|
+
};
|
|
76
81
|
export interface Extension {
|
|
77
82
|
name: string;
|
|
78
83
|
onStartup?: (utils: ExtensionUtils) => Promise<void>;
|
|
79
|
-
components?: Record<
|
|
84
|
+
components?: Partial<Record<ExtensionComponentKey, ExtensionComponent>>;
|
|
80
85
|
dashboardNavs?: DashboardNavs;
|
|
81
86
|
}
|
|
82
87
|
export {};
|
|
@@ -4,6 +4,6 @@ import type { CTX } from "../store.types";
|
|
|
4
4
|
export declare function getComponents(): Components;
|
|
5
5
|
export declare function getExtensionUtils(lobb: LobbClient, ctx: CTX): ExtensionUtils;
|
|
6
6
|
export declare function loadExtensions(lobb: LobbClient, ctx: CTX, extensionMap?: Record<string, any>): Promise<Record<string, Extension>>;
|
|
7
|
-
export declare function loadExtensionComponents(ctx: CTX, name: string, filterByExtensions?: string[]): any[];
|
|
7
|
+
export declare function loadExtensionComponents(ctx: CTX, name: string, filterByExtensions?: string[], props?: Record<string, any>): any[];
|
|
8
8
|
export declare function executeExtensionsOnStartup(lobb: LobbClient, ctx: CTX): Promise<void>;
|
|
9
9
|
export declare function getDashboardNavs(ctx: CTX): DashboardNavs;
|
|
@@ -120,7 +120,7 @@ export function loadExtensions(lobb_1, ctx_1) {
|
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
|
-
export function loadExtensionComponents(ctx, name, filterByExtensions) {
|
|
123
|
+
export function loadExtensionComponents(ctx, name, filterByExtensions, props) {
|
|
124
124
|
var components = [];
|
|
125
125
|
for (var _i = 0, _a = Object.entries(ctx.extensions); _i < _a.length; _i++) {
|
|
126
126
|
var _b = _a[_i], extensionName = _b[0], extensionValue = _b[1];
|
|
@@ -131,7 +131,14 @@ export function loadExtensionComponents(ctx, name, filterByExtensions) {
|
|
|
131
131
|
for (var _c = 0, _d = Object.entries(extensionValue.components); _c < _d.length; _c++) {
|
|
132
132
|
var _e = _d[_c], componentName = _e[0], componentValue = _e[1];
|
|
133
133
|
if (name.startsWith(componentName)) {
|
|
134
|
-
|
|
134
|
+
if (componentValue && typeof componentValue === "object" && "component" in componentValue && "when" in componentValue) {
|
|
135
|
+
if (!props || componentValue.when(props)) {
|
|
136
|
+
components.push(componentValue.component);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
components.push(componentValue);
|
|
141
|
+
}
|
|
135
142
|
}
|
|
136
143
|
}
|
|
137
144
|
}
|
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",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
import UpdateDetailViewButton from "../detailView/update/updateDetailViewButton.svelte";
|
|
16
16
|
import { emitEvent } from "../../eventSystem";
|
|
17
17
|
import type { Snippet } from "svelte";
|
|
18
|
+
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
19
|
+
import { getExtensionUtils, loadExtensionComponents } from "../../extensions/extensionUtils";
|
|
20
|
+
import Tabs from "./dataTableTabs.svelte";
|
|
18
21
|
|
|
19
22
|
const { lobb, ctx } = getStudioContext();
|
|
20
23
|
|
|
@@ -40,15 +43,26 @@
|
|
|
40
43
|
headerLeft,
|
|
41
44
|
}: Props = $props();
|
|
42
45
|
|
|
46
|
+
const hasRowActions = $derived(
|
|
47
|
+
loadExtensionComponents(ctx, "listView.entry.actions", undefined, { collectionName }).length > 0
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
let activeTabFilter = $state<any>(undefined);
|
|
51
|
+
|
|
43
52
|
const fields = getCollectionParamsFields(ctx, collectionName);
|
|
44
53
|
let params = $state({
|
|
45
54
|
fields: fields,
|
|
46
|
-
filter: filter
|
|
55
|
+
filter: { ...filter, ...activeTabFilter },
|
|
47
56
|
sort: {},
|
|
48
57
|
limit: "100",
|
|
49
58
|
page: 1,
|
|
50
59
|
});
|
|
51
60
|
|
|
61
|
+
$effect(() => {
|
|
62
|
+
const tabFilter = activeTabFilter;
|
|
63
|
+
params.filter = { ...filter, ...tabFilter };
|
|
64
|
+
});
|
|
65
|
+
|
|
52
66
|
let selectedRecords = $state([]);
|
|
53
67
|
let totalCount = $state(0);
|
|
54
68
|
let data: TableProps["data"] = $state([]);
|
|
@@ -130,6 +144,17 @@
|
|
|
130
144
|
{unifiedBgColor ? unifiedBgColor : ''}
|
|
131
145
|
"
|
|
132
146
|
>
|
|
147
|
+
{#snippet rowActionsSnippet(entry: Record<string, any>)}
|
|
148
|
+
<ExtensionsComponents
|
|
149
|
+
name="listView.entry.actions"
|
|
150
|
+
utils={getExtensionUtils(lobb, ctx)}
|
|
151
|
+
{entry}
|
|
152
|
+
{collectionName}
|
|
153
|
+
refresh={() => { params = { ...params }; }}
|
|
154
|
+
/>
|
|
155
|
+
{/snippet}
|
|
156
|
+
|
|
157
|
+
<Tabs {collectionName} {filter} bind:activeTabFilter />
|
|
133
158
|
{#if showHeader}
|
|
134
159
|
<Header bind:params {collectionName} bind:selectedRecords>
|
|
135
160
|
{#snippet left()}
|
|
@@ -157,7 +182,7 @@
|
|
|
157
182
|
{unifiedBgColor}
|
|
158
183
|
bind:tableWidth={dataTableWidth}
|
|
159
184
|
{...tableProps}
|
|
160
|
-
|
|
185
|
+
rowActions={hasRowActions ? rowActionsSnippet : undefined}>
|
|
161
186
|
{#snippet tools(entry)}
|
|
162
187
|
<UpdateDetailViewButton
|
|
163
188
|
{collectionName}
|
|
@@ -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}
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
// snippets
|
|
33
33
|
overrideCell?: Snippet<[any, Column, Entry]>;
|
|
34
34
|
tools?: Snippet<[Entry, number]>;
|
|
35
|
+
rowActions?: Snippet<[Entry, number]>;
|
|
35
36
|
collapsible?: Snippet<[Entry, number]>;
|
|
36
37
|
|
|
37
38
|
// other
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
parentWidth,
|
|
73
74
|
overrideCell,
|
|
74
75
|
tools,
|
|
76
|
+
rowActions,
|
|
75
77
|
collapsible,
|
|
76
78
|
unifiedBgColor,
|
|
77
79
|
select,
|
|
@@ -82,6 +84,7 @@
|
|
|
82
84
|
|
|
83
85
|
// calculate columns count
|
|
84
86
|
const toolsExists = selectedRecords || tools ? 1 : 0;
|
|
87
|
+
const rowActionsExists = $derived(rowActions ? 1 : 0);
|
|
85
88
|
const columnsLength = columns.length + toolsExists;
|
|
86
89
|
|
|
87
90
|
// set table width
|
|
@@ -167,8 +170,7 @@
|
|
|
167
170
|
<div
|
|
168
171
|
style="
|
|
169
172
|
display: grid;
|
|
170
|
-
grid-template-columns: minmax(auto, 7.5rem) repeat({columnsLength -
|
|
171
|
-
1}, minmax(auto, 15rem));
|
|
173
|
+
grid-template-columns: minmax(auto, 7.5rem) repeat({columnsLength - 1}, minmax(auto, 15rem)){rowActionsExists ? ' minmax(auto, 7.5rem)' : ''};
|
|
172
174
|
grid-template-rows: 2.5rem;
|
|
173
175
|
"
|
|
174
176
|
>
|
|
@@ -179,7 +181,7 @@
|
|
|
179
181
|
sticky left-0 top-0 z-20
|
|
180
182
|
flex items-center p-2.5 text-xs h-10
|
|
181
183
|
border-r border-b gap-2
|
|
182
|
-
{unifiedBgColor ? unifiedBgColor : 'bg-muted
|
|
184
|
+
{unifiedBgColor ? unifiedBgColor : 'bg-muted'}
|
|
183
185
|
"
|
|
184
186
|
>
|
|
185
187
|
<!-- collapsable toggle -->
|
|
@@ -225,6 +227,16 @@
|
|
|
225
227
|
</div>
|
|
226
228
|
</button>
|
|
227
229
|
{/each}
|
|
230
|
+
{#if rowActions}
|
|
231
|
+
<div
|
|
232
|
+
class="
|
|
233
|
+
sticky top-0 right-0 z-20
|
|
234
|
+
flex items-center p-2.5 h-10
|
|
235
|
+
{unifiedBgColor ? unifiedBgColor : 'bg-muted'}
|
|
236
|
+
border-l border-b
|
|
237
|
+
"
|
|
238
|
+
></div>
|
|
239
|
+
{/if}
|
|
228
240
|
{#if Object.keys(data).length}
|
|
229
241
|
{#each data as entry, index}
|
|
230
242
|
{@const isDisabled = Boolean(entry.__disabled)}
|
|
@@ -292,9 +304,21 @@
|
|
|
292
304
|
{/if}
|
|
293
305
|
</div>
|
|
294
306
|
{/each}
|
|
307
|
+
{#if rowActions}
|
|
308
|
+
<div
|
|
309
|
+
class="
|
|
310
|
+
sticky right-0 z-10
|
|
311
|
+
flex items-center p-2.5 text-xs h-10
|
|
312
|
+
border-l gap-2
|
|
313
|
+
{unifiedBgColor ? unifiedBgColor : 'bg-background'}
|
|
314
|
+
"
|
|
315
|
+
>
|
|
316
|
+
{@render rowActions?.(entry, index)}
|
|
317
|
+
</div>
|
|
318
|
+
{/if}
|
|
295
319
|
<!-- nested data -->
|
|
296
320
|
<div
|
|
297
|
-
style="grid-column: span {columnsLength};"
|
|
321
|
+
style="grid-column: span {columnsLength + rowActionsExists};"
|
|
298
322
|
class="
|
|
299
323
|
{!showLastColumnBorder ? '' : 'border-r'}
|
|
300
324
|
{lastRow && !showLastRowBorder ? '' : 'border-b'}
|
|
@@ -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
|
|
@@ -83,10 +83,26 @@ export interface ExtensionProps {
|
|
|
83
83
|
[key: string]: any;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// TODO: instead of doing them this way. you can make the components generic and have static keys and we can pass some dynamic data so that we for example extend the list view
|
|
87
|
+
export type ExtensionComponentKey =
|
|
88
|
+
| `pages.${string}`
|
|
89
|
+
| "studio.listView"
|
|
90
|
+
| `dvFields.topRight.${string}.${string}`
|
|
91
|
+
| `detailView.update.subRecords.${string}`
|
|
92
|
+
| `detailView.create.subRecords.${string}`
|
|
93
|
+
| `detailView.fields.topRight.${string}.${string}`
|
|
94
|
+
| `detailView.fields.foreignKey.${string}`
|
|
95
|
+
| `listView.entry.children.${string}`
|
|
96
|
+
| `listView.entry.actions`;
|
|
97
|
+
|
|
98
|
+
export type ExtensionComponent =
|
|
99
|
+
| any
|
|
100
|
+
| { component: any; when: (props: Record<string, any>) => boolean };
|
|
101
|
+
|
|
86
102
|
// extension exported object
|
|
87
103
|
export interface Extension {
|
|
88
104
|
name: string;
|
|
89
105
|
onStartup?: (utils: ExtensionUtils) => Promise<void>;
|
|
90
|
-
components?: Record<
|
|
106
|
+
components?: Partial<Record<ExtensionComponentKey, ExtensionComponent>>;
|
|
91
107
|
dashboardNavs?: DashboardNavs;
|
|
92
108
|
}
|
|
@@ -88,6 +88,7 @@ export function loadExtensionComponents(
|
|
|
88
88
|
ctx: CTX,
|
|
89
89
|
name: string,
|
|
90
90
|
filterByExtensions?: string[],
|
|
91
|
+
props?: Record<string, any>,
|
|
91
92
|
): any[] {
|
|
92
93
|
const components = [];
|
|
93
94
|
for (const [extensionName, extensionValue] of Object.entries(
|
|
@@ -101,7 +102,13 @@ export function loadExtensionComponents(
|
|
|
101
102
|
extensionValue.components,
|
|
102
103
|
)) {
|
|
103
104
|
if (name.startsWith(componentName)) {
|
|
104
|
-
|
|
105
|
+
if (componentValue && typeof componentValue === "object" && "component" in componentValue && "when" in componentValue) {
|
|
106
|
+
if (!props || componentValue.when(props)) {
|
|
107
|
+
components.push(componentValue.component);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
components.push(componentValue);
|
|
111
|
+
}
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
}
|
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
|
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TODO
|
|
3
|
-
*
|
|
4
|
-
* this file shows an example of downloading the types files of a library from npm to use them in adding type support of that library in the vscode editor
|
|
5
|
-
* so the whole point is that for users to be able to paste a url from npm or jsr of a library and have all ts support from its types
|
|
6
|
-
*/
|
|
7
|
-
export {};
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TODO
|
|
3
|
-
*
|
|
4
|
-
* this file shows an example of downloading the types files of a library from npm to use them in adding type support of that library in the vscode editor
|
|
5
|
-
* so the whole point is that for users to be able to paste a url from npm or jsr of a library and have all ts support from its types
|
|
6
|
-
*/
|
|
7
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
8
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
9
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
10
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
11
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
12
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
13
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
14
|
-
});
|
|
15
|
-
};
|
|
16
|
-
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
17
|
-
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
18
|
-
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
19
|
-
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
20
|
-
function step(op) {
|
|
21
|
-
if (f) throw new TypeError("Generator is already executing.");
|
|
22
|
-
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
23
|
-
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
24
|
-
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
25
|
-
switch (op[0]) {
|
|
26
|
-
case 0: case 1: t = op; break;
|
|
27
|
-
case 4: _.label++; return { value: op[1], done: false };
|
|
28
|
-
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
29
|
-
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
30
|
-
default:
|
|
31
|
-
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
32
|
-
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
33
|
-
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
34
|
-
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
35
|
-
if (t[2]) _.ops.pop();
|
|
36
|
-
_.trys.pop(); continue;
|
|
37
|
-
}
|
|
38
|
-
op = body.call(thisArg, _);
|
|
39
|
-
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
40
|
-
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
import { gunzipSync } from "fflate";
|
|
44
|
-
import { untar } from "@andrewbranch/untar.js";
|
|
45
|
-
function extractTgz(buffer) {
|
|
46
|
-
return __awaiter(this, void 0, void 0, function () {
|
|
47
|
-
var tarBuffer, cleanBuffer, files, dtsFiles, _i, files_1, file, content;
|
|
48
|
-
return __generator(this, function (_a) {
|
|
49
|
-
switch (_a.label) {
|
|
50
|
-
case 0:
|
|
51
|
-
tarBuffer = gunzipSync(new Uint8Array(buffer));
|
|
52
|
-
cleanBuffer = tarBuffer.buffer.slice(tarBuffer.byteOffset, tarBuffer.byteOffset + tarBuffer.byteLength);
|
|
53
|
-
return [4 /*yield*/, untar(cleanBuffer)];
|
|
54
|
-
case 1:
|
|
55
|
-
files = _a.sent();
|
|
56
|
-
dtsFiles = [];
|
|
57
|
-
for (_i = 0, files_1 = files; _i < files_1.length; _i++) {
|
|
58
|
-
file = files_1[_i];
|
|
59
|
-
if (file.filename.endsWith(".d.ts")) {
|
|
60
|
-
content = new TextDecoder().decode(file.fileData);
|
|
61
|
-
dtsFiles.push({ name: file.filename, content: content });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return [2 /*return*/, dtsFiles];
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
var tgzURL = "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz";
|
|
70
|
-
var response = await fetch(tgzURL);
|
|
71
|
-
var arrayBuffer = await response.arrayBuffer();
|
|
72
|
-
var files = await extractTgz(arrayBuffer);
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TODO
|
|
3
|
-
*
|
|
4
|
-
* this file shows an example of downloading the types files of a library from npm to use them in adding type support of that library in the vscode editor
|
|
5
|
-
* so the whole point is that for users to be able to paste a url from npm or jsr of a library and have all ts support from its types
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { gunzipSync } from "fflate";
|
|
9
|
-
import { untar } from "@andrewbranch/untar.js";
|
|
10
|
-
|
|
11
|
-
async function extractTgz(buffer: ArrayBuffer) {
|
|
12
|
-
const tarBuffer = gunzipSync(new Uint8Array(buffer));
|
|
13
|
-
|
|
14
|
-
const cleanBuffer = tarBuffer.buffer.slice(
|
|
15
|
-
tarBuffer.byteOffset,
|
|
16
|
-
tarBuffer.byteOffset + tarBuffer.byteLength,
|
|
17
|
-
);
|
|
18
|
-
const files = await untar(cleanBuffer as ArrayBuffer);
|
|
19
|
-
|
|
20
|
-
const dtsFiles = [];
|
|
21
|
-
|
|
22
|
-
for (const file of files) {
|
|
23
|
-
if (file.filename.endsWith(".d.ts")) {
|
|
24
|
-
const content = new TextDecoder().decode(file.fileData);
|
|
25
|
-
dtsFiles.push({ name: file.filename, content });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return dtsFiles;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const tgzURL = "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz";
|
|
33
|
-
const response = await fetch(tgzURL);
|
|
34
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
35
|
-
|
|
36
|
-
const files = await extractTgz(arrayBuffer);
|