@lobb-js/lobb-ext-storage 0.1.28

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/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@lobb-js/lobb-ext-storage",
3
+ "version": "0.1.28",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "files": [
9
+ "studio"
10
+ ],
11
+ "exports": {
12
+ ".": "./studio/src/index.ts"
13
+ },
14
+ "scripts": {
15
+ "dev": "cd studio && vite",
16
+ "build": "cd studio && vite build",
17
+ "preview": "cd studio && vite preview",
18
+ "check": "cd studio && svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
19
+ },
20
+ "dependencies": {
21
+ "@lobb-js/studio": "0.1.31",
22
+ "path-browserify": "^1.0.1"
23
+ },
24
+ "devDependencies": {
25
+ "@sveltejs/vite-plugin-svelte": "6.2.1",
26
+ "@tsconfig/svelte": "^5.0.6",
27
+ "@types/path-browserify": "^1.0.3",
28
+ "@types/node": "^24.10.1",
29
+ "autoprefixer": "^10.4.23",
30
+ "svelte": "^5.43.8",
31
+ "svelte-check": "^4.3.4",
32
+ "tailwindcss": "^3.4.19",
33
+ "tailwindcss-animate": "^1.0.7",
34
+ "typescript": "~5.9.3",
35
+ "vite": "6.3.3"
36
+ }
37
+ }
@@ -0,0 +1,35 @@
1
+ # Storage Extension - Studio
2
+
3
+ This directory contains the frontend/dashboard interface for the storage extension.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ studio/
9
+ ├── src/
10
+ │ ├── index.ts # Extension entry point
11
+ │ ├── main.ts # Vite app entry
12
+ │ ├── components/ # UI components
13
+ │ │ ├── fileExplorer.svelte
14
+ │ │ ├── childrenFileExplorer.svelte
15
+ │ │ ├── fileIcon.svelte
16
+ │ │ ├── fileManagerBreadCrumbs.svelte
17
+ │ │ └── explorerNotSupported.svelte
18
+ │ ├── pages/ # UI pages
19
+ │ │ └── fileExplorerPage.svelte
20
+ │ └── foreignKeyComponent.svelte
21
+ ├── public/ # Static assets
22
+ ├── index.html # HTML entry point
23
+ ├── vite.config.ts # Vite configuration
24
+ ├── tailwind.config.ts # Tailwind CSS configuration
25
+ └── tsconfig.json # TypeScript configuration
26
+ ```
27
+
28
+ ## Features
29
+
30
+ The studio interface includes:
31
+ - File explorer with directory navigation
32
+ - File upload and management
33
+ - Visual file icons
34
+ - Breadcrumb navigation
35
+ - Foreign key component integration
@@ -0,0 +1,28 @@
1
+ // TODO: Import these types from @lobb-js/studio once available
2
+ export interface Extension {
3
+ name: string;
4
+ onStartup?: (utils: ExtensionUtils) => void;
5
+ components?: Record<string, any>;
6
+ dashboardNavs?: {
7
+ top?: NavItem[];
8
+ middle?: NavItem[];
9
+ bottom?: NavItem[];
10
+ };
11
+ }
12
+
13
+ export interface NavItem {
14
+ label: string;
15
+ icon?: any;
16
+ href?: string;
17
+ onclick?: () => void;
18
+ navs?: NavItem[];
19
+ }
20
+
21
+ export interface ExtensionUtils {
22
+ components: {
23
+ Icons: Record<string, any>;
24
+ };
25
+ location: {
26
+ navigate: (path: string) => void;
27
+ };
28
+ }
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Lobb Studio</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {}
5
+ }
6
+ };
@@ -0,0 +1,13 @@
1
+ <script lang="ts">
2
+ import { FolderX } from "lucide-svelte";
3
+ import type { ExtensionProps } from "../../extension.types";
4
+ import FileExplorer from "./fileExplorer.svelte";
5
+
6
+ interface Props extends ExtensionProps {
7
+ class: string;
8
+ }
9
+
10
+ const { utils, filter, class: className }: Props = $props();
11
+ </script>
12
+
13
+ <FileExplorer foreignKeyObject={filter} utils={utils} class={className} />
@@ -0,0 +1,13 @@
1
+ <script lang="ts">
2
+ import { FolderX } from "lucide-svelte";
3
+ </script>
4
+
5
+ <div class="border rounded-md p-4 bg-soft flex items-center flex-col text-muted-foreground gap-2">
6
+ <FolderX size={35} />
7
+ <div class="flex flex-col justify-center items-center">
8
+ <div class="text-sm font-bold">Storage file system is not supported here</div>
9
+ <div class="text-xs text-center max-w-96">
10
+ To access the storage file system for this record, please create the record first, then edit it to enable storage access.
11
+ </div>
12
+ </div>
13
+ </div>
@@ -0,0 +1,404 @@
1
+ <script lang="ts">
2
+ import type { ExtensionProps } from "src/extensions/extension.types";
3
+ import type { Snippet } from "svelte";
4
+
5
+ import path from "path-browserify"
6
+ import FileManagerBreadCrumbs from "./fileManagerBreadCrumbs.svelte";
7
+ import FileIcon from "./fileIcon.svelte";
8
+
9
+ export interface Entry {
10
+ id: string;
11
+ name: string;
12
+ path: string;
13
+ type: "directory" | "file";
14
+ icon: string;
15
+ file_mime_type: string;
16
+ }
17
+
18
+ interface Props extends ExtensionProps {
19
+ location?: string;
20
+ onFileSelect?: (entry: Entry) => void;
21
+ foreignKeyObject?: Record<string, any>;
22
+ topLeftHeader?: Snippet<[]>;
23
+ class: string;
24
+ }
25
+
26
+ let { location = $bindable('/'), onFileSelect, topLeftHeader, foreignKeyObject = {}, class: className, ...props }: Props = $props();
27
+
28
+ let loading = $state(true);
29
+ let entries: Entry[] = $state([]);
30
+
31
+ let contextType: "directory" | "file" | "background" = $state("background");
32
+ let selectedEntry: Entry | undefined = $state(undefined);
33
+ let contextMenuOpen: boolean = $state(false);
34
+
35
+ const utils = props.utils;
36
+ const icons = utils.components.Icons;
37
+ const { Tooltip, Button, ContextMenu } = utils.components;
38
+
39
+ $effect(() => {
40
+ renderFileExplorer(location);
41
+ });
42
+
43
+ async function renderFileExplorer(
44
+ location: string,
45
+ ) {
46
+ loading = true;
47
+
48
+ const localFilter = {
49
+ filter: {
50
+ path: location,
51
+ ...foreignKeyObject,
52
+ },
53
+ };
54
+ const response = await utils.lobb.findAll("storage_fs", localFilter);
55
+ entries = (await response.json()).data;
56
+
57
+ loading = false;
58
+ }
59
+
60
+ async function refreshFileExplorer() {
61
+ await renderFileExplorer(location);
62
+ }
63
+
64
+ async function handleCreateFolder() {
65
+ contextMenuOpen = false;
66
+ const newFolderName = prompt("Please enter a name for the new folder:");
67
+ if (newFolderName) {
68
+ const response = await utils.lobb.createOne("storage_fs", {
69
+ name: newFolderName,
70
+ path: location,
71
+ type: "directory",
72
+ ...foreignKeyObject,
73
+ });
74
+ const result = await response.json();
75
+ if (result.error) {
76
+ if (result.error.status === 409) {
77
+ utils.toast.error(
78
+ "Directories with the same name cannot exist in the same directory.",
79
+ );
80
+ } else {
81
+ utils.toast.error(result.error.message);
82
+ }
83
+ return;
84
+ }
85
+ await refreshFileExplorer();
86
+ }
87
+ }
88
+
89
+ async function handleFileUpload() {
90
+ contextMenuOpen = false;
91
+
92
+ async function uploadFile(file: File) {
93
+ if (file) {
94
+ const response = await utils.lobb.createOne(
95
+ "storage_fs",
96
+ {
97
+ path: location,
98
+ ...foreignKeyObject,
99
+ },
100
+ file,
101
+ );
102
+ const result = await response.json();
103
+ if (result.error) {
104
+ if (result.error.status === 409) {
105
+ utils.toast.error(
106
+ "Files with the same name cannot exist in the same directory.",
107
+ );
108
+ } else {
109
+ utils.toast.error(result.error.message);
110
+ }
111
+ contextMenuOpen = false;
112
+ return;
113
+ }
114
+ }
115
+ await refreshFileExplorer();
116
+ }
117
+
118
+ const file = await utils.getFileFromUser();
119
+
120
+ utils.toast.promise(
121
+ uploadFile(file),
122
+ {
123
+ loading: 'Uploading File...',
124
+ success: 'Done Uploading!',
125
+ error: 'Something went wrong While Uploading!',
126
+ duration: 3000,
127
+ },
128
+ );
129
+ }
130
+
131
+ async function handleDeleteEntry() {
132
+ if (!selectedEntry) {
133
+ throw new Error(
134
+ "The selected Element is (undefined) for some reason",
135
+ );
136
+ }
137
+ const entryId = selectedEntry.id;
138
+ if (!entryId) {
139
+ throw new Error("The id of the entry is (null) for some reason");
140
+ }
141
+ const response = await utils.lobb.deleteOne(
142
+ "storage_fs",
143
+ entryId,
144
+ );
145
+ const result = await response.json();
146
+ if (result.status >= 400) {
147
+ return;
148
+ }
149
+ await refreshFileExplorer();
150
+ utils.toast.success("Deleted entry successfully");
151
+ contextMenuOpen = false;
152
+ }
153
+
154
+ async function oncontextmenucapture(e: Event) {
155
+ const element = e.target as HTMLElement;
156
+ const entryId = element.getAttribute("data-id");
157
+ if (entryId) {
158
+ selectedEntry = entries.find((el: any) => el.id == entryId);
159
+ if (selectedEntry?.type === "file") {
160
+ contextType = "file";
161
+ } else if (selectedEntry?.type === "directory") {
162
+ contextType = "directory";
163
+ } else {
164
+ contextType = "background";
165
+ }
166
+ } else {
167
+ selectedEntry = undefined;
168
+ contextType = "background";
169
+ }
170
+ }
171
+
172
+ async function handleSelect(e: Event) {
173
+ const element = e.target as HTMLElement;
174
+ const entryId = element.getAttribute("data-id");
175
+ if (entryId) {
176
+ selectedEntry = entries.find((el: any) => el.id == entryId);
177
+ } else {
178
+ selectedEntry = undefined;
179
+ }
180
+ }
181
+
182
+ async function handleEntryDoubleClick(entry: Entry) {
183
+ if (entry.type === "directory") {
184
+ const locationPath = path.join(entry.path, entry.name);
185
+ location = locationPath;
186
+ } else if (entry.type === 'file') {
187
+ if (onFileSelect) {
188
+ onFileSelect(entry);
189
+ }
190
+ }
191
+ }
192
+ </script>
193
+
194
+ <div class="flex h-full w-full flex-col bg-background {className}">
195
+ <!-- file explorer header -->
196
+ <div
197
+ class="flex h-10 items-center justify-between border-b px-2"
198
+ >
199
+ <div class="flex items-center justify-between gap-2">
200
+ {@render topLeftHeader?.()}
201
+ <FileManagerBreadCrumbs bind:location={location} { utils } />
202
+ </div>
203
+ <div class="flex gap-2">
204
+ <!-- entries view toggle -->
205
+ <Tooltip.Provider delayDuration={0}>
206
+ <Tooltip.Root>
207
+ <Tooltip.Trigger>
208
+ <Button
209
+ class="h-6 w-6 text-muted-foreground hover:bg-transparent"
210
+ variant="ghost"
211
+ size="icon"
212
+ >
213
+ <icons.List />
214
+ </Button>
215
+ </Tooltip.Trigger>
216
+ <Tooltip.Content
217
+ side="bottom"
218
+ sideOffset={7.5}
219
+ >
220
+ List View
221
+ </Tooltip.Content>
222
+ </Tooltip.Root>
223
+ </Tooltip.Provider>
224
+ <Button
225
+ variant="outline"
226
+ class="h-7 px-3 text-xs font-normal"
227
+ Icon={icons.FolderPlus}
228
+ onclick={handleCreateFolder}
229
+ >
230
+ New folder
231
+ </Button>
232
+ <Button
233
+ class="h-7 px-3 text-xs font-normal"
234
+ Icon={icons.FilePlus}
235
+ onclick={() => { handleFileUpload() }}
236
+ >
237
+ New file
238
+ </Button>
239
+ </div>
240
+ </div>
241
+ <!-- file explorer body -->
242
+ <div class="flex flex-1 overflow-auto h-[calc(100vh-7.5rem)]">
243
+ <ContextMenu.Root bind:open={contextMenuOpen}>
244
+ <ContextMenu.Trigger
245
+ class="z-10 flex flex-1 flex-wrap content-start items-start justify-start gap-4 p-4"
246
+ onclick={handleSelect}
247
+ {oncontextmenucapture}
248
+ >
249
+ {#if loading}
250
+ <div
251
+ class="flex h-full w-full items-center justify-center"
252
+ >
253
+ <div
254
+ class="flex h-full w-full flex-col items-center justify-center gap-4 text-muted-foreground"
255
+ >
256
+ <icons.LoaderCircle
257
+ class="opacity-50 animate-spin"
258
+ size="50"
259
+ />
260
+ <div
261
+ class="flex flex-col items-center justify-center"
262
+ >
263
+ <div>Loading files and directories, please wait...</div>
264
+ <div class="text-xs">
265
+ Fetching the contents of the current folder from the lobb server.
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ {:else}
271
+ {#if entries && entries.length}
272
+ {#each entries as entry}
273
+ {@const selectedCss =
274
+ selectedEntry?.id === entry.id
275
+ ? "bg-opacity-25 hover:bg-opacity-25"
276
+ : "hover:bg-opacity-10"}
277
+ <button
278
+ class="w-32 cursor-default rounded-md bg-muted-foreground bg-opacity-0 p-2 {selectedCss}"
279
+ data-id={entry.id}
280
+ ondblclick={() =>
281
+ handleEntryDoubleClick(entry)}
282
+ >
283
+ {#if entry.type === "directory"}
284
+ <div
285
+ class="pointer-events-none flex min-w-28 flex-col items-center"
286
+ >
287
+ <div
288
+ class="pointer-events-none relative"
289
+ >
290
+ <icons.Folder
291
+ size="50"
292
+ class="fill-muted-foreground stroke-none"
293
+ />
294
+ {#if entry.icon}
295
+ {@const key = entry.icon}
296
+ {/* @ts-ignore */ null}
297
+ {@const Icon = icons[key]}
298
+ <div
299
+ class="absolute left-0 top-0 flex h-full w-full items-center justify-center"
300
+ >
301
+ <Icon
302
+ class="text-primary-foreground"
303
+ style="transform: translateY(0.1rem);"
304
+ size="15"
305
+ />
306
+ </div>
307
+ {/if}
308
+ </div>
309
+ <div
310
+ class="pointer-events-none w-full break-words text-center text-sm font-medium"
311
+ >
312
+ {entry.name}
313
+ </div>
314
+ </div>
315
+ {:else}
316
+ <FileIcon {...props} {entry} />
317
+ {/if}
318
+ </button>
319
+ {/each}
320
+ {:else}
321
+ <div
322
+ class="flex h-full w-full items-center justify-center p-8"
323
+ >
324
+ <div
325
+ class="flex h-full w-full flex-col items-center justify-center gap-4 text-muted-foreground"
326
+ >
327
+ <icons.CircleSlash2
328
+ class="opacity-50"
329
+ size="50"
330
+ />
331
+ <div
332
+ class="flex flex-col items-center justify-center"
333
+ >
334
+ <div>Folder is empty</div>
335
+ <div class="text-xs">
336
+ Right-click to create a new folder
337
+ or upload a file.
338
+ </div>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ {/if}
343
+ {/if}
344
+ </ContextMenu.Trigger>
345
+ <ContextMenu.Content
346
+ class="z-10 flex flex-col rounded-md border border-muted bg-background p-1 shadow-md outline-none w-40"
347
+ >
348
+ {#if contextType === "directory"}
349
+ <Button
350
+ variant="ghost"
351
+ onclick={handleDeleteEntry}
352
+ class="flex items-center gap-2 rounded-md px-4 h-8 hover:bg-muted focus-visible:ring-0 cursor-default font-normal"
353
+ >
354
+ Delete directory
355
+ </Button>
356
+ {:else if contextType === "file"}
357
+ <Button
358
+ variant="ghost"
359
+ onclick={handleDeleteEntry}
360
+ class="rounded-md px-4 h-8 hover:bg-muted focus-visible:ring-0 cursor-default font-normal"
361
+ >
362
+ Delete file
363
+ </Button>
364
+ {:else}
365
+ <Button
366
+ variant="ghost"
367
+ onclick={handleCreateFolder}
368
+ class="flex items-center gap-2 rounded-md px-4 h-8 hover:bg-muted focus-visible:ring-0 cursor-default font-normal"
369
+ >
370
+ New folder
371
+ </Button>
372
+ <Button
373
+ variant="ghost"
374
+ onclick={handleFileUpload}
375
+ class="flex items-center gap-2 rounded-md px-4 h-8 hover:bg-muted focus-visible:ring-0 cursor-default font-normal"
376
+ >
377
+ New file
378
+ </Button>
379
+ {/if}
380
+ </ContextMenu.Content>
381
+ </ContextMenu.Root>
382
+ </div>
383
+ <div
384
+ class="flex h-10 items-center justify-center border-t px-4 text-sm text-muted-foreground"
385
+ >
386
+ {#if loading}
387
+ <div class="flex gap-2 justify-center items-center">
388
+ <icons.LoaderCircle
389
+ class="animate-spin"
390
+ size="15"
391
+ />
392
+ <div>loading...</div>
393
+ </div>
394
+ {:else}
395
+ {entries ? entries.length : "0"} items available
396
+ {/if}
397
+ </div>
398
+ </div>
399
+
400
+ <style>
401
+ .selected {
402
+ background-color: theme("colors.muted.DEFAULT");
403
+ }
404
+ </style>
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { ExtensionProps } from "src/extensions/extension.types";
3
+ import type { Entry } from "./fileExplorer.svelte";
4
+ import { ctx } from "$lib/store.svelte";
5
+
6
+ interface Props extends ExtensionProps {
7
+ entry: Entry;
8
+ }
9
+
10
+ const { entry, utils }: Props = $props();
11
+
12
+ const MainIcon = utils.components.Icons.File;
13
+ const InnerIcon = utils.components.Icons.Text;
14
+ </script>
15
+
16
+ <div
17
+ class="pointer-events-none flex flex-col items-center gap-2 min-w-28"
18
+ >
19
+ {#if entry.file_mime_type.startsWith("image/")}
20
+ <div class="rounded-md w-24 h-24 bg-center bg-contain bg-no-repeat" style="background-image: url('{ctx.lobbUrl}/api/collections/storage_fs/{entry.id}?action=view');"></div>
21
+ {:else}
22
+ <div
23
+ class="pointer-events-none relative"
24
+ >
25
+ <MainIcon
26
+ size="50"
27
+ class="fill-muted stroke-muted-foreground"
28
+ style="stroke-width: 0.03rem;"
29
+ />
30
+ <div
31
+ class="absolute left-0 top-0 flex h-full w-full items-center justify-center"
32
+ >
33
+ <InnerIcon
34
+ class="text-muted-foreground"
35
+ style="transform: translateY(0.1rem);"
36
+ size="15"
37
+ />
38
+ </div>
39
+ </div>
40
+ {/if}
41
+ <div
42
+ class="pointer-events-none w-full break-words text-center text-sm font-medium"
43
+ >
44
+ {entry.name}
45
+ </div>
46
+ </div>
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import type { ExtensionProps } from "src/extensions/extension.types";
3
+ import path from "path-browserify"
4
+
5
+ interface Props extends ExtensionProps {
6
+ location: string;
7
+ }
8
+
9
+ let { location = $bindable(), utils }: Props = $props();
10
+
11
+ let pathNames: string[] | undefined = $state(undefined);
12
+
13
+ $effect(() => {
14
+ pathNames = ["/", ...location.split("/").filter(Boolean)];
15
+ });
16
+ </script>
17
+
18
+ <utils.components.Breadcrumb.Root class="pl-2">
19
+ <utils.components.Breadcrumb.List>
20
+ {#if pathNames}
21
+ {#each pathNames as localPath, index}
22
+ {@const isLastElement = pathNames.length - 1 === index}
23
+ {@const currentFullPaths = pathNames
24
+ .slice(1, index + 1)
25
+ .join("/")}
26
+ {@const pathLabel = localPath === "/" ? "Home" : localPath}
27
+ <utils.components.Breadcrumb.Item>
28
+ {#if isLastElement}
29
+ <utils.components.Breadcrumb.Page>
30
+ {pathLabel}
31
+ </utils.components.Breadcrumb.Page>
32
+ {:else}
33
+ <utils.components.Breadcrumb.Link
34
+ class="cursor-pointer"
35
+ onclick={() => {
36
+ const invertedIndex = pathNames && pathNames.length - 1 - index;
37
+ location = path.join(location, Array(invertedIndex).fill("..").join('/'))
38
+ }}
39
+ >
40
+ {pathLabel}
41
+ </utils.components.Breadcrumb.Link>
42
+ {/if}
43
+ </utils.components.Breadcrumb.Item>
44
+ {#if !isLastElement}
45
+ <utils.components.Breadcrumb.Separator />
46
+ {/if}
47
+ {/each}
48
+ {/if}
49
+ </utils.components.Breadcrumb.List>
50
+ </utils.components.Breadcrumb.Root>
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import type { ExtensionProps } from "../extension.types";
3
+ import FileExplorer, { type Entry } from "./components/fileExplorer.svelte";
4
+ import FileIcon from "./components/fileIcon.svelte";
5
+
6
+ interface Props extends ExtensionProps {
7
+ value: Entry;
8
+ }
9
+
10
+ let { utils, value = $bindable(), ...props }: Props = $props();
11
+
12
+ const { FilePlus2 } = utils.components.Icons;
13
+ const { Drawer } = utils.components;
14
+
15
+ let openDrawer = $state(false);
16
+
17
+ function handleClick() {
18
+ openDrawer = true;
19
+ }
20
+
21
+ async function onHideHandle() {
22
+ openDrawer = false;
23
+ }
24
+
25
+ async function handleOnFileSelect(entry: Entry) {
26
+ value = entry;
27
+ openDrawer = false;
28
+ }
29
+ </script>
30
+
31
+ <button onclick={handleClick} class="flex flex-col items-center gap-2 w-full border p-4 rounded-md bg-soft">
32
+ {#if value}
33
+ <FileIcon entry={value} {utils} />
34
+ {:else}
35
+ <FilePlus2 class="text-muted-foreground" />
36
+ <div class="text-sm text-muted-foreground">Click to add an asset</div>
37
+ {/if}
38
+ </button>
39
+
40
+ {#if openDrawer}
41
+ <Drawer onHide={onHideHandle}>
42
+ <FileExplorer onFileSelect={handleOnFileSelect} utils={utils} {...props} />
43
+ </Drawer>
44
+ {/if}
@@ -0,0 +1,28 @@
1
+ import type { Extension, ExtensionUtils } from "src/extensions/extension.types";
2
+
3
+ import FileExplorerPage from "./pages/fileExplorerPage.svelte";
4
+ import ForeignKeyComponent from "./foreignKeyComponent.svelte";
5
+ import ChildrenFileExplorer from "./components/childrenFileExplorer.svelte";
6
+ import ExplorerNotSupported from "./components/explorerNotSupported.svelte";
7
+
8
+ export function extension(utils: ExtensionUtils): Extension {
9
+ return {
10
+ name: "storage",
11
+ components: {
12
+ "pages.file_manager": FileExplorerPage,
13
+ "detailView.fields.foreignKey.storage_fs": ForeignKeyComponent,
14
+ "detailView.create.subRecords.storage_fs": ExplorerNotSupported,
15
+ "detailView.update.subRecords.storage_fs": ChildrenFileExplorer,
16
+ "listView.entry.children.storage_fs": ChildrenFileExplorer,
17
+ },
18
+ dashboardNavs: {
19
+ middle: [
20
+ {
21
+ href: "/extensions/storage/file_manager",
22
+ icon: utils.components.Icons.Folders,
23
+ label: "File Manager",
24
+ },
25
+ ],
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,12 @@
1
+ import { mount } from "svelte";
2
+ import Studio from "@lobb-js/studio";
3
+ import { extension } from "./index.ts";
4
+
5
+ const app = mount(Studio, {
6
+ target: document.getElementById("app")!,
7
+ props: {
8
+ extensions: [extension],
9
+ },
10
+ });
11
+
12
+ export default app;
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ import type { ExtensionProps } from "src/extensions/extension.types";
3
+ import type { SideBarData, SideBarElement } from "$lib/components/sidebar/sidebarElements.svelte";
4
+
5
+ import FileExplorer, { type Entry } from "../components/fileExplorer.svelte";
6
+ import path from "path-browserify"
7
+ import { onMount } from "svelte";
8
+
9
+ const props: ExtensionProps = $props();
10
+ let { utils } = props;
11
+ let sidebarData: SideBarData | null = $state(null);
12
+ const { location: routerLocation } = utils;
13
+ const icons = utils.components.Icons;
14
+ const { Sidebar, SidebarTrigger } = utils.components;
15
+ let location: string = $state("/");
16
+
17
+ onMount(() => {
18
+ getSidebarData();
19
+ updateExplorerLocationFromRouter(routerLocation);
20
+ })
21
+
22
+ $effect(() => {
23
+ updateRouterLocationFromExplorer(location);
24
+ })
25
+
26
+ async function updateExplorerLocationFromRouter(localLocation: typeof routerLocation) {
27
+ const fileExplorerPath = localLocation.url.pathname.replace('/extensions/storage/file_manager', '');
28
+ location = fileExplorerPath ? fileExplorerPath : "/";
29
+ }
30
+
31
+ async function updateRouterLocationFromExplorer(localLocation: string) {
32
+ const newPath = `/extensions/storage/file_manager${localLocation}`;
33
+ if (routerLocation.url.pathname !== newPath) {
34
+ routerLocation.navigate(newPath);
35
+ }
36
+ }
37
+
38
+ async function getSidebarData() {
39
+ const response = await utils.lobb.findAll("storage_fs", {
40
+ filter: {
41
+ type: "directory",
42
+ is_pinned_sidebar: {
43
+ $eq: true,
44
+ },
45
+ },
46
+ });
47
+ const result = await response.json();
48
+ const entries = result.data;
49
+
50
+ sidebarData = entries.map((el: Entry): SideBarElement => {
51
+ return {
52
+ name: el.name,
53
+ icon: icons.Folder,
54
+ onclick: () => {
55
+ location = path.join(el.path, el.name);
56
+ }
57
+ };
58
+ });
59
+ }
60
+ </script>
61
+
62
+ <Sidebar title="Storage" data={sidebarData}>
63
+ <FileExplorer bind:location={location} {...props}>
64
+ {#snippet topLeftHeader()}
65
+ <SidebarTrigger />
66
+ {/snippet}
67
+ </FileExplorer>
68
+ </Sidebar>
@@ -0,0 +1,8 @@
1
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
2
+
3
+ /** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
4
+ export default {
5
+ // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
6
+ // for more information about preprocessors
7
+ preprocess: vitePreprocess(),
8
+ }
@@ -0,0 +1,92 @@
1
+ import { fontFamily } from "tailwindcss/defaultTheme";
2
+ import type { Config } from "tailwindcss";
3
+ import tailwindcssAnimate from "tailwindcss-animate";
4
+
5
+ const config: Config = {
6
+ darkMode: ["class"],
7
+ content: [
8
+ "./src/**/*.{html,js,svelte,ts}",
9
+ "../../../packages/studio/src/**/*.{html,js,svelte,ts}",
10
+ ],
11
+ safelist: ["dark"],
12
+ theme: {
13
+ container: {
14
+ center: true,
15
+ padding: "2rem",
16
+ screens: {
17
+ "2xl": "1400px",
18
+ },
19
+ },
20
+ extend: {
21
+ colors: {
22
+ border: "hsl(var(--border) / <alpha-value>)",
23
+ input: "hsl(var(--input) / <alpha-value>)",
24
+ ring: "hsl(var(--ring) / <alpha-value>)",
25
+ background: "hsl(var(--background) / <alpha-value>)",
26
+ foreground: "hsl(var(--foreground) / <alpha-value>)",
27
+ primary: {
28
+ DEFAULT: "hsl(var(--primary) / <alpha-value>)",
29
+ foreground: "hsl(var(--primary-foreground) / <alpha-value>)",
30
+ },
31
+ secondary: {
32
+ DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
33
+ foreground: "hsl(var(--secondary-foreground) / <alpha-value>)",
34
+ },
35
+ destructive: {
36
+ DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
37
+ foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
38
+ },
39
+ soft: {
40
+ DEFAULT: "hsl(var(--soft) / <alpha-value>)",
41
+ },
42
+ muted: {
43
+ DEFAULT: "hsl(var(--muted) / <alpha-value>)",
44
+ foreground: "hsl(var(--muted-foreground) / <alpha-value>)",
45
+ },
46
+ accent: {
47
+ DEFAULT: "hsl(var(--accent) / <alpha-value>)",
48
+ foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
49
+ },
50
+ popover: {
51
+ DEFAULT: "hsl(var(--popover) / <alpha-value>)",
52
+ foreground: "hsl(var(--popover-foreground) / <alpha-value>)",
53
+ },
54
+ card: {
55
+ DEFAULT: "hsl(var(--card) / <alpha-value>)",
56
+ foreground: "hsl(var(--card-foreground) / <alpha-value>)",
57
+ },
58
+ },
59
+ borderRadius: {
60
+ xl: "calc(var(--radius) + 4px)",
61
+ lg: "var(--radius)",
62
+ md: "calc(var(--radius) - 2px)",
63
+ sm: "calc(var(--radius) - 4px)",
64
+ },
65
+ fontFamily: {
66
+ sans: [...fontFamily.sans],
67
+ },
68
+ keyframes: {
69
+ "accordion-down": {
70
+ from: { height: "0" },
71
+ to: { height: "var(--bits-accordion-content-height)" },
72
+ },
73
+ "accordion-up": {
74
+ from: { height: "var(--bits-accordion-content-height)" },
75
+ to: { height: "0" },
76
+ },
77
+ "caret-blink": {
78
+ "0%,70%,100%": { opacity: "1" },
79
+ "20%,50%": { opacity: "0" },
80
+ },
81
+ },
82
+ animation: {
83
+ "accordion-down": "accordion-down 0.2s ease-out",
84
+ "accordion-up": "accordion-up 0.2s ease-out",
85
+ "caret-blink": "caret-blink 1.25s ease-out infinite",
86
+ },
87
+ },
88
+ },
89
+ plugins: [tailwindcssAnimate],
90
+ };
91
+
92
+ export default config;
@@ -0,0 +1,22 @@
1
+ {
2
+ "extends": "@tsconfig/svelte/tsconfig.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5
+ "target": "ES2022",
6
+ "useDefineForClassFields": true,
7
+ "module": "ESNext",
8
+ "types": ["svelte", "vite/client"],
9
+ "noEmit": true,
10
+ "allowArbitraryExtensions": true,
11
+ /**
12
+ * Typecheck JS in `.svelte` and `.js` files by default.
13
+ * Disable checkJs if you'd like to use dynamic types in JS.
14
+ * Note that setting allowJs false does not prevent the use
15
+ * of JS in `.svelte` files.
16
+ */
17
+ "allowJs": true,
18
+ "checkJs": true,
19
+ "moduleDetection": "force"
20
+ },
21
+ "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
22
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "vite";
2
+ import { svelte } from "@sveltejs/vite-plugin-svelte";
3
+ import path from "path";
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [svelte()],
8
+ resolve: {
9
+ alias: {
10
+ $lib: path.resolve("../../../packages/studio/src/lib"),
11
+ },
12
+ },
13
+ });