@makolabs/ripple 2.4.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/storage/GoogleDriveAdapter.d.ts +42 -0
- package/dist/adapters/storage/GoogleDriveAdapter.js +206 -0
- package/dist/adapters/storage/index.d.ts +1 -0
- package/dist/adapters/storage/index.js +1 -0
- package/dist/ai/ai-types.d.ts +2 -0
- package/dist/file-browser/FileBrowser.svelte +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FileItem, FileListResult } from './types.js';
|
|
2
|
+
import { BaseAdapter } from './BaseAdapter.js';
|
|
3
|
+
/**
|
|
4
|
+
* Google Drive storage adapter for the FileBrowser component.
|
|
5
|
+
*
|
|
6
|
+
* Unlike S3 (which uses path-based navigation), Google Drive uses folder IDs.
|
|
7
|
+
* The adapter translates between the FileBrowser's path-based model and
|
|
8
|
+
* Drive's ID-based hierarchy.
|
|
9
|
+
*
|
|
10
|
+
* Requires backend endpoints:
|
|
11
|
+
* GET /api/drive/auth/status — check OAuth status
|
|
12
|
+
* GET /api/drive/auth — initiate OAuth flow
|
|
13
|
+
* GET /api/drive/list — list folder contents
|
|
14
|
+
* GET /api/drive/file — get file/folder metadata
|
|
15
|
+
* GET /api/drive/download — get download URL
|
|
16
|
+
* POST /api/drive/import — trigger file import
|
|
17
|
+
*/
|
|
18
|
+
export declare class GoogleDriveAdapter extends BaseAdapter {
|
|
19
|
+
private rootFolderId;
|
|
20
|
+
private folderNameCache;
|
|
21
|
+
constructor(rootFolderId?: string);
|
|
22
|
+
getName(): string;
|
|
23
|
+
isConfigured(): Promise<boolean>;
|
|
24
|
+
authenticate(): Promise<boolean>;
|
|
25
|
+
setReopenFlag(): void;
|
|
26
|
+
shouldReopenBrowser(): boolean;
|
|
27
|
+
clearReopenFlag(): void;
|
|
28
|
+
list(folderId: string, searchQuery?: string): Promise<FileListResult>;
|
|
29
|
+
download(file: FileItem): Promise<string>;
|
|
30
|
+
/** Cache a folder name for breadcrumb display. */
|
|
31
|
+
setFolderName(folderId: string, name: string): void;
|
|
32
|
+
/** Retrieve a cached folder name. */
|
|
33
|
+
getFolderNameFromCache(folderId: string): string | undefined;
|
|
34
|
+
protected getApiPath(): string;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve folder name and parent ID, using cache first, then the file list,
|
|
37
|
+
* then falling back to an API call.
|
|
38
|
+
*/
|
|
39
|
+
private resolveFolderInfo;
|
|
40
|
+
private getParentFolderId;
|
|
41
|
+
private getFolderDetails;
|
|
42
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { BaseAdapter } from './BaseAdapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* Google Drive storage adapter for the FileBrowser component.
|
|
4
|
+
*
|
|
5
|
+
* Unlike S3 (which uses path-based navigation), Google Drive uses folder IDs.
|
|
6
|
+
* The adapter translates between the FileBrowser's path-based model and
|
|
7
|
+
* Drive's ID-based hierarchy.
|
|
8
|
+
*
|
|
9
|
+
* Requires backend endpoints:
|
|
10
|
+
* GET /api/drive/auth/status — check OAuth status
|
|
11
|
+
* GET /api/drive/auth — initiate OAuth flow
|
|
12
|
+
* GET /api/drive/list — list folder contents
|
|
13
|
+
* GET /api/drive/file — get file/folder metadata
|
|
14
|
+
* GET /api/drive/download — get download URL
|
|
15
|
+
* POST /api/drive/import — trigger file import
|
|
16
|
+
*/
|
|
17
|
+
export class GoogleDriveAdapter extends BaseAdapter {
|
|
18
|
+
rootFolderId;
|
|
19
|
+
folderNameCache = new Map();
|
|
20
|
+
constructor(rootFolderId) {
|
|
21
|
+
super();
|
|
22
|
+
this.rootFolderId = rootFolderId || 'root';
|
|
23
|
+
}
|
|
24
|
+
getName() {
|
|
25
|
+
return 'Google Drive';
|
|
26
|
+
}
|
|
27
|
+
async isConfigured() {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch('/api/drive/auth/status');
|
|
30
|
+
const data = await response.json().catch(() => ({ authenticated: false }));
|
|
31
|
+
return data.authenticated === true;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async authenticate() {
|
|
38
|
+
try {
|
|
39
|
+
const currentUrl = window.location.href;
|
|
40
|
+
const customRedirectUri = window.location.origin + '/api/drive/auth';
|
|
41
|
+
this.setReopenFlag();
|
|
42
|
+
window.location.href = `/api/drive/auth?redirect_uri=${encodeURIComponent(customRedirectUri)}&redirect_back=${encodeURIComponent(currentUrl)}`;
|
|
43
|
+
// Redirect happens — this won't resolve
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
setReopenFlag() {
|
|
51
|
+
if (typeof localStorage !== 'undefined') {
|
|
52
|
+
localStorage.setItem('reopen_drive_browser', 'true');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
shouldReopenBrowser() {
|
|
56
|
+
if (typeof localStorage !== 'undefined') {
|
|
57
|
+
return localStorage.getItem('reopen_drive_browser') === 'true';
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
clearReopenFlag() {
|
|
62
|
+
if (typeof localStorage !== 'undefined') {
|
|
63
|
+
localStorage.removeItem('reopen_drive_browser');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async list(folderId, searchQuery) {
|
|
67
|
+
const actualFolderId = folderId || this.rootFolderId;
|
|
68
|
+
let url = `/api/drive/list?folderId=${encodeURIComponent(actualFolderId)}`;
|
|
69
|
+
if (searchQuery) {
|
|
70
|
+
url += `&query=${encodeURIComponent(searchQuery)}`;
|
|
71
|
+
}
|
|
72
|
+
const response = await fetch(url);
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const errorData = await response.json();
|
|
75
|
+
throw new Error(errorData.error || 'Failed to fetch files');
|
|
76
|
+
}
|
|
77
|
+
const apiResponse = await response.json();
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
const files = (apiResponse.files || []).map((file) => ({
|
|
80
|
+
id: file.id,
|
|
81
|
+
key: file.id,
|
|
82
|
+
name: file.name,
|
|
83
|
+
lastModified: new Date(file.modifiedTime),
|
|
84
|
+
createdAt: file.createdTime ? new Date(file.createdTime) : undefined,
|
|
85
|
+
size: parseInt(file.size || '0'),
|
|
86
|
+
isFolder: file.mimeType === 'application/vnd.google-apps.folder',
|
|
87
|
+
mimeType: file.mimeType,
|
|
88
|
+
parentId: file.parents?.[0]
|
|
89
|
+
}));
|
|
90
|
+
// Sort: folders first, then by creation/modification time (newest first)
|
|
91
|
+
files.sort((a, b) => {
|
|
92
|
+
if (a.isFolder !== b.isFolder)
|
|
93
|
+
return a.isFolder ? -1 : 1;
|
|
94
|
+
const aTime = (a.createdAt || a.lastModified).getTime();
|
|
95
|
+
const bTime = (b.createdAt || b.lastModified).getTime();
|
|
96
|
+
return bTime - aTime;
|
|
97
|
+
});
|
|
98
|
+
// Resolve current folder name and parent
|
|
99
|
+
let folderName = 'Google Drive';
|
|
100
|
+
let parentFolderId = '';
|
|
101
|
+
if (actualFolderId !== this.rootFolderId) {
|
|
102
|
+
const resolved = await this.resolveFolderInfo(actualFolderId, files);
|
|
103
|
+
folderName = resolved.name;
|
|
104
|
+
parentFolderId = resolved.parentId;
|
|
105
|
+
}
|
|
106
|
+
// Build breadcrumbs
|
|
107
|
+
const breadcrumbs = [
|
|
108
|
+
{
|
|
109
|
+
name: 'Google Drive',
|
|
110
|
+
path: this.rootFolderId,
|
|
111
|
+
id: this.rootFolderId,
|
|
112
|
+
current: actualFolderId === this.rootFolderId,
|
|
113
|
+
clickable: true
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
if (actualFolderId !== this.rootFolderId) {
|
|
117
|
+
breadcrumbs.push({
|
|
118
|
+
name: folderName,
|
|
119
|
+
path: actualFolderId,
|
|
120
|
+
id: actualFolderId,
|
|
121
|
+
current: true,
|
|
122
|
+
clickable: false
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const result = {
|
|
126
|
+
files,
|
|
127
|
+
currentPath: actualFolderId,
|
|
128
|
+
breadcrumbs
|
|
129
|
+
};
|
|
130
|
+
if (actualFolderId !== this.rootFolderId && parentFolderId) {
|
|
131
|
+
result.parentPath = parentFolderId;
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
async download(file) {
|
|
136
|
+
const fileId = file.id || file.key;
|
|
137
|
+
const response = await fetch(`/api/drive/download?fileId=${encodeURIComponent(fileId)}`);
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const errorData = await response.json();
|
|
140
|
+
throw new Error(errorData.error || 'Failed to download file');
|
|
141
|
+
}
|
|
142
|
+
return response.text();
|
|
143
|
+
}
|
|
144
|
+
/** Cache a folder name for breadcrumb display. */
|
|
145
|
+
setFolderName(folderId, name) {
|
|
146
|
+
this.folderNameCache.set(folderId, name);
|
|
147
|
+
}
|
|
148
|
+
/** Retrieve a cached folder name. */
|
|
149
|
+
getFolderNameFromCache(folderId) {
|
|
150
|
+
return this.folderNameCache.get(folderId);
|
|
151
|
+
}
|
|
152
|
+
getApiPath() {
|
|
153
|
+
return 'drive';
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve folder name and parent ID, using cache first, then the file list,
|
|
157
|
+
* then falling back to an API call.
|
|
158
|
+
*/
|
|
159
|
+
async resolveFolderInfo(folderId, currentFiles) {
|
|
160
|
+
// Try cache first
|
|
161
|
+
const cached = this.getFolderNameFromCache(folderId);
|
|
162
|
+
if (cached) {
|
|
163
|
+
const parentId = await this.getParentFolderId(folderId);
|
|
164
|
+
return { name: cached, parentId };
|
|
165
|
+
}
|
|
166
|
+
// Try the current file list
|
|
167
|
+
const folderInList = currentFiles.find((f) => f.id === folderId);
|
|
168
|
+
if (folderInList?.name) {
|
|
169
|
+
this.setFolderName(folderId, folderInList.name);
|
|
170
|
+
return {
|
|
171
|
+
name: folderInList.name,
|
|
172
|
+
parentId: folderInList.parentId || this.rootFolderId
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// Fall back to API
|
|
176
|
+
try {
|
|
177
|
+
const details = await this.getFolderDetails(folderId);
|
|
178
|
+
const name = details.name || `Folder ${folderId.substring(0, 5)}...`;
|
|
179
|
+
if (details.name)
|
|
180
|
+
this.setFolderName(folderId, name);
|
|
181
|
+
const parentId = details.parents && details.parents.length > 0 ? details.parents[0] : this.rootFolderId;
|
|
182
|
+
return { name, parentId };
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return { name: `Folder ${folderId.substring(0, 5)}...`, parentId: this.rootFolderId };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async getParentFolderId(folderId) {
|
|
189
|
+
try {
|
|
190
|
+
const details = await this.getFolderDetails(folderId);
|
|
191
|
+
return details.parents?.[0] || this.rootFolderId;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return this.rootFolderId;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
198
|
+
async getFolderDetails(folderId) {
|
|
199
|
+
const response = await fetch(`/api/drive/file?fileId=${encodeURIComponent(folderId)}`);
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
throw new Error('Failed to fetch folder details');
|
|
202
|
+
}
|
|
203
|
+
const result = await response.json();
|
|
204
|
+
return result.name ? result : { name: `Folder ${folderId.substring(0, 5)}...` };
|
|
205
|
+
}
|
|
206
|
+
}
|
package/dist/ai/ai-types.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface FileBrowserProps {
|
|
|
36
36
|
startPath?: string;
|
|
37
37
|
actions?: FileAction[];
|
|
38
38
|
selectedFiles?: string[];
|
|
39
|
+
/** Controls whether the select-all checkbox selects the current page or all data. @default 'page' */
|
|
40
|
+
selectAllScope?: 'page' | 'all';
|
|
39
41
|
infoSection?: (props: {
|
|
40
42
|
selectedFiles: string[];
|
|
41
43
|
navToFileFolder: (fileKey: string) => void;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
startPath = '',
|
|
17
17
|
actions = [],
|
|
18
18
|
infoSection,
|
|
19
|
+
selectAllScope = 'page',
|
|
19
20
|
selectedFiles = $bindable([])
|
|
20
21
|
}: FileBrowserProps = $props();
|
|
21
22
|
|
|
@@ -843,6 +844,7 @@
|
|
|
843
844
|
}}
|
|
844
845
|
onsort={handleSort}
|
|
845
846
|
selectable={true}
|
|
847
|
+
{selectAllScope}
|
|
846
848
|
{onselect}
|
|
847
849
|
{selected}
|
|
848
850
|
/>
|