@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.
@@ -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/30'}
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";
@@ -21,7 +21,7 @@
21
21
  ...props
22
22
  }: Props = $props();
23
23
 
24
- const Components = loadExtensionComponents(ctx, name, filterByExtensions);
24
+ const Components = $derived(loadExtensionComponents(ctx, name, filterByExtensions, props));
25
25
  </script>
26
26
 
27
27
  {#if Components.length}
@@ -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;
@@ -103,6 +103,7 @@
103
103
  class={cn(buttonVariants({ variant, size }), className)}
104
104
  {type}
105
105
  onclick={handleClick}
106
+ disabled={showLoading}
106
107
  {...restProps}
107
108
  >
108
109
  {@render innerPart()}
@@ -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<string, any>;
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
- components.push(componentValue);
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
  }
@@ -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.11.1",
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",
@@ -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/30'}
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'}
@@ -21,7 +21,7 @@
21
21
  ...props
22
22
  }: Props = $props();
23
23
 
24
- const Components = loadExtensionComponents(ctx, name, filterByExtensions);
24
+ const Components = $derived(loadExtensionComponents(ctx, name, filterByExtensions, props));
25
25
  </script>
26
26
 
27
27
  {#if Components.length}
@@ -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
@@ -103,6 +103,7 @@
103
103
  class={cn(buttonVariants({ variant, size }), className)}
104
104
  {type}
105
105
  onclick={handleClick}
106
+ disabled={showLoading}
106
107
  {...restProps}
107
108
  >
108
109
  {@render innerPart()}
@@ -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<string, any>;
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
- components.push(componentValue);
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
  }
@@ -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);