@lobb-js/lobb-ext-storage 0.8.2 → 0.8.4
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/extension.types.d.ts +25 -0
- package/dist/extension.types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/lib/components/childrenFileExplorer.svelte +12 -0
- package/dist/lib/components/childrenFileExplorer.svelte.d.ts +14 -0
- package/dist/lib/components/explorerNotSupported.svelte +13 -0
- package/dist/lib/components/explorerNotSupported.svelte.d.ts +14 -0
- package/dist/lib/components/fileExplorer.svelte +399 -0
- package/dist/lib/components/fileExplorer.svelte.d.ts +22 -0
- package/dist/lib/components/fileIcon.svelte +45 -0
- package/dist/lib/components/fileIcon.svelte.d.ts +14 -0
- package/dist/lib/components/fileManagerBreadCrumbs.svelte +50 -0
- package/dist/lib/components/fileManagerBreadCrumbs.svelte.d.ts +14 -0
- package/dist/lib/components/foreignKeyComponent.svelte +44 -0
- package/dist/lib/components/foreignKeyComponent.svelte.d.ts +14 -0
- package/dist/lib/components/pages/fileExplorerPage.svelte +66 -0
- package/dist/lib/components/pages/fileExplorerPage.svelte.d.ts +14 -0
- package/dist/lib/index.d.ts +0 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/utils.d.ts +12 -0
- package/dist/lib/utils.js +5 -0
- package/dist/tests/fileManager.spec.d.ts +1 -0
- package/dist/tests/fileManager.spec.js +18 -0
- package/dist/tests/package.json +1 -0
- package/dist/tests/playwright.config.cjs +27 -0
- package/dist/tests/playwright.config.d.cts +2 -0
- package/package.json +7 -3
- package/.github/workflows/create_tag_on_version_change.yaml +0 -45
- package/.github/workflows/publish_npm_package.yaml +0 -26
- package/.vscode/settings.json +0 -5
- package/CHANGELOG.md +0 -207
- package/lobb.ts +0 -54
- package/scripts/postpublish.sh +0 -12
- package/scripts/prepublish.sh +0 -17
- package/studio/app.html +0 -12
- package/studio/routes/+layout.svelte +0 -7
- package/studio/routes/+layout.ts +0 -1
- package/studio/routes/[...path]/+page.svelte +0 -6
- package/svelte.config.js +0 -24
- package/todo.md +0 -36
- package/tsconfig.app.json +0 -27
- package/tsconfig.json +0 -13
- package/tsconfig.node.json +0 -26
- package/vite.config.ts +0 -8
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface Extension {
|
|
2
|
+
name: string;
|
|
3
|
+
onStartup?: (utils: ExtensionUtils) => void;
|
|
4
|
+
components?: Record<string, any>;
|
|
5
|
+
dashboardNavs?: {
|
|
6
|
+
top?: NavItem[];
|
|
7
|
+
middle?: NavItem[];
|
|
8
|
+
bottom?: NavItem[];
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface NavItem {
|
|
12
|
+
label: string;
|
|
13
|
+
icon?: any;
|
|
14
|
+
href?: string;
|
|
15
|
+
onclick?: () => void;
|
|
16
|
+
navs?: NavItem[];
|
|
17
|
+
}
|
|
18
|
+
export interface ExtensionUtils {
|
|
19
|
+
components: {
|
|
20
|
+
Icons: Record<string, any>;
|
|
21
|
+
};
|
|
22
|
+
location: {
|
|
23
|
+
navigate: (path: string) => void;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import FileExplorerPage from "./lib/components/pages/fileExplorerPage.svelte";
|
|
2
|
+
import ForeignKeyComponent from "./lib/components/foreignKeyComponent.svelte";
|
|
3
|
+
import ChildrenFileExplorer from "./lib/components/childrenFileExplorer.svelte";
|
|
4
|
+
import ExplorerNotSupported from "./lib/components/explorerNotSupported.svelte";
|
|
5
|
+
export default function extension(utils) {
|
|
6
|
+
return {
|
|
7
|
+
name: "storage",
|
|
8
|
+
components: {
|
|
9
|
+
"pages.file_manager": FileExplorerPage,
|
|
10
|
+
"detailView.fields.foreignKey.storage_fs": ForeignKeyComponent,
|
|
11
|
+
"detailView.create.subRecords.storage_fs": ExplorerNotSupported,
|
|
12
|
+
"detailView.update.subRecords.storage_fs": ChildrenFileExplorer,
|
|
13
|
+
"listView.entry.children.storage_fs": ChildrenFileExplorer,
|
|
14
|
+
},
|
|
15
|
+
dashboardNavs: {
|
|
16
|
+
middle: [
|
|
17
|
+
{
|
|
18
|
+
href: "/studio/extensions/storage/file_manager",
|
|
19
|
+
icon: utils.components.Icons.Folders,
|
|
20
|
+
label: "File Manager",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "@lobb-js/studio";
|
|
3
|
+
import FileExplorer from "./fileExplorer.svelte";
|
|
4
|
+
|
|
5
|
+
interface Props extends ExtensionProps {
|
|
6
|
+
class: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { utils, filter, class: className }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<FileExplorer foreignKeyObject={filter} utils={utils} class={className} />
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: Record<string, never>;
|
|
4
|
+
events: {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
};
|
|
7
|
+
slots: {};
|
|
8
|
+
};
|
|
9
|
+
export type ChildrenFileExplorerProps = typeof __propDef.props;
|
|
10
|
+
export type ChildrenFileExplorerEvents = typeof __propDef.events;
|
|
11
|
+
export type ChildrenFileExplorerSlots = typeof __propDef.slots;
|
|
12
|
+
export default class ChildrenFileExplorer extends SvelteComponentTyped<ChildrenFileExplorerProps, ChildrenFileExplorerEvents, ChildrenFileExplorerSlots> {
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -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-muted/30 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,14 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: Record<string, never>;
|
|
4
|
+
events: {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
};
|
|
7
|
+
slots: {};
|
|
8
|
+
};
|
|
9
|
+
export type ExplorerNotSupportedProps = typeof __propDef.props;
|
|
10
|
+
export type ExplorerNotSupportedEvents = typeof __propDef.events;
|
|
11
|
+
export type ExplorerNotSupportedSlots = typeof __propDef.slots;
|
|
12
|
+
export default class ExplorerNotSupported extends SvelteComponentTyped<ExplorerNotSupportedProps, ExplorerNotSupportedEvents, ExplorerNotSupportedSlots> {
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "@lobb-js/studio";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
|
|
5
|
+
import { fileOpen } from 'browser-fs-access';
|
|
6
|
+
import path from "path-browserify"
|
|
7
|
+
import FileManagerBreadCrumbs from "./fileManagerBreadCrumbs.svelte";
|
|
8
|
+
import FileIcon from "./fileIcon.svelte";
|
|
9
|
+
|
|
10
|
+
export interface Entry {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
path: string;
|
|
14
|
+
type: "directory" | "file";
|
|
15
|
+
icon: string;
|
|
16
|
+
file_mime_type: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Props extends ExtensionProps {
|
|
20
|
+
location?: string;
|
|
21
|
+
onFileSelect?: (entry: Entry) => void;
|
|
22
|
+
foreignKeyObject?: Record<string, any>;
|
|
23
|
+
topLeftHeader?: Snippet<[]>;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let { location = $bindable('/'), onFileSelect, topLeftHeader, foreignKeyObject = {}, class: className, ...props }: Props = $props();
|
|
28
|
+
|
|
29
|
+
let loading = $state(true);
|
|
30
|
+
let entries: Entry[] = $state([]);
|
|
31
|
+
|
|
32
|
+
let contextType: "directory" | "file" | "background" = $state("background");
|
|
33
|
+
let selectedEntry: Entry | undefined = $state(undefined);
|
|
34
|
+
let contextMenuOpen: boolean = $state(false);
|
|
35
|
+
|
|
36
|
+
const utils = props.utils;
|
|
37
|
+
const icons = utils.components.Icons;
|
|
38
|
+
const { Tooltip, Button, ContextMenu } = utils.components;
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
renderFileExplorer(location);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
async function renderFileExplorer(
|
|
45
|
+
location: string,
|
|
46
|
+
) {
|
|
47
|
+
loading = true;
|
|
48
|
+
|
|
49
|
+
const localFilter = {
|
|
50
|
+
filter: {
|
|
51
|
+
path: location,
|
|
52
|
+
...foreignKeyObject,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
const response = await utils.lobb.findAll("storage_fs", localFilter);
|
|
56
|
+
entries = (await response.json()).data;
|
|
57
|
+
|
|
58
|
+
loading = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function refreshFileExplorer() {
|
|
62
|
+
await renderFileExplorer(location);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function handleCreateFolder() {
|
|
66
|
+
contextMenuOpen = false;
|
|
67
|
+
const newFolderName = prompt("Please enter a name for the new folder:");
|
|
68
|
+
if (newFolderName) {
|
|
69
|
+
const response = await utils.lobb.createOne("storage_fs", {
|
|
70
|
+
name: newFolderName,
|
|
71
|
+
path: location,
|
|
72
|
+
type: "directory",
|
|
73
|
+
...foreignKeyObject,
|
|
74
|
+
});
|
|
75
|
+
const result = await response.json();
|
|
76
|
+
if (result.error) {
|
|
77
|
+
if (result.error.status === 409) {
|
|
78
|
+
utils.toast.error(
|
|
79
|
+
"Directories with the same name cannot exist in the same directory.",
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
utils.toast.error(result.error.message);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await refreshFileExplorer();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function handleFileUpload() {
|
|
91
|
+
contextMenuOpen = false;
|
|
92
|
+
|
|
93
|
+
async function uploadFile(file: File) {
|
|
94
|
+
if (file) {
|
|
95
|
+
const response = await utils.lobb.createOne(
|
|
96
|
+
"storage_fs",
|
|
97
|
+
{
|
|
98
|
+
path: location,
|
|
99
|
+
...foreignKeyObject,
|
|
100
|
+
},
|
|
101
|
+
file,
|
|
102
|
+
);
|
|
103
|
+
const result = await response.json();
|
|
104
|
+
if (result.error) {
|
|
105
|
+
if (result.error.status === 409) {
|
|
106
|
+
utils.toast.error(
|
|
107
|
+
"Files with the same name cannot exist in the same directory.",
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
utils.toast.error(result.error.message);
|
|
111
|
+
}
|
|
112
|
+
contextMenuOpen = false;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await refreshFileExplorer();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const file = await fileOpen();
|
|
120
|
+
|
|
121
|
+
utils.toast.promise(
|
|
122
|
+
uploadFile(file),
|
|
123
|
+
{
|
|
124
|
+
loading: 'Uploading File...',
|
|
125
|
+
success: 'Done Uploading!',
|
|
126
|
+
error: 'Something went wrong While Uploading!',
|
|
127
|
+
duration: 3000,
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function handleDeleteEntry() {
|
|
133
|
+
if (!selectedEntry) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"The selected Element is (undefined) for some reason",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const entryId = selectedEntry.id;
|
|
139
|
+
if (!entryId) {
|
|
140
|
+
throw new Error("The id of the entry is (null) for some reason");
|
|
141
|
+
}
|
|
142
|
+
const response = await utils.lobb.deleteOne(
|
|
143
|
+
"storage_fs",
|
|
144
|
+
entryId,
|
|
145
|
+
);
|
|
146
|
+
const result = await response.json();
|
|
147
|
+
if (result.status >= 400) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await refreshFileExplorer();
|
|
151
|
+
utils.toast.success("Deleted entry successfully");
|
|
152
|
+
contextMenuOpen = false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function oncontextmenucapture(e: Event) {
|
|
156
|
+
const element = e.target as HTMLElement;
|
|
157
|
+
const entryId = element.getAttribute("data-id");
|
|
158
|
+
if (entryId) {
|
|
159
|
+
selectedEntry = entries.find((el: any) => el.id == entryId);
|
|
160
|
+
if (selectedEntry?.type === "file") {
|
|
161
|
+
contextType = "file";
|
|
162
|
+
} else if (selectedEntry?.type === "directory") {
|
|
163
|
+
contextType = "directory";
|
|
164
|
+
} else {
|
|
165
|
+
contextType = "background";
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
selectedEntry = undefined;
|
|
169
|
+
contextType = "background";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function handleSelect(e: Event) {
|
|
174
|
+
const element = e.target as HTMLElement;
|
|
175
|
+
const entryId = element.getAttribute("data-id");
|
|
176
|
+
if (entryId) {
|
|
177
|
+
selectedEntry = entries.find((el: any) => el.id == entryId);
|
|
178
|
+
} else {
|
|
179
|
+
selectedEntry = undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function handleEntryDoubleClick(entry: Entry) {
|
|
184
|
+
if (entry.type === "directory") {
|
|
185
|
+
const locationPath = path.join(entry.path, entry.name);
|
|
186
|
+
location = locationPath;
|
|
187
|
+
} else if (entry.type === 'file') {
|
|
188
|
+
if (onFileSelect) {
|
|
189
|
+
onFileSelect(entry);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<div class="flex h-full w-full flex-col bg-background {className}">
|
|
196
|
+
<!-- file explorer header -->
|
|
197
|
+
<div
|
|
198
|
+
class="flex h-10 items-center justify-between border-b px-2"
|
|
199
|
+
>
|
|
200
|
+
<div class="flex items-center justify-between gap-2">
|
|
201
|
+
{@render topLeftHeader?.()}
|
|
202
|
+
<FileManagerBreadCrumbs bind:location={location} {utils} />
|
|
203
|
+
</div>
|
|
204
|
+
<div class="flex gap-2">
|
|
205
|
+
<!-- entries view toggle -->
|
|
206
|
+
<Tooltip.Provider delayDuration={0}>
|
|
207
|
+
<Tooltip.Root>
|
|
208
|
+
<Tooltip.Trigger>
|
|
209
|
+
<Button
|
|
210
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
211
|
+
variant="ghost"
|
|
212
|
+
size="icon"
|
|
213
|
+
>
|
|
214
|
+
<icons.List />
|
|
215
|
+
</Button>
|
|
216
|
+
</Tooltip.Trigger>
|
|
217
|
+
<Tooltip.Content
|
|
218
|
+
side="bottom"
|
|
219
|
+
sideOffset={7.5}
|
|
220
|
+
>
|
|
221
|
+
List View
|
|
222
|
+
</Tooltip.Content>
|
|
223
|
+
</Tooltip.Root>
|
|
224
|
+
</Tooltip.Provider>
|
|
225
|
+
<Button
|
|
226
|
+
variant="outline"
|
|
227
|
+
class="h-7 px-3 text-xs font-normal"
|
|
228
|
+
Icon={icons.FolderPlus}
|
|
229
|
+
onclick={handleCreateFolder}
|
|
230
|
+
>
|
|
231
|
+
New folder
|
|
232
|
+
</Button>
|
|
233
|
+
<Button
|
|
234
|
+
class="h-7 px-3 text-xs font-normal"
|
|
235
|
+
Icon={icons.FilePlus}
|
|
236
|
+
onclick={() => { handleFileUpload() }}
|
|
237
|
+
>
|
|
238
|
+
New file
|
|
239
|
+
</Button>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
<!-- file explorer body -->
|
|
243
|
+
<div class="flex flex-1 overflow-auto h-[calc(100vh-7.5rem)]">
|
|
244
|
+
<ContextMenu.Root bind:open={contextMenuOpen}>
|
|
245
|
+
<ContextMenu.Trigger
|
|
246
|
+
class="z-10 flex flex-1 flex-wrap content-start items-start justify-start gap-4 p-4"
|
|
247
|
+
onclick={handleSelect}
|
|
248
|
+
{oncontextmenucapture}
|
|
249
|
+
>
|
|
250
|
+
{#if loading}
|
|
251
|
+
<div
|
|
252
|
+
class="flex h-full w-full items-center justify-center"
|
|
253
|
+
>
|
|
254
|
+
<div
|
|
255
|
+
class="flex h-full w-full flex-col items-center justify-center gap-4 text-muted-foreground"
|
|
256
|
+
>
|
|
257
|
+
<icons.LoaderCircle
|
|
258
|
+
class="opacity-50 animate-spin"
|
|
259
|
+
size="50"
|
|
260
|
+
/>
|
|
261
|
+
<div
|
|
262
|
+
class="flex flex-col items-center justify-center"
|
|
263
|
+
>
|
|
264
|
+
<div>Loading files and directories, please wait...</div>
|
|
265
|
+
<div class="text-xs">
|
|
266
|
+
Fetching the contents of the current folder from the lobb server.
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
{:else}
|
|
272
|
+
{#if entries && entries.length}
|
|
273
|
+
{#each entries as entry}
|
|
274
|
+
{@const selectedCss =
|
|
275
|
+
selectedEntry?.id === entry.id
|
|
276
|
+
? "bg-opacity-25 hover:bg-opacity-25"
|
|
277
|
+
: "hover:bg-opacity-10"}
|
|
278
|
+
<button
|
|
279
|
+
class="w-32 cursor-default rounded-md bg-muted-foreground bg-opacity-0 p-2 {selectedCss}"
|
|
280
|
+
data-id={entry.id}
|
|
281
|
+
ondblclick={() =>
|
|
282
|
+
handleEntryDoubleClick(entry)}
|
|
283
|
+
>
|
|
284
|
+
{#if entry.type === "directory"}
|
|
285
|
+
<div
|
|
286
|
+
class="pointer-events-none flex min-w-28 flex-col items-center"
|
|
287
|
+
>
|
|
288
|
+
<div
|
|
289
|
+
class="pointer-events-none relative"
|
|
290
|
+
>
|
|
291
|
+
<icons.Folder
|
|
292
|
+
size="50"
|
|
293
|
+
class="fill-muted-foreground stroke-none"
|
|
294
|
+
/>
|
|
295
|
+
{#if entry.icon}
|
|
296
|
+
{@const key = entry.icon}
|
|
297
|
+
{/* @ts-ignore */ null}
|
|
298
|
+
{@const Icon = icons[key]}
|
|
299
|
+
<div
|
|
300
|
+
class="absolute left-0 top-0 flex h-full w-full items-center justify-center"
|
|
301
|
+
>
|
|
302
|
+
<Icon
|
|
303
|
+
class="text-primary-foreground"
|
|
304
|
+
style="transform: translateY(0.1rem);"
|
|
305
|
+
size="15"
|
|
306
|
+
/>
|
|
307
|
+
</div>
|
|
308
|
+
{/if}
|
|
309
|
+
</div>
|
|
310
|
+
<div
|
|
311
|
+
class="pointer-events-none w-full break-words text-center text-sm font-medium"
|
|
312
|
+
>
|
|
313
|
+
{entry.name}
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
{:else}
|
|
317
|
+
<FileIcon {...props} {entry} />
|
|
318
|
+
{/if}
|
|
319
|
+
</button>
|
|
320
|
+
{/each}
|
|
321
|
+
{:else}
|
|
322
|
+
<div
|
|
323
|
+
class="flex h-full w-full items-center justify-center p-8"
|
|
324
|
+
>
|
|
325
|
+
<div
|
|
326
|
+
class="flex h-full w-full flex-col items-center justify-center gap-4 text-muted-foreground"
|
|
327
|
+
>
|
|
328
|
+
<icons.CircleSlash2
|
|
329
|
+
class="opacity-50"
|
|
330
|
+
size="50"
|
|
331
|
+
/>
|
|
332
|
+
<div
|
|
333
|
+
class="flex flex-col items-center justify-center"
|
|
334
|
+
>
|
|
335
|
+
<div>Folder is empty</div>
|
|
336
|
+
<div class="text-xs">
|
|
337
|
+
Right-click to create a new folder
|
|
338
|
+
or upload a file.
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
{/if}
|
|
344
|
+
{/if}
|
|
345
|
+
</ContextMenu.Trigger>
|
|
346
|
+
<ContextMenu.Content
|
|
347
|
+
class="z-10 flex flex-col rounded-md border border-muted bg-background p-1 shadow-md outline-none w-40"
|
|
348
|
+
>
|
|
349
|
+
{#if contextType === "directory"}
|
|
350
|
+
<Button
|
|
351
|
+
variant="ghost"
|
|
352
|
+
onclick={handleDeleteEntry}
|
|
353
|
+
class="flex items-center gap-2 rounded-md px-4 h-8 hover:bg-muted focus-visible:ring-0 cursor-default font-normal"
|
|
354
|
+
>
|
|
355
|
+
Delete directory
|
|
356
|
+
</Button>
|
|
357
|
+
{:else if contextType === "file"}
|
|
358
|
+
<Button
|
|
359
|
+
variant="ghost"
|
|
360
|
+
onclick={handleDeleteEntry}
|
|
361
|
+
class="rounded-md px-4 h-8 hover:bg-muted focus-visible:ring-0 cursor-default font-normal"
|
|
362
|
+
>
|
|
363
|
+
Delete file
|
|
364
|
+
</Button>
|
|
365
|
+
{:else}
|
|
366
|
+
<Button
|
|
367
|
+
variant="ghost"
|
|
368
|
+
onclick={handleCreateFolder}
|
|
369
|
+
class="flex items-center gap-2 rounded-md px-4 h-8 hover:bg-muted focus-visible:ring-0 cursor-default font-normal"
|
|
370
|
+
>
|
|
371
|
+
New folder
|
|
372
|
+
</Button>
|
|
373
|
+
<Button
|
|
374
|
+
variant="ghost"
|
|
375
|
+
onclick={handleFileUpload}
|
|
376
|
+
class="flex items-center gap-2 rounded-md px-4 h-8 hover:bg-muted focus-visible:ring-0 cursor-default font-normal"
|
|
377
|
+
>
|
|
378
|
+
New file
|
|
379
|
+
</Button>
|
|
380
|
+
{/if}
|
|
381
|
+
</ContextMenu.Content>
|
|
382
|
+
</ContextMenu.Root>
|
|
383
|
+
</div>
|
|
384
|
+
<div
|
|
385
|
+
class="flex h-10 items-center justify-center border-t px-4 text-sm text-muted-foreground"
|
|
386
|
+
>
|
|
387
|
+
{#if loading}
|
|
388
|
+
<div class="flex gap-2 justify-center items-center">
|
|
389
|
+
<icons.LoaderCircle
|
|
390
|
+
class="animate-spin"
|
|
391
|
+
size="15"
|
|
392
|
+
/>
|
|
393
|
+
<div>loading...</div>
|
|
394
|
+
</div>
|
|
395
|
+
{:else}
|
|
396
|
+
{entries ? entries.length : "0"} items available
|
|
397
|
+
{/if}
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
export interface Entry {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
path: string;
|
|
6
|
+
type: "directory" | "file";
|
|
7
|
+
icon: string;
|
|
8
|
+
file_mime_type: string;
|
|
9
|
+
}
|
|
10
|
+
declare const __propDef: {
|
|
11
|
+
props: Record<string, never>;
|
|
12
|
+
events: {
|
|
13
|
+
[evt: string]: CustomEvent<any>;
|
|
14
|
+
};
|
|
15
|
+
slots: {};
|
|
16
|
+
};
|
|
17
|
+
export type FileExplorerProps = typeof __propDef.props;
|
|
18
|
+
export type FileExplorerEvents = typeof __propDef.events;
|
|
19
|
+
export type FileExplorerSlots = typeof __propDef.slots;
|
|
20
|
+
export default class FileExplorer extends SvelteComponentTyped<FileExplorerProps, FileExplorerEvents, FileExplorerSlots> {
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "@lobb-js/studio";
|
|
3
|
+
import type { Entry } from "./fileExplorer.svelte";
|
|
4
|
+
|
|
5
|
+
interface Props extends ExtensionProps {
|
|
6
|
+
entry: Entry;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { entry, utils }: Props = $props();
|
|
10
|
+
|
|
11
|
+
const MainIcon = utils.components.Icons.File;
|
|
12
|
+
const InnerIcon = utils.components.Icons.Text;
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
class="pointer-events-none flex flex-col items-center gap-2 min-w-28"
|
|
17
|
+
>
|
|
18
|
+
{#if entry.file_mime_type.startsWith("image/")}
|
|
19
|
+
<div class="rounded-md w-24 h-24 bg-center bg-contain bg-no-repeat" style="background-image: url('{utils.lobb.lobbUrl}/api/collections/storage_fs/{entry.id}?action=view');"></div>
|
|
20
|
+
{:else}
|
|
21
|
+
<div
|
|
22
|
+
class="pointer-events-none relative"
|
|
23
|
+
>
|
|
24
|
+
<MainIcon
|
|
25
|
+
size="50"
|
|
26
|
+
class="fill-muted stroke-muted-foreground"
|
|
27
|
+
style="stroke-width: 0.03rem;"
|
|
28
|
+
/>
|
|
29
|
+
<div
|
|
30
|
+
class="absolute left-0 top-0 flex h-full w-full items-center justify-center"
|
|
31
|
+
>
|
|
32
|
+
<InnerIcon
|
|
33
|
+
class="text-muted-foreground"
|
|
34
|
+
style="transform: translateY(0.1rem);"
|
|
35
|
+
size="15"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
{/if}
|
|
40
|
+
<div
|
|
41
|
+
class="pointer-events-none w-full break-words text-center text-sm font-medium"
|
|
42
|
+
>
|
|
43
|
+
{entry.name}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: Record<string, never>;
|
|
4
|
+
events: {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
};
|
|
7
|
+
slots: {};
|
|
8
|
+
};
|
|
9
|
+
export type FileIconProps = typeof __propDef.props;
|
|
10
|
+
export type FileIconEvents = typeof __propDef.events;
|
|
11
|
+
export type FileIconSlots = typeof __propDef.slots;
|
|
12
|
+
export default class FileIcon extends SvelteComponentTyped<FileIconProps, FileIconEvents, FileIconSlots> {
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "@lobb-js/studio";
|
|
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,14 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: Record<string, never>;
|
|
4
|
+
events: {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
};
|
|
7
|
+
slots: {};
|
|
8
|
+
};
|
|
9
|
+
export type FileManagerBreadCrumbsProps = typeof __propDef.props;
|
|
10
|
+
export type FileManagerBreadCrumbsEvents = typeof __propDef.events;
|
|
11
|
+
export type FileManagerBreadCrumbsSlots = typeof __propDef.slots;
|
|
12
|
+
export default class FileManagerBreadCrumbs extends SvelteComponentTyped<FileManagerBreadCrumbsProps, FileManagerBreadCrumbsEvents, FileManagerBreadCrumbsSlots> {
|
|
13
|
+
}
|
|
14
|
+
export {};
|