@makolabs/ripple 0.0.1-dev.8 → 0.0.1-dev.81
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/README.md +1 -1
- package/dist/adapters/storage/BaseAdapter.d.ts +20 -0
- package/dist/adapters/storage/BaseAdapter.js +171 -0
- package/dist/adapters/storage/S3Adapter.d.ts +21 -0
- package/dist/adapters/storage/S3Adapter.js +194 -0
- package/dist/adapters/storage/index.d.ts +3 -0
- package/dist/adapters/storage/index.js +3 -0
- package/dist/adapters/storage/types.d.ts +102 -0
- package/dist/adapters/storage/types.js +4 -0
- package/dist/charts/Chart.svelte +59 -47
- package/dist/charts/Chart.svelte.d.ts +1 -1
- package/dist/drawer/drawer.js +3 -3
- package/dist/elements/accordion/Accordion.svelte +98 -0
- package/dist/elements/accordion/Accordion.svelte.d.ts +4 -0
- package/dist/elements/accordion/accordion.d.ts +227 -0
- package/dist/elements/accordion/accordion.js +138 -0
- package/dist/elements/alert/Alert.svelte +7 -3
- package/dist/elements/dropdown/Dropdown.svelte +74 -107
- package/dist/elements/dropdown/Select.svelte +81 -62
- package/dist/elements/dropdown/dropdown.js +1 -1
- package/dist/elements/dropdown/select.js +8 -8
- package/dist/elements/file-upload/FileUpload.svelte +17 -95
- package/dist/elements/file-upload/FilesPreview.svelte +93 -0
- package/dist/elements/file-upload/FilesPreview.svelte.d.ts +4 -0
- package/dist/elements/progress/Progress.svelte +83 -25
- package/dist/file-browser/FileBrowser.svelte +837 -0
- package/dist/file-browser/FileBrowser.svelte.d.ts +14 -0
- package/dist/file-browser/index.d.ts +1 -0
- package/dist/file-browser/index.js +1 -0
- package/dist/filters/CompactFilters.svelte +147 -0
- package/dist/filters/CompactFilters.svelte.d.ts +4 -0
- package/dist/filters/index.d.ts +1 -0
- package/dist/filters/index.js +1 -0
- package/dist/forms/Checkbox.svelte +2 -2
- package/dist/forms/DateRange.svelte +21 -21
- package/dist/forms/Input.svelte +3 -3
- package/dist/forms/NumberInput.svelte +1 -1
- package/dist/forms/RadioInputs.svelte +3 -3
- package/dist/forms/Tags.svelte +5 -5
- package/dist/forms/Toggle.svelte +3 -3
- package/dist/forms/slider.js +4 -4
- package/dist/header/PageHeader.svelte +49 -11
- package/dist/index.d.ts +256 -143
- package/dist/index.js +19 -2
- package/dist/layout/card/MetricCard.svelte +64 -0
- package/dist/layout/card/MetricCard.svelte.d.ts +4 -0
- package/dist/layout/card/StatsCard.svelte +4 -3
- package/dist/layout/card/StatsCard.svelte.d.ts +1 -1
- package/dist/layout/card/metric-card.d.ts +49 -0
- package/dist/layout/card/metric-card.js +10 -0
- package/dist/layout/card/stats-card.d.ts +0 -15
- package/dist/layout/card/stats-card.js +1 -1
- package/dist/layout/sidebar/NavGroup.svelte +8 -9
- package/dist/layout/sidebar/NavItem.svelte +2 -2
- package/dist/layout/sidebar/Sidebar.svelte +102 -49
- package/dist/layout/table/Table.svelte +464 -87
- package/dist/layout/table/Table.svelte.d.ts +1 -1
- package/dist/layout/table/table.d.ts +0 -47
- package/dist/layout/table/table.js +0 -8
- package/dist/layout/tabs/Tab.svelte +9 -6
- package/dist/layout/tabs/Tab.svelte.d.ts +1 -1
- package/dist/layout/tabs/TabContent.svelte +1 -2
- package/dist/layout/tabs/TabContent.svelte.d.ts +1 -1
- package/dist/layout/tabs/TabGroup.svelte +10 -5
- package/dist/layout/tabs/TabGroup.svelte.d.ts +2 -2
- package/dist/layout/tabs/tabs.d.ts +61 -76
- package/dist/layout/tabs/tabs.js +170 -28
- package/dist/modal/Modal.svelte +3 -3
- package/dist/modal/modal.js +3 -3
- package/dist/utils/Portal.svelte +108 -0
- package/dist/utils/Portal.svelte.d.ts +8 -0
- package/dist/utils/dateUtils.d.ts +7 -0
- package/dist/utils/dateUtils.js +26 -0
- package/dist/variants.d.ts +11 -1
- package/dist/variants.js +17 -0
- package/package.json +2 -2
- package/dist/header/pageheaders.d.ts +0 -10
- package/dist/header/pageheaders.js +0 -1
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { Button, Table, Color, Size } from '../index.js';
|
|
4
|
+
import type { TableColumn } from '../index.js';
|
|
5
|
+
import { formatDate } from '../utils/dateUtils.js';
|
|
6
|
+
import type {
|
|
7
|
+
StorageAdapter,
|
|
8
|
+
FileItem,
|
|
9
|
+
Breadcrumb,
|
|
10
|
+
FileAction,
|
|
11
|
+
FileActionBatch,
|
|
12
|
+
FileActionSingle
|
|
13
|
+
} from '../adapters/storage/index.js';
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
adapter,
|
|
17
|
+
startPath = '',
|
|
18
|
+
actions = [],
|
|
19
|
+
infoSection,
|
|
20
|
+
selectedFiles = $bindable([])
|
|
21
|
+
} = $props<{
|
|
22
|
+
adapter: StorageAdapter;
|
|
23
|
+
startPath?: string;
|
|
24
|
+
actions?: FileAction[];
|
|
25
|
+
selectedFiles?: string[];
|
|
26
|
+
infoSection?: (props: {
|
|
27
|
+
selectedFiles: string[];
|
|
28
|
+
navToFileFolder: (fileKey: string) => void;
|
|
29
|
+
}) => any;
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
let files = $state<FileItem[]>([]);
|
|
33
|
+
let displayFiles = $state<FileItem[]>([]);
|
|
34
|
+
let currentPath = $state(startPath || '');
|
|
35
|
+
let isLoading = $state(true);
|
|
36
|
+
let isAuthenticated = $state(false);
|
|
37
|
+
let error = $state<string | null>(null);
|
|
38
|
+
let breadcrumbs = $state<Breadcrumb[]>([]);
|
|
39
|
+
let searchQuery = $state('');
|
|
40
|
+
|
|
41
|
+
let sortState = $state<{ column: string | null; direction: 'asc' | 'desc' | 'default' | null }>({
|
|
42
|
+
column: null,
|
|
43
|
+
direction: null
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Add state for folder traversal
|
|
47
|
+
let isFetchingRecursively = $state(false);
|
|
48
|
+
let fileQueue = $state<FileItem[]>([]);
|
|
49
|
+
let processedFolders = $state<Set<string>>(new Set());
|
|
50
|
+
let fetchingFolderName = $state<string | null>(null);
|
|
51
|
+
|
|
52
|
+
// Format file size in human readable format
|
|
53
|
+
function formatFileSize(bytes: number): string {
|
|
54
|
+
if (bytes === 0) return '0 B';
|
|
55
|
+
const k = 1024;
|
|
56
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
57
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
58
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Function that determines if a batch action should be shown
|
|
62
|
+
function isBatchActionAllowed(action: FileAction): action is FileActionBatch {
|
|
63
|
+
if (isFetchingRecursively) return false;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
'batchAction' in action &&
|
|
67
|
+
typeof action.batchAction === 'function' &&
|
|
68
|
+
action.isAllowed(allFilesAcquired)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Function that determines if a single action should be shown for a file
|
|
73
|
+
function isSingleActionAllowed(action: FileAction, file: FileItem): action is FileActionSingle {
|
|
74
|
+
return 'action' in action && typeof action.action === 'function' && action.isAllowed(file);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function navigateUp() {
|
|
78
|
+
// Normalize currentPath and startPath by removing leading/trailing slashes
|
|
79
|
+
const normCurrent = currentPath.replace(/^\/+|\/+$/g, '');
|
|
80
|
+
const normStart = startPath.replace(/^\/+|\/+$/g, '');
|
|
81
|
+
|
|
82
|
+
if (normCurrent !== normStart) {
|
|
83
|
+
const segments = normCurrent.split('/');
|
|
84
|
+
segments.pop(); // Remove last segment
|
|
85
|
+
let newPath = segments.join('/');
|
|
86
|
+
if (!newPath) newPath = startPath || '';
|
|
87
|
+
navigateToFolder(newPath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function navToFileFolder(fileKey: string) {
|
|
92
|
+
// Get the parent folder path by removing the filename from the key
|
|
93
|
+
const parentPath = fileKey.substring(0, fileKey.lastIndexOf('/'));
|
|
94
|
+
|
|
95
|
+
// Check if we're already in this folder, don't navigate in that case
|
|
96
|
+
if (parentPath === currentPath) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Store the currently selected files before navigation
|
|
101
|
+
const currentSelection = [...selectedFiles];
|
|
102
|
+
|
|
103
|
+
// Make sure we preserve the selection of the file that triggered this navigation
|
|
104
|
+
if (!currentSelection.includes(fileKey)) {
|
|
105
|
+
currentSelection.push(fileKey);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Navigate to folder, then restore selection
|
|
109
|
+
navigateToFolder(parentPath);
|
|
110
|
+
|
|
111
|
+
// In case the navigateToFolder resets selection, ensure this file is still selected
|
|
112
|
+
// after a short delay to allow the folder contents to load
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
if (!selectedFiles.includes(fileKey)) {
|
|
115
|
+
selectedFiles = [...selectedFiles, fileKey];
|
|
116
|
+
|
|
117
|
+
// Update table selection as well
|
|
118
|
+
const fileItem = files.find((f) => f.key === fileKey);
|
|
119
|
+
if (fileItem && !selected.some((s) => s.key === fileKey)) {
|
|
120
|
+
selected = [...selected, fileItem];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}, 100);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// On component mount, check auth status and list files
|
|
127
|
+
onMount(async () => {
|
|
128
|
+
await checkAdapterConfiguration();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
async function checkAdapterConfiguration() {
|
|
132
|
+
try {
|
|
133
|
+
isAuthenticated = await adapter.isConfigured();
|
|
134
|
+
|
|
135
|
+
if (isAuthenticated) {
|
|
136
|
+
await listFiles(currentPath || startPath);
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error('Error checking adapter configuration:', err);
|
|
140
|
+
isAuthenticated = false;
|
|
141
|
+
error = 'Failed to initialize storage adapter';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function authenticate() {
|
|
146
|
+
if (adapter.authenticate) {
|
|
147
|
+
try {
|
|
148
|
+
// Use the adapter's method to set the reopening flag
|
|
149
|
+
if (adapter.setReopenFlag) {
|
|
150
|
+
adapter.setReopenFlag();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await adapter.authenticate();
|
|
154
|
+
// The adapter's authenticate method might redirect, so this may not complete
|
|
155
|
+
isAuthenticated = await adapter.isConfigured();
|
|
156
|
+
if (isAuthenticated) {
|
|
157
|
+
await listFiles(currentPath || startPath);
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error('Authentication error:', err);
|
|
161
|
+
error = 'Failed to authenticate with storage provider';
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
error = 'This storage provider does not support authentication';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Loading files using the adapter
|
|
169
|
+
async function listFiles(path: string) {
|
|
170
|
+
isLoading = true;
|
|
171
|
+
error = null;
|
|
172
|
+
|
|
173
|
+
// Remember the previous selection before loading new files
|
|
174
|
+
const previousSelectedFiles = [...selectedFiles];
|
|
175
|
+
const previousFileQueue = [...fileQueue];
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// Only pass the searchQuery if it's not empty
|
|
179
|
+
const result = await adapter.list(path, searchQuery.trim() ? searchQuery : undefined);
|
|
180
|
+
|
|
181
|
+
files = result.files;
|
|
182
|
+
currentPath = result.currentPath;
|
|
183
|
+
|
|
184
|
+
if (result.breadcrumbs) {
|
|
185
|
+
breadcrumbs = result.breadcrumbs;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Default sort: folders alphabetically, files by date (newest first)
|
|
189
|
+
sortState = { column: 'default', direction: 'default' };
|
|
190
|
+
displayFiles = getSortedFiles(files);
|
|
191
|
+
|
|
192
|
+
// Retain selected files that are still valid
|
|
193
|
+
// We need to filter out any folder paths that might no longer exist
|
|
194
|
+
selectedFiles = previousSelectedFiles;
|
|
195
|
+
fileQueue = previousFileQueue;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error('Error fetching files:', err);
|
|
198
|
+
error = err instanceof Error ? err.message : 'An unknown error occurred';
|
|
199
|
+
} finally {
|
|
200
|
+
isLoading = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function navigateToFolder(folderPath: string) {
|
|
205
|
+
// If we have the file info for this folder, cache its name in the adapter
|
|
206
|
+
// before navigating (useful for Google Drive adapter)
|
|
207
|
+
const folderFile = files.find((file) => file.key === folderPath);
|
|
208
|
+
if (folderFile && folderFile.name && adapter.getName() === 'Google Drive') {
|
|
209
|
+
// Assuming the adapter might have a setFolderName method
|
|
210
|
+
(adapter as any).setFolderName?.(folderPath, folderFile.name);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Store the currently selected files before navigation
|
|
214
|
+
const previouslySelectedFiles = [...selectedFiles];
|
|
215
|
+
|
|
216
|
+
// Figure out if we're navigating back up to a parent
|
|
217
|
+
const isNavigatingUp = currentPath.includes(folderPath) && currentPath !== folderPath;
|
|
218
|
+
|
|
219
|
+
// If we're navigating up, make sure to keep the folder we're coming from selected
|
|
220
|
+
const shouldKeepCurrent = isNavigatingUp && currentPath;
|
|
221
|
+
|
|
222
|
+
// Keep track of existing fileQueue
|
|
223
|
+
const existingQueue = [...fileQueue];
|
|
224
|
+
|
|
225
|
+
// Navigate to the folder
|
|
226
|
+
listFiles(folderPath).then(() => {
|
|
227
|
+
// After navigation is complete, restore the selection
|
|
228
|
+
let filesToSelect = [];
|
|
229
|
+
|
|
230
|
+
// If navigating up, keep the current folder selected
|
|
231
|
+
if (shouldKeepCurrent) {
|
|
232
|
+
// Find the folder we just came from
|
|
233
|
+
const currentFolder = files.find(
|
|
234
|
+
(f) => f.key === currentPath || (f.isFolder && currentPath.startsWith(f.key + '/'))
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (currentFolder) {
|
|
238
|
+
filesToSelect.push(currentFolder.key);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Also keep previously selected files that are still visible
|
|
243
|
+
const visibleSelectedFiles = previouslySelectedFiles.filter((key) =>
|
|
244
|
+
files.some((file) => file.key === key)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
filesToSelect = [...new Set([...filesToSelect, ...visibleSelectedFiles])];
|
|
248
|
+
|
|
249
|
+
// Update selection
|
|
250
|
+
if (filesToSelect.length > 0) {
|
|
251
|
+
selectedFiles = filesToSelect;
|
|
252
|
+
selected = displayFiles.filter((file) => filesToSelect.includes(file.key));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Restore fileQueue to ensure we keep track of all selected files
|
|
256
|
+
// Only keep files that are still selected
|
|
257
|
+
fileQueue = [...existingQueue];
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function handleRowClick(file: FileItem) {
|
|
262
|
+
if (file.isFolder) {
|
|
263
|
+
navigateToFolder(file.key);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Sort files based on current sort state
|
|
268
|
+
function getSortedFiles(filesArray: FileItem[]): FileItem[] {
|
|
269
|
+
// Separate folders and files
|
|
270
|
+
const folders = filesArray.filter((file) => file.isFolder);
|
|
271
|
+
const files = filesArray.filter((file) => !file.isFolder);
|
|
272
|
+
|
|
273
|
+
// Handle default sort (folders alphabetically, files by date newest first)
|
|
274
|
+
if (sortState.column === 'default' && sortState.direction === 'default') {
|
|
275
|
+
// Sort folders alphabetically (A-Z)
|
|
276
|
+
const sortedFolders = [...folders].sort((a, b) => a.name.localeCompare(b.name));
|
|
277
|
+
|
|
278
|
+
// Sort files by date (newest first)
|
|
279
|
+
const sortedFiles = [...files].sort((a, b) => {
|
|
280
|
+
// Use createdAt if available, otherwise fallback to lastModified
|
|
281
|
+
const aTime = a.createdAt ? a.createdAt.getTime() : a.lastModified.getTime();
|
|
282
|
+
const bTime = b.createdAt ? b.createdAt.getTime() : b.lastModified.getTime();
|
|
283
|
+
return bTime - aTime; // Descending order (newest first)
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Return folders first, then files
|
|
287
|
+
return [...sortedFolders, ...sortedFiles];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// For user-specified sorting
|
|
291
|
+
// If no sort specified, default to sorting by name
|
|
292
|
+
if (!sortState.column || !sortState.direction) {
|
|
293
|
+
sortState = { column: 'name', direction: 'asc' };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Create a comparison function based on the current sort state
|
|
297
|
+
const compareFiles = (a: FileItem, b: FileItem): number => {
|
|
298
|
+
let comparison = 0;
|
|
299
|
+
|
|
300
|
+
switch (sortState.column) {
|
|
301
|
+
case 'name':
|
|
302
|
+
comparison = a.name.localeCompare(b.name);
|
|
303
|
+
break;
|
|
304
|
+
case 'size':
|
|
305
|
+
comparison = a.size - b.size;
|
|
306
|
+
break;
|
|
307
|
+
case 'lastModified':
|
|
308
|
+
comparison = a.lastModified.getTime() - b.lastModified.getTime();
|
|
309
|
+
break;
|
|
310
|
+
default:
|
|
311
|
+
// Default to sorting by name
|
|
312
|
+
return a.name.localeCompare(b.name);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Reverse for descending order
|
|
316
|
+
return sortState.direction === 'asc' ? comparison : -comparison;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Sort folders and files separately
|
|
320
|
+
const sortedFolders = [...folders].sort(compareFiles);
|
|
321
|
+
const sortedFiles = [...files].sort(compareFiles);
|
|
322
|
+
|
|
323
|
+
// Return sorted folders followed by sorted files
|
|
324
|
+
return [...sortedFolders, ...sortedFiles];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Handle sort column click
|
|
328
|
+
function handleSort(newSortState: { column: string | null; direction: 'asc' | 'desc' | null }) {
|
|
329
|
+
// Update the component's sort state
|
|
330
|
+
sortState = {
|
|
331
|
+
column: newSortState.column,
|
|
332
|
+
direction: newSortState.direction
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// Update the display files with the new sort
|
|
336
|
+
displayFiles = getSortedFiles(files);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Handle search
|
|
340
|
+
function handleSearch() {
|
|
341
|
+
// Make sure we're actually using the current searchQuery value
|
|
342
|
+
listFiles(currentPath);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Clear the search and reset results
|
|
346
|
+
function clearSearch() {
|
|
347
|
+
// Only refresh if there was a previous search
|
|
348
|
+
if (searchQuery) {
|
|
349
|
+
searchQuery = '';
|
|
350
|
+
listFiles(currentPath);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Effect to update the Table component's selection state when selectedFiles changes
|
|
355
|
+
$effect(() => {
|
|
356
|
+
if (displayFiles.length === 0) return;
|
|
357
|
+
|
|
358
|
+
// Get all files that should be selected based on our current selection state
|
|
359
|
+
const filesToSelect = displayFiles.filter((file) => isRowSelected(file));
|
|
360
|
+
|
|
361
|
+
// We need to update the Table's internal selection state without triggering additional updates
|
|
362
|
+
// This ensures the UI is properly updated
|
|
363
|
+
if (
|
|
364
|
+
JSON.stringify(selected.map((f) => f.key).sort()) !==
|
|
365
|
+
JSON.stringify(filesToSelect.map((f) => f.key).sort())
|
|
366
|
+
) {
|
|
367
|
+
// Only update if the selection has actually changed
|
|
368
|
+
selected = filesToSelect;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Track the selected items for the Table component's internal state
|
|
373
|
+
let selected = $state<FileItem[]>([]);
|
|
374
|
+
|
|
375
|
+
// Derived: all files to import (not folders), including recursively fetched
|
|
376
|
+
const allFilesAcquired = $derived.by(() => {
|
|
377
|
+
const selectedFileKeys = new Set(selectedFiles);
|
|
378
|
+
|
|
379
|
+
// Get files from current view that are selected and not folders
|
|
380
|
+
const selectedCurrentFiles = displayFiles.filter(
|
|
381
|
+
(f) => selectedFileKeys.has(f.key) && !f.isFolder
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// Get files from the queue (these are explicitly added when folders are processed)
|
|
385
|
+
const queuedFiles = fileQueue.filter((f) => !f.isFolder);
|
|
386
|
+
|
|
387
|
+
// Combine both sources
|
|
388
|
+
const allFiles = [...selectedCurrentFiles, ...queuedFiles];
|
|
389
|
+
|
|
390
|
+
// Remove duplicates by key
|
|
391
|
+
return Array.from(new Map(allFiles.map((f) => [f.key, f])).values());
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Ensure our selectedFiles state is updated when the Table selection changes
|
|
395
|
+
function onselect(selectedItems: FileItem[]) {
|
|
396
|
+
// Filter out files that aren't allowed to be selected based on actions' isAllowed logic
|
|
397
|
+
const allowedItems = selectedItems.filter((item) => {
|
|
398
|
+
// Check if any single action allows this file
|
|
399
|
+
const hasAllowedAction = actions.some((action: FileAction) => {
|
|
400
|
+
// Only check single file actions for individual file selection
|
|
401
|
+
if ('action' in action && typeof action.action === 'function') {
|
|
402
|
+
return action.isAllowed(item);
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Always allow folders to be selected (for recursive selection)
|
|
408
|
+
return item.isFolder || hasAllowedAction;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Get keys from allowed selected items
|
|
412
|
+
const newSelectedKeys = allowedItems.map((item) => item.key);
|
|
413
|
+
|
|
414
|
+
// Update the global selection state with only allowed items
|
|
415
|
+
handleSelectByKeys(newSelectedKeys, allowedItems);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Handle selection based on keys to avoid redundant code
|
|
419
|
+
function handleSelectByKeys(keys: string[], items: FileItem[] = []) {
|
|
420
|
+
// Get folder items from the selection
|
|
421
|
+
const folderItems = items.filter((item) => item.isFolder);
|
|
422
|
+
|
|
423
|
+
// Update the selectedFiles array
|
|
424
|
+
selectedFiles = keys;
|
|
425
|
+
|
|
426
|
+
// Process any newly selected folders recursively to get their contents
|
|
427
|
+
const newFolders = folderItems.filter((folder) => !processedFolders.has(folder.key));
|
|
428
|
+
newFolders.forEach((folder) => {
|
|
429
|
+
handleSelectFolder(folder);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Update select all state
|
|
433
|
+
const nonFolderFiles = displayFiles.filter((file) => !file.isFolder);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function fetchFilesFromFolder(folderKey: string, folderName: string) {
|
|
437
|
+
if (processedFolders.has(folderKey)) return;
|
|
438
|
+
|
|
439
|
+
fetchingFolderName = folderName;
|
|
440
|
+
processedFolders.add(folderKey);
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
// Use the adapter to list files in this folder
|
|
444
|
+
const result = await adapter.list(folderKey);
|
|
445
|
+
|
|
446
|
+
// Process files in this folder
|
|
447
|
+
const folderItems = result.files || [];
|
|
448
|
+
|
|
449
|
+
// Add non-folder files to selection
|
|
450
|
+
folderItems.forEach((item: FileItem) => {
|
|
451
|
+
if (!item.isFolder) {
|
|
452
|
+
if (!selectedFiles.includes(item.key)) {
|
|
453
|
+
selectedFiles = [...selectedFiles, item.key];
|
|
454
|
+
}
|
|
455
|
+
// Add to fileQueue for tracking
|
|
456
|
+
fileQueue = [...fileQueue, item];
|
|
457
|
+
} else {
|
|
458
|
+
// Add folders to queue for reference
|
|
459
|
+
fileQueue = [...fileQueue, item];
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Return any folders found for further processing
|
|
464
|
+
return folderItems.filter((item: FileItem) => item.isFolder);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
console.error(`Error fetching files from folder ${folderName}:`, err);
|
|
467
|
+
// Note: In a real app, you'd want to import a toast library
|
|
468
|
+
console.error(`Failed to fetch files from ${folderName}`);
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Handle selecting a folder to process recursively
|
|
474
|
+
async function handleSelectFolder(folder: FileItem) {
|
|
475
|
+
if (!folder.isFolder) return;
|
|
476
|
+
|
|
477
|
+
isFetchingRecursively = true;
|
|
478
|
+
processedFolders = new Set();
|
|
479
|
+
// Don't clear fileQueue entirely, it will lose files from other folders
|
|
480
|
+
// Store existing fileQueue
|
|
481
|
+
const existingQueue = [...fileQueue];
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
// Start with the selected folder
|
|
485
|
+
let foldersToProcess = [folder];
|
|
486
|
+
|
|
487
|
+
// Process folders breadth-first
|
|
488
|
+
while (foldersToProcess.length > 0) {
|
|
489
|
+
const currentFolder = foldersToProcess.shift();
|
|
490
|
+
if (!currentFolder) continue;
|
|
491
|
+
|
|
492
|
+
const subFolders = await fetchFilesFromFolder(currentFolder.key, currentFolder.name);
|
|
493
|
+
|
|
494
|
+
if (subFolders && subFolders.length > 0) {
|
|
495
|
+
foldersToProcess.push(...subFolders);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Merge new files with existing queue, avoiding duplicates
|
|
500
|
+
fileQueue = [
|
|
501
|
+
...existingQueue,
|
|
502
|
+
...fileQueue.filter(
|
|
503
|
+
(newItem) => !existingQueue.some((existingItem) => existingItem.key === newItem.key)
|
|
504
|
+
)
|
|
505
|
+
];
|
|
506
|
+
|
|
507
|
+
console.log(`Selected ${selectedFiles.length} files from folders`);
|
|
508
|
+
} catch (err) {
|
|
509
|
+
console.error('Error in recursive folder processing:', err);
|
|
510
|
+
console.error('Failed to process some folders');
|
|
511
|
+
} finally {
|
|
512
|
+
isFetchingRecursively = false;
|
|
513
|
+
fetchingFolderName = null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Check if a file is within a selected folder by examining its key path
|
|
518
|
+
function isFileInSelectedFolder(fileKey: string): boolean {
|
|
519
|
+
if (!fileKey) return false;
|
|
520
|
+
|
|
521
|
+
// Iterate through selected folders and check if the file key starts with any selected folder's key
|
|
522
|
+
// This assumes folder paths are prefixes of their contained files
|
|
523
|
+
for (const selectedKey of selectedFiles) {
|
|
524
|
+
const selectedFile =
|
|
525
|
+
files.find((f) => f.key === selectedKey) || fileQueue.find((f) => f.key === selectedKey);
|
|
526
|
+
|
|
527
|
+
// Only check folders
|
|
528
|
+
if (selectedFile && selectedFile.isFolder) {
|
|
529
|
+
// If the file's key starts with the folder's key and has additional path elements, it's inside the folder
|
|
530
|
+
if (fileKey.startsWith(selectedKey) && fileKey !== selectedKey) {
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const columns: TableColumn<FileItem>[] = [
|
|
540
|
+
{
|
|
541
|
+
key: 'name',
|
|
542
|
+
header: 'Name',
|
|
543
|
+
cell: NameCell,
|
|
544
|
+
sortable: true
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
key: 'size',
|
|
548
|
+
header: 'Size',
|
|
549
|
+
cell: SizeCell,
|
|
550
|
+
sortable: true
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
key: 'lastModified',
|
|
554
|
+
header: 'Last Modified',
|
|
555
|
+
cell: DateCell,
|
|
556
|
+
sortable: true
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
key: 'status',
|
|
560
|
+
header: 'Status',
|
|
561
|
+
cell: StatusCell
|
|
562
|
+
}
|
|
563
|
+
];
|
|
564
|
+
|
|
565
|
+
async function handleUnauthenticated() {
|
|
566
|
+
isLoading = true;
|
|
567
|
+
|
|
568
|
+
try {
|
|
569
|
+
// Use the adapter's method to set the reopening flag
|
|
570
|
+
if (adapter.setReopenFlag) {
|
|
571
|
+
adapter.setReopenFlag();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Try to authenticate with the adapter
|
|
575
|
+
const authenticated = adapter.authenticate ? await adapter.authenticate() : false;
|
|
576
|
+
|
|
577
|
+
// If authentication failed, but not due to redirection, show error message
|
|
578
|
+
if (!authenticated) {
|
|
579
|
+
console.error('Failed to authenticate with storage provider');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Note: authentication usually redirects the page, so code below won't run
|
|
583
|
+
isLoading = false;
|
|
584
|
+
} catch (error) {
|
|
585
|
+
console.error('Authentication error:', error);
|
|
586
|
+
console.error('Authentication error');
|
|
587
|
+
isLoading = false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Handle "Not authorized" messages from the adapter by calling authenticate
|
|
592
|
+
$effect(() => {
|
|
593
|
+
if (error && error.includes('not authorized')) {
|
|
594
|
+
handleUnauthenticated();
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Calculate if a row should be treated as selected
|
|
599
|
+
function isRowSelected(row: FileItem): boolean {
|
|
600
|
+
return selectedFiles.includes(row.key) || isFileInSelectedFolder(row.key);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const batchActions = $derived(actions.filter(isBatchActionAllowed));
|
|
604
|
+
|
|
605
|
+
const singularActions = (file: FileItem): FileActionSingle[] => {
|
|
606
|
+
return actions.filter((a: FileAction) => isSingleActionAllowed(a, file)) as FileActionSingle[];
|
|
607
|
+
};
|
|
608
|
+
</script>
|
|
609
|
+
|
|
610
|
+
{#snippet StatusCell(file: FileItem)}
|
|
611
|
+
{#if file.isFolder}
|
|
612
|
+
<span class="text-default-500">-</span>
|
|
613
|
+
{:else}
|
|
614
|
+
<div class="flex items-center gap-2">
|
|
615
|
+
{#each singularActions(file) as action (action.label)}
|
|
616
|
+
{#if action.isAllowed(file)}
|
|
617
|
+
<Button size={Size.XS} onclick={() => action.action?.(file)}>{action.label(file)}</Button>
|
|
618
|
+
{/if}
|
|
619
|
+
{/each}
|
|
620
|
+
</div>
|
|
621
|
+
{/if}
|
|
622
|
+
{/snippet}
|
|
623
|
+
|
|
624
|
+
{#snippet NameCell(file: FileItem)}
|
|
625
|
+
<div class="flex items-center gap-2">
|
|
626
|
+
{#if file.isFolder}
|
|
627
|
+
<svg
|
|
628
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
629
|
+
width="16"
|
|
630
|
+
height="16"
|
|
631
|
+
viewBox="0 0 24 24"
|
|
632
|
+
class="text-amber-500"
|
|
633
|
+
>
|
|
634
|
+
<path
|
|
635
|
+
fill="currentColor"
|
|
636
|
+
d="M3.5 6.25V8h4.629a.75.75 0 0 0 .53-.22l1.53-1.53l-1.53-1.53a.75.75 0 0 0-.53-.22H5.25A1.75 1.75 0 0 0 3.5 6.25m-1.5 0A3.25 3.25 0 0 1 5.25 3h2.879a2.25 2.25 0 0 1 1.59.659L11.562 5.5h7.189A3.25 3.25 0 0 1 22 8.75v9A3.25 3.25 0 0 1 18.75 21H5.25A3.25 3.25 0 0 1 2 17.75zM3.5 9.5v8.25c0 .966.784 1.75 1.75 1.75h13.5a1.75 1.75 0 0 0 1.75-1.75v-9A1.75 1.75 0 0 0 18.75 7h-7.19L9.72 8.841a2.25 2.25 0 0 1-1.591.659z"
|
|
637
|
+
/>
|
|
638
|
+
</svg>
|
|
639
|
+
{:else}
|
|
640
|
+
<svg
|
|
641
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
642
|
+
width="16"
|
|
643
|
+
height="16"
|
|
644
|
+
viewBox="0 0 24 24"
|
|
645
|
+
class="text-blue-500"
|
|
646
|
+
>
|
|
647
|
+
<path
|
|
648
|
+
fill="currentColor"
|
|
649
|
+
d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414l-5.828-5.828A2 2 0 0 0 12.172 2zm-.5 2a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5zm11.88 4.5H14a.5.5 0 0 1-.5-.5V4.62z"
|
|
650
|
+
/>
|
|
651
|
+
</svg>
|
|
652
|
+
{/if}
|
|
653
|
+
<span class="font-medium">{file.name}</span>
|
|
654
|
+
</div>
|
|
655
|
+
{/snippet}
|
|
656
|
+
|
|
657
|
+
{#snippet SizeCell(file: FileItem)}
|
|
658
|
+
{#if file.isFolder}
|
|
659
|
+
<span class="text-default-500">-</span>
|
|
660
|
+
{:else}
|
|
661
|
+
<span class="font-mono">{formatFileSize(file.size)}</span>
|
|
662
|
+
{/if}
|
|
663
|
+
{/snippet}
|
|
664
|
+
|
|
665
|
+
{#snippet DateCell(file: FileItem)}
|
|
666
|
+
<span class="text-default-600">{formatDate(file.lastModified, 'DD.MM.YYYY HH:mm')}</span>
|
|
667
|
+
{/snippet}
|
|
668
|
+
|
|
669
|
+
<div class="relative flex h-[calc(100vh-100px)] px-0">
|
|
670
|
+
<div class="min-w-0 flex-1">
|
|
671
|
+
{#if !isAuthenticated && adapter.authenticate}
|
|
672
|
+
<div class="flex h-full flex-col items-center justify-center">
|
|
673
|
+
<div class="mb-4 text-center text-lg">
|
|
674
|
+
Authentication required to access {adapter.getName()}
|
|
675
|
+
</div>
|
|
676
|
+
<Button color={Color.PRIMARY} onclick={authenticate}>Authenticate</Button>
|
|
677
|
+
</div>
|
|
678
|
+
{:else}
|
|
679
|
+
<div class="mb-2 flex flex-wrap items-center justify-between border-b border-gray-100 pb-3">
|
|
680
|
+
<div class="flex flex-wrap items-center">
|
|
681
|
+
{#if breadcrumbs.length > 1}
|
|
682
|
+
<button
|
|
683
|
+
class="text-default-600 mr-1 rounded-full px-2 py-1 hover:bg-gray-100"
|
|
684
|
+
onclick={navigateUp}
|
|
685
|
+
title="Go up"
|
|
686
|
+
aria-label="Navigate to parent folder"
|
|
687
|
+
>
|
|
688
|
+
<svg
|
|
689
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
690
|
+
width="16"
|
|
691
|
+
height="16"
|
|
692
|
+
viewBox="0 0 24 24"
|
|
693
|
+
fill="none"
|
|
694
|
+
stroke="currentColor"
|
|
695
|
+
stroke-width="2"
|
|
696
|
+
stroke-linecap="round"
|
|
697
|
+
stroke-linejoin="round"
|
|
698
|
+
>
|
|
699
|
+
<path d="M19 12H5" />
|
|
700
|
+
<path d="M12 19l-7-7 7-7" />
|
|
701
|
+
</svg>
|
|
702
|
+
</button>
|
|
703
|
+
{/if}
|
|
704
|
+
<span class="text-default-400 mx-1 text-sm">/</span>
|
|
705
|
+
|
|
706
|
+
<div class="flex flex-wrap items-center">
|
|
707
|
+
{#each breadcrumbs as crumb, i}
|
|
708
|
+
{#if i > 0}
|
|
709
|
+
<span class="text-default-400 mx-1 text-sm">/</span>
|
|
710
|
+
{/if}
|
|
711
|
+
|
|
712
|
+
{#if crumb.clickable && !crumb.current}
|
|
713
|
+
<button
|
|
714
|
+
class="cursor-pointer border-0 bg-transparent px-1 py-0 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
715
|
+
onclick={() => navigateToFolder(crumb.path)}
|
|
716
|
+
>
|
|
717
|
+
{crumb.name}
|
|
718
|
+
</button>
|
|
719
|
+
{:else}
|
|
720
|
+
<span
|
|
721
|
+
class={`px-1 text-sm ${crumb.current ? 'font-semibold text-gray-800' : 'text-gray-600'}`}
|
|
722
|
+
>
|
|
723
|
+
{crumb.name}
|
|
724
|
+
</span>
|
|
725
|
+
{/if}
|
|
726
|
+
{/each}
|
|
727
|
+
</div>
|
|
728
|
+
</div>
|
|
729
|
+
|
|
730
|
+
<div class="flex items-center gap-1">
|
|
731
|
+
{#if isFetchingRecursively}
|
|
732
|
+
<div
|
|
733
|
+
class="flex items-center gap-2 rounded border border-amber-200 bg-amber-50 px-3 py-1 text-xs"
|
|
734
|
+
>
|
|
735
|
+
<div
|
|
736
|
+
class="h-3 w-3 animate-spin rounded-full border-2 border-amber-500 border-t-transparent"
|
|
737
|
+
></div>
|
|
738
|
+
<span>
|
|
739
|
+
{#if fetchingFolderName}
|
|
740
|
+
Fetching: <span class="font-medium">{fetchingFolderName}</span>
|
|
741
|
+
{:else}
|
|
742
|
+
Fetching files
|
|
743
|
+
{/if}
|
|
744
|
+
<span class="ml-1 font-medium text-amber-700">
|
|
745
|
+
({selectedFiles.length})
|
|
746
|
+
</span>
|
|
747
|
+
</span>
|
|
748
|
+
</div>
|
|
749
|
+
{:else}
|
|
750
|
+
{#each batchActions as action (action.label)}
|
|
751
|
+
<Button
|
|
752
|
+
color={Color.PRIMARY}
|
|
753
|
+
onclick={() => action.batchAction?.(allFilesAcquired)}
|
|
754
|
+
disabled={!action.isAllowed(allFilesAcquired)}
|
|
755
|
+
class="h-7 px-3 py-1 text-xs"
|
|
756
|
+
>
|
|
757
|
+
{action.label(allFilesAcquired)}
|
|
758
|
+
</Button>
|
|
759
|
+
{/each}
|
|
760
|
+
{/if}
|
|
761
|
+
<div class="flex items-center gap-1">
|
|
762
|
+
<div class="relative flex items-center">
|
|
763
|
+
<input
|
|
764
|
+
type="text"
|
|
765
|
+
class="h-7 w-40 rounded border border-gray-300 px-2 py-1 text-xs"
|
|
766
|
+
placeholder="Search files..."
|
|
767
|
+
value={searchQuery}
|
|
768
|
+
oninput={(e) => {
|
|
769
|
+
searchQuery = (e.target as HTMLInputElement).value;
|
|
770
|
+
}}
|
|
771
|
+
onkeydown={(e) => {
|
|
772
|
+
if (e.key === 'Enter') handleSearch();
|
|
773
|
+
}}
|
|
774
|
+
/>
|
|
775
|
+
{#if searchQuery}
|
|
776
|
+
<button
|
|
777
|
+
class="absolute right-1 text-xs text-gray-400 hover:text-gray-600"
|
|
778
|
+
onclick={clearSearch}
|
|
779
|
+
title="Clear search"
|
|
780
|
+
>
|
|
781
|
+
×
|
|
782
|
+
</button>
|
|
783
|
+
{/if}
|
|
784
|
+
</div>
|
|
785
|
+
<button
|
|
786
|
+
onclick={handleSearch}
|
|
787
|
+
class="rounded bg-gray-200 px-2 py-1 text-xs hover:bg-gray-300"
|
|
788
|
+
>
|
|
789
|
+
Search
|
|
790
|
+
</button>
|
|
791
|
+
</div>
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
|
|
795
|
+
{#if isLoading}
|
|
796
|
+
<div class="flex h-full items-center justify-center">
|
|
797
|
+
<div class="text-default-500">Loading files...</div>
|
|
798
|
+
</div>
|
|
799
|
+
{:else if error}
|
|
800
|
+
<div class="flex h-full flex-col items-center justify-center">
|
|
801
|
+
<div class="text-danger-500 mb-4">{error}</div>
|
|
802
|
+
<Button color={Color.PRIMARY} onclick={() => listFiles(currentPath)}>Retry</Button>
|
|
803
|
+
</div>
|
|
804
|
+
{:else if files.length === 0}
|
|
805
|
+
<div class="flex h-full items-center justify-center">
|
|
806
|
+
<div class="text-default-500">No files found in this directory</div>
|
|
807
|
+
</div>
|
|
808
|
+
{:else}
|
|
809
|
+
<Table
|
|
810
|
+
{columns}
|
|
811
|
+
data={displayFiles}
|
|
812
|
+
loading={isLoading}
|
|
813
|
+
bordered={false}
|
|
814
|
+
onrowclick={handleRowClick}
|
|
815
|
+
rowclass={(row) => {
|
|
816
|
+
let classes = row.isFolder ? 'hover:bg-amber-50 cursor-pointer' : 'hover:bg-blue-50';
|
|
817
|
+
if (isRowSelected(row)) {
|
|
818
|
+
classes += ' bg-primary-50';
|
|
819
|
+
}
|
|
820
|
+
return classes;
|
|
821
|
+
}}
|
|
822
|
+
onsort={handleSort}
|
|
823
|
+
selectable={true}
|
|
824
|
+
{onselect}
|
|
825
|
+
{selected}
|
|
826
|
+
/>
|
|
827
|
+
{/if}
|
|
828
|
+
{/if}
|
|
829
|
+
</div>
|
|
830
|
+
|
|
831
|
+
{#if infoSection}
|
|
832
|
+
{@render infoSection({
|
|
833
|
+
selectedFiles: allFilesAcquired.map((file) => file.key),
|
|
834
|
+
navToFileFolder
|
|
835
|
+
})}
|
|
836
|
+
{/if}
|
|
837
|
+
</div>
|