@makolabs/ripple 0.0.1-dev.8 → 0.0.1-dev.80

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.
Files changed (77) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/storage/BaseAdapter.d.ts +20 -0
  3. package/dist/adapters/storage/BaseAdapter.js +171 -0
  4. package/dist/adapters/storage/S3Adapter.d.ts +21 -0
  5. package/dist/adapters/storage/S3Adapter.js +194 -0
  6. package/dist/adapters/storage/index.d.ts +3 -0
  7. package/dist/adapters/storage/index.js +3 -0
  8. package/dist/adapters/storage/types.d.ts +102 -0
  9. package/dist/adapters/storage/types.js +4 -0
  10. package/dist/charts/Chart.svelte +59 -47
  11. package/dist/charts/Chart.svelte.d.ts +1 -1
  12. package/dist/drawer/drawer.js +3 -3
  13. package/dist/elements/accordion/Accordion.svelte +98 -0
  14. package/dist/elements/accordion/Accordion.svelte.d.ts +4 -0
  15. package/dist/elements/accordion/accordion.d.ts +227 -0
  16. package/dist/elements/accordion/accordion.js +138 -0
  17. package/dist/elements/alert/Alert.svelte +7 -3
  18. package/dist/elements/dropdown/Dropdown.svelte +74 -107
  19. package/dist/elements/dropdown/Select.svelte +81 -62
  20. package/dist/elements/dropdown/dropdown.js +1 -1
  21. package/dist/elements/dropdown/select.js +8 -8
  22. package/dist/elements/file-upload/FileUpload.svelte +17 -95
  23. package/dist/elements/file-upload/FilesPreview.svelte +93 -0
  24. package/dist/elements/file-upload/FilesPreview.svelte.d.ts +4 -0
  25. package/dist/elements/progress/Progress.svelte +83 -25
  26. package/dist/file-browser/FileBrowser.svelte +837 -0
  27. package/dist/file-browser/FileBrowser.svelte.d.ts +14 -0
  28. package/dist/file-browser/index.d.ts +1 -0
  29. package/dist/file-browser/index.js +1 -0
  30. package/dist/filters/CompactFilters.svelte +147 -0
  31. package/dist/filters/CompactFilters.svelte.d.ts +4 -0
  32. package/dist/filters/index.d.ts +1 -0
  33. package/dist/filters/index.js +1 -0
  34. package/dist/forms/Checkbox.svelte +2 -2
  35. package/dist/forms/DateRange.svelte +21 -21
  36. package/dist/forms/Input.svelte +3 -3
  37. package/dist/forms/NumberInput.svelte +1 -1
  38. package/dist/forms/RadioInputs.svelte +3 -3
  39. package/dist/forms/Tags.svelte +5 -5
  40. package/dist/forms/Toggle.svelte +3 -3
  41. package/dist/forms/slider.js +4 -4
  42. package/dist/index.d.ts +254 -143
  43. package/dist/index.js +19 -2
  44. package/dist/layout/card/MetricCard.svelte +64 -0
  45. package/dist/layout/card/MetricCard.svelte.d.ts +4 -0
  46. package/dist/layout/card/StatsCard.svelte +4 -3
  47. package/dist/layout/card/StatsCard.svelte.d.ts +1 -1
  48. package/dist/layout/card/metric-card.d.ts +49 -0
  49. package/dist/layout/card/metric-card.js +10 -0
  50. package/dist/layout/card/stats-card.d.ts +0 -15
  51. package/dist/layout/card/stats-card.js +1 -1
  52. package/dist/layout/sidebar/NavGroup.svelte +8 -9
  53. package/dist/layout/sidebar/NavItem.svelte +2 -2
  54. package/dist/layout/sidebar/Sidebar.svelte +102 -49
  55. package/dist/layout/table/Table.svelte +464 -87
  56. package/dist/layout/table/Table.svelte.d.ts +1 -1
  57. package/dist/layout/table/table.d.ts +0 -47
  58. package/dist/layout/table/table.js +0 -8
  59. package/dist/layout/tabs/Tab.svelte +9 -6
  60. package/dist/layout/tabs/Tab.svelte.d.ts +1 -1
  61. package/dist/layout/tabs/TabContent.svelte +1 -2
  62. package/dist/layout/tabs/TabContent.svelte.d.ts +1 -1
  63. package/dist/layout/tabs/TabGroup.svelte +10 -5
  64. package/dist/layout/tabs/TabGroup.svelte.d.ts +2 -2
  65. package/dist/layout/tabs/tabs.d.ts +61 -76
  66. package/dist/layout/tabs/tabs.js +170 -28
  67. package/dist/modal/Modal.svelte +3 -3
  68. package/dist/modal/modal.js +3 -3
  69. package/dist/utils/Portal.svelte +108 -0
  70. package/dist/utils/Portal.svelte.d.ts +8 -0
  71. package/dist/utils/dateUtils.d.ts +7 -0
  72. package/dist/utils/dateUtils.js +26 -0
  73. package/dist/variants.d.ts +11 -1
  74. package/dist/variants.js +17 -0
  75. package/package.json +2 -2
  76. package/dist/header/pageheaders.d.ts +0 -10
  77. 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>