@ai-matrx/media 0.8.0 → 0.9.1

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 (185) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/files/engine/api/assets.d.ts +223 -0
  3. package/dist/files/engine/api/assets.js +124 -0
  4. package/dist/files/engine/api/assets.js.map +1 -0
  5. package/dist/files/engine/api/direct.d.ts +43 -0
  6. package/dist/files/engine/api/direct.js +73 -0
  7. package/dist/files/engine/api/direct.js.map +1 -0
  8. package/dist/files/engine/api/fileOrganization.d.ts +34 -0
  9. package/dist/files/engine/api/fileOrganization.js +38 -0
  10. package/dist/files/engine/api/fileOrganization.js.map +1 -0
  11. package/dist/files/engine/api/files.d.ts +249 -0
  12. package/dist/files/engine/api/files.js +231 -0
  13. package/dist/files/engine/api/files.js.map +1 -0
  14. package/dist/files/engine/api/folders.d.ts +63 -0
  15. package/dist/files/engine/api/folders.js +56 -0
  16. package/dist/files/engine/api/folders.js.map +1 -0
  17. package/dist/files/engine/api/office.d.ts +27 -0
  18. package/dist/files/engine/api/office.js +19 -0
  19. package/dist/files/engine/api/office.js.map +1 -0
  20. package/dist/files/engine/api/permissions.d.ts +37 -0
  21. package/dist/files/engine/api/permissions.js +109 -0
  22. package/dist/files/engine/api/permissions.js.map +1 -0
  23. package/dist/files/engine/api/versions.d.ts +26 -0
  24. package/dist/files/engine/api/versions.js +37 -0
  25. package/dist/files/engine/api/versions.js.map +1 -0
  26. package/dist/files/engine/cache/idb-store.d.ts +95 -0
  27. package/dist/files/engine/cache/idb-store.js +172 -0
  28. package/dist/files/engine/cache/idb-store.js.map +1 -0
  29. package/dist/files/engine/cache/policy.d.ts +14 -0
  30. package/dist/files/engine/cache/policy.js +40 -0
  31. package/dist/files/engine/cache/policy.js.map +1 -0
  32. package/dist/files/engine/cache/register-service-worker.d.ts +57 -0
  33. package/dist/files/engine/cache/register-service-worker.js +80 -0
  34. package/dist/files/engine/cache/register-service-worker.js.map +1 -0
  35. package/dist/files/engine/db-types.d.ts +48865 -0
  36. package/dist/files/engine/db-types.js +1 -0
  37. package/dist/files/engine/db-types.js.map +1 -0
  38. package/dist/files/engine/filesDb.d.ts +1913 -0
  39. package/dist/files/engine/filesDb.js +28 -0
  40. package/dist/files/engine/filesDb.js.map +1 -0
  41. package/dist/files/engine/handler/handler.d.ts +82 -0
  42. package/dist/files/engine/handler/handler.js +124 -0
  43. package/dist/files/engine/handler/handler.js.map +1 -0
  44. package/dist/files/engine/handler/hooks/useFile.d.ts +17 -0
  45. package/dist/files/engine/handler/hooks/useFile.js +75 -0
  46. package/dist/files/engine/handler/hooks/useFile.js.map +1 -0
  47. package/dist/files/engine/handler/hooks/useFileUpload.d.ts +76 -0
  48. package/dist/files/engine/handler/hooks/useFileUpload.js +67 -0
  49. package/dist/files/engine/handler/hooks/useFileUpload.js.map +1 -0
  50. package/dist/files/engine/handler/input/normalize.d.ts +14 -0
  51. package/dist/files/engine/handler/input/normalize.js +365 -0
  52. package/dist/files/engine/handler/input/normalize.js.map +1 -0
  53. package/dist/files/engine/handler/intelligence/access.d.ts +35 -0
  54. package/dist/files/engine/handler/intelligence/access.js +85 -0
  55. package/dist/files/engine/handler/intelligence/access.js.map +1 -0
  56. package/dist/files/engine/handler/intelligence/magic-bytes.d.ts +21 -0
  57. package/dist/files/engine/handler/intelligence/magic-bytes.js +67 -0
  58. package/dist/files/engine/handler/intelligence/magic-bytes.js.map +1 -0
  59. package/dist/files/engine/handler/output/target.d.ts +19 -0
  60. package/dist/files/engine/handler/output/target.js +222 -0
  61. package/dist/files/engine/handler/output/target.js.map +1 -0
  62. package/dist/files/engine/handler/resolver.d.ts +34 -0
  63. package/dist/files/engine/handler/resolver.js +133 -0
  64. package/dist/files/engine/handler/resolver.js.map +1 -0
  65. package/dist/files/engine/handler/types.d.ts +367 -0
  66. package/dist/files/engine/handler/types.js +1 -0
  67. package/dist/files/engine/handler/types.js.map +1 -0
  68. package/dist/files/engine/handler/upload.d.ts +32 -0
  69. package/dist/files/engine/handler/upload.js +295 -0
  70. package/dist/files/engine/handler/upload.js.map +1 -0
  71. package/dist/files/engine/handler/utils/classify.d.ts +14 -0
  72. package/dist/files/engine/handler/utils/classify.js +27 -0
  73. package/dist/files/engine/handler/utils/classify.js.map +1 -0
  74. package/dist/files/engine/handler/utils/prefer-locator.d.ts +32 -0
  75. package/dist/files/engine/handler/utils/prefer-locator.js +34 -0
  76. package/dist/files/engine/handler/utils/prefer-locator.js.map +1 -0
  77. package/dist/files/engine/handler/utils/python-base.d.ts +129 -0
  78. package/dist/files/engine/handler/utils/python-base.js +60 -0
  79. package/dist/files/engine/handler/utils/python-base.js.map +1 -0
  80. package/dist/files/engine/hooks/blob-cache.d.ts +130 -0
  81. package/dist/files/engine/hooks/blob-cache.js +162 -0
  82. package/dist/files/engine/hooks/blob-cache.js.map +1 -0
  83. package/dist/files/engine/hooks/office-extraction-cache.d.ts +28 -0
  84. package/dist/files/engine/hooks/office-extraction-cache.js +67 -0
  85. package/dist/files/engine/hooks/office-extraction-cache.js.map +1 -0
  86. package/dist/files/engine/host/configure.d.ts +166 -0
  87. package/dist/files/engine/host/configure.js +31 -0
  88. package/dist/files/engine/host/configure.js.map +1 -0
  89. package/dist/files/engine/host/org.d.ts +8 -0
  90. package/dist/files/engine/host/org.js +16 -0
  91. package/dist/files/engine/host/org.js.map +1 -0
  92. package/dist/files/engine/host/python-client.d.ts +60 -0
  93. package/dist/files/engine/host/python-client.js +46 -0
  94. package/dist/files/engine/host/python-client.js.map +1 -0
  95. package/dist/files/engine/host/share-links.d.ts +20 -0
  96. package/dist/files/engine/host/share-links.js +32 -0
  97. package/dist/files/engine/host/share-links.js.map +1 -0
  98. package/dist/files/engine/host/store.d.ts +24 -0
  99. package/dist/files/engine/host/store.js +23 -0
  100. package/dist/files/engine/host/store.js.map +1 -0
  101. package/dist/files/engine/host/supabase.d.ts +3808 -0
  102. package/dist/files/engine/host/supabase.js +33 -0
  103. package/dist/files/engine/host/supabase.js.map +1 -0
  104. package/dist/files/engine/host/toast.d.ts +14 -0
  105. package/dist/files/engine/host/toast.js +20 -0
  106. package/dist/files/engine/host/toast.js.map +1 -0
  107. package/dist/files/engine/host/typed-client.d.ts +136 -0
  108. package/dist/files/engine/host/typed-client.js +66 -0
  109. package/dist/files/engine/host/typed-client.js.map +1 -0
  110. package/dist/files/engine/index.d.ts +22 -0
  111. package/dist/files/engine/index.js +49 -0
  112. package/dist/files/engine/index.js.map +1 -0
  113. package/dist/files/engine/media/our-file-sources.d.ts +39 -0
  114. package/dist/files/engine/media/our-file-sources.js +83 -0
  115. package/dist/files/engine/media/our-file-sources.js.map +1 -0
  116. package/dist/files/engine/media/signed-url.d.ts +1 -0
  117. package/dist/files/engine/media/signed-url.js +6 -0
  118. package/dist/files/engine/media/signed-url.js.map +1 -0
  119. package/dist/files/engine/redux/converters.d.ts +73 -0
  120. package/dist/files/engine/redux/converters.js +286 -0
  121. package/dist/files/engine/redux/converters.js.map +1 -0
  122. package/dist/files/engine/redux/file-hydration.d.ts +15 -0
  123. package/dist/files/engine/redux/file-hydration.js +46 -0
  124. package/dist/files/engine/redux/file-hydration.js.map +1 -0
  125. package/dist/files/engine/redux/file-tree-auth-boundary.d.ts +5 -0
  126. package/dist/files/engine/redux/file-tree-auth-boundary.js +28 -0
  127. package/dist/files/engine/redux/file-tree-auth-boundary.js.map +1 -0
  128. package/dist/files/engine/redux/file-tree-timeout.d.ts +32 -0
  129. package/dist/files/engine/redux/file-tree-timeout.js +73 -0
  130. package/dist/files/engine/redux/file-tree-timeout.js.map +1 -0
  131. package/dist/files/engine/redux/mutation-toast-middleware.d.ts +27 -0
  132. package/dist/files/engine/redux/mutation-toast-middleware.js +103 -0
  133. package/dist/files/engine/redux/mutation-toast-middleware.js.map +1 -0
  134. package/dist/files/engine/redux/realtime-middleware.d.ts +61 -0
  135. package/dist/files/engine/redux/realtime-middleware.js +431 -0
  136. package/dist/files/engine/redux/realtime-middleware.js.map +1 -0
  137. package/dist/files/engine/redux/request-ledger.d.ts +68 -0
  138. package/dist/files/engine/redux/request-ledger.js +66 -0
  139. package/dist/files/engine/redux/request-ledger.js.map +1 -0
  140. package/dist/files/engine/redux/selectors.d.ts +3254 -0
  141. package/dist/files/engine/redux/selectors.js +428 -0
  142. package/dist/files/engine/redux/selectors.js.map +1 -0
  143. package/dist/files/engine/redux/slice.d.ts +157 -0
  144. package/dist/files/engine/redux/slice.js +836 -0
  145. package/dist/files/engine/redux/slice.js.map +1 -0
  146. package/dist/files/engine/redux/thunks.d.ts +449 -0
  147. package/dist/files/engine/redux/thunks.js +1684 -0
  148. package/dist/files/engine/redux/thunks.js.map +1 -0
  149. package/dist/files/engine/redux/tree-utils.d.ts +90 -0
  150. package/dist/files/engine/redux/tree-utils.js +200 -0
  151. package/dist/files/engine/redux/tree-utils.js.map +1 -0
  152. package/dist/files/engine/support/claimsUser.d.ts +77 -0
  153. package/dist/files/engine/support/claimsUser.js +48 -0
  154. package/dist/files/engine/support/claimsUser.js.map +1 -0
  155. package/dist/files/engine/support/datetime.d.ts +6 -0
  156. package/dist/files/engine/support/datetime.js +15 -0
  157. package/dist/files/engine/support/datetime.js.map +1 -0
  158. package/dist/files/engine/support/document-visibility.d.ts +14 -0
  159. package/dist/files/engine/support/document-visibility.js +28 -0
  160. package/dist/files/engine/support/document-visibility.js.map +1 -0
  161. package/dist/files/engine/support/logger.d.ts +28 -0
  162. package/dist/files/engine/support/logger.js +36 -0
  163. package/dist/files/engine/support/logger.js.map +1 -0
  164. package/dist/files/engine/types.d.ts +1081 -0
  165. package/dist/files/engine/types.js +30 -0
  166. package/dist/files/engine/types.js.map +1 -0
  167. package/dist/files/engine/upload/cloudUpload.d.ts +182 -0
  168. package/dist/files/engine/upload/cloudUpload.js +418 -0
  169. package/dist/files/engine/upload/cloudUpload.js.map +1 -0
  170. package/dist/files/engine/upload/tusUpload.d.ts +100 -0
  171. package/dist/files/engine/upload/tusUpload.js +258 -0
  172. package/dist/files/engine/upload/tusUpload.js.map +1 -0
  173. package/dist/files/engine/upload/uploadDedupGuard.d.ts +58 -0
  174. package/dist/files/engine/upload/uploadDedupGuard.js +41 -0
  175. package/dist/files/engine/upload/uploadDedupGuard.js.map +1 -0
  176. package/dist/files/engine/upload/uploadGuardOpeners.d.ts +70 -0
  177. package/dist/files/engine/upload/uploadGuardOpeners.js +54 -0
  178. package/dist/files/engine/upload/uploadGuardOpeners.js.map +1 -0
  179. package/dist/files/engine/utils/file-types.d.ts +243 -0
  180. package/dist/files/engine/utils/file-types.js +1722 -0
  181. package/dist/files/engine/utils/file-types.js.map +1 -0
  182. package/dist/files/engine/utils/user-visible.d.ts +133 -0
  183. package/dist/files/engine/utils/user-visible.js +115 -0
  184. package/dist/files/engine/utils/user-visible.js.map +1 -0
  185. package/package.json +392 -8
@@ -0,0 +1,1684 @@
1
+ "use client";
2
+ import { createAsyncThunk } from "@reduxjs/toolkit";
3
+ import { supabase } from "../host/supabase.js";
4
+ import { filesDb, FILE_VERSIONS_TABLE_COLUMNS } from "../filesDb.js";
5
+ import { pgErrorToError } from "@ai-matrx/data";
6
+ import * as Files from "../api/files.js";
7
+ import {
8
+ claimUpload,
9
+ uploadDedupKey
10
+ } from "../upload/uploadDedupGuard.js";
11
+ import * as Folders from "../api/folders.js";
12
+ import * as Permissions from "../api/permissions.js";
13
+ import * as Versions from "../api/versions.js";
14
+ import {
15
+ createShareLink as createCanonicalShareLink,
16
+ listShareLinks as listCanonicalShareLinks,
17
+ revokeShareLink as revokeCanonicalShareLink
18
+ } from "../host/share-links.js";
19
+ import {
20
+ restoreFileDirect,
21
+ restoreFolderDirect,
22
+ softDeleteFileDirect,
23
+ softDeleteFolderDirect
24
+ } from "../api/direct.js";
25
+ import { fileHandler } from "../handler/handler.js";
26
+ import { newRequestId } from "../host/python-client.js";
27
+ import { extractErrorMessage } from "@ai-matrx/data/net";
28
+ import {
29
+ apiFileRecordToCloudFile,
30
+ dbRowToCloudFilePermission,
31
+ dbRowToCloudFileVersion,
32
+ dbRowToCloudFolder,
33
+ canonicalShareLinkToCloudShareLink,
34
+ parseCloudTreeRows,
35
+ rpcPermissionRowToCloudFilePermission
36
+ } from "./converters.js";
37
+ import {
38
+ areCloudFileFieldsLoaded,
39
+ FILE_DB_RECORD_FIELDS,
40
+ fileHintToCloudFilePartial,
41
+ needsOnlyRenderFields
42
+ } from "./file-hydration.js";
43
+ import {
44
+ registerRequest,
45
+ releaseRequest,
46
+ beginResourceOp,
47
+ isLatestResourceOp,
48
+ hasInFlight
49
+ } from "./request-ledger.js";
50
+ import { toast } from "../host/toast.js";
51
+ import { buildTreeState } from "./tree-utils.js";
52
+ import {
53
+ isUserVisibleFilePath,
54
+ isUserVisibleFolderPath
55
+ } from "../utils/user-visible.js";
56
+ import {
57
+ hasMatchingFileTreeSession,
58
+ runFileTreeSessionOperation
59
+ } from "./file-tree-auth-boundary.js";
60
+ import {
61
+ isFileTreeLoadDeferred,
62
+ runWithFileTreeLoadTimeout
63
+ } from "./file-tree-timeout.js";
64
+ import {
65
+ invalidate as invalidateBlobCache,
66
+ replaceWithSavedBytes
67
+ } from "../hooks/blob-cache.js";
68
+ import { invalidateOfficeExtraction } from "../hooks/office-extraction-cache.js";
69
+ import {
70
+ addFilePendingRequest,
71
+ attachChildToFolder,
72
+ clearUpload,
73
+ detachChildFromFolder,
74
+ markFileSaved,
75
+ markFolderFullyLoaded,
76
+ removeFile,
77
+ removeFilePendingRequest,
78
+ removeFolder,
79
+ removePermissionForResource,
80
+ removeShareLink,
81
+ replaceTree,
82
+ rollbackFileOptimisticUpdate,
83
+ setFileError,
84
+ setFileField,
85
+ setFileLoading,
86
+ setTreeStatus,
87
+ trackUploadStart,
88
+ updateUploadProgress,
89
+ updateUploadStatus,
90
+ upsertFile,
91
+ upsertFiles,
92
+ upsertFolder,
93
+ upsertFolders,
94
+ upsertPermissionsForResource,
95
+ upsertShareLinksForResource,
96
+ upsertVersionsForFile
97
+ } from "./slice.js";
98
+ import { getFileFromState } from "./selectors.js";
99
+ import { getClaimsUser } from "../host/supabase.js";
100
+ const fileFieldHydrationInFlight = /* @__PURE__ */ new Map();
101
+ async function fetchCloudFileFields(fileId, fields) {
102
+ const key = `${fileId}:${[...fields].sort().join(",")}`;
103
+ const existing = fileFieldHydrationInFlight.get(key);
104
+ if (existing) return existing;
105
+ const request = (async () => {
106
+ const { data } = await Files.getFileMetadata(fileId);
107
+ const record = apiFileRecordToCloudFile(data);
108
+ if (record.deletedAt) return null;
109
+ if (needsOnlyRenderFields(fields)) {
110
+ return {
111
+ id: record.id,
112
+ fileName: record.fileName,
113
+ mimeType: record.mimeType,
114
+ fileSize: record.fileSize,
115
+ visibility: record.visibility
116
+ };
117
+ }
118
+ return record;
119
+ })().finally(() => fileFieldHydrationInFlight.delete(key));
120
+ fileFieldHydrationInFlight.set(key, request);
121
+ return request;
122
+ }
123
+ const ensureCloudFileFields = createAsyncThunk(
124
+ "cloudFiles/ensureCloudFileFields",
125
+ async ({ fileId, fields = FILE_DB_RECORD_FIELDS, hint }, { dispatch, getState }) => {
126
+ dispatch(upsertFile(fileHintToCloudFilePartial(fileId, hint)));
127
+ if (areCloudFileFieldsLoaded(getFileFromState(getState(), fileId), fields)) {
128
+ return "ready";
129
+ }
130
+ dispatch(setFileLoading({ id: fileId, loading: true }));
131
+ dispatch(setFileError({ id: fileId, error: null }));
132
+ try {
133
+ const partial = await fetchCloudFileFields(fileId, fields);
134
+ if (!partial) return "missing";
135
+ dispatch(upsertFile(partial));
136
+ return "ready";
137
+ } catch (error) {
138
+ const normalized = pgErrorToError(error);
139
+ dispatch(setFileError({ id: fileId, error: normalized.message }));
140
+ throw normalized;
141
+ } finally {
142
+ dispatch(setFileLoading({ id: fileId, loading: false }));
143
+ }
144
+ }
145
+ );
146
+ const TREE_PAGE_SIZE = 5e3;
147
+ const TREE_MAX_PAGES = 20;
148
+ let _treeInFlight = null;
149
+ let _treeInFlightUserId = null;
150
+ const loadUserFileTree = createAsyncThunk("cloudFiles/loadUserFileTree", async ({ userId }, { dispatch, getState }) => {
151
+ if (!await hasMatchingFileTreeSession(userId)) return;
152
+ if (_treeInFlight && _treeInFlightUserId === userId) return _treeInFlight;
153
+ const run = (async () => {
154
+ dispatch(setTreeStatus({ status: "loading" }));
155
+ try {
156
+ const rows = await runWithFileTreeLoadTimeout(async (signal) => {
157
+ const rows2 = [];
158
+ for (let page = 0; page < TREE_MAX_PAGES; page += 1) {
159
+ const { data, error } = await runFileTreeSessionOperation(
160
+ () => supabase.rpc("get_user_file_tree", {
161
+ p_user_id: userId,
162
+ p_limit: TREE_PAGE_SIZE,
163
+ p_offset: page * TREE_PAGE_SIZE,
164
+ p_include_folders: true,
165
+ p_include_deleted: false
166
+ }).abortSignal(signal)
167
+ );
168
+ if (error) {
169
+ if (!signal.aborted) {
170
+ dispatch(
171
+ setTreeStatus({ status: "error", error: error.message })
172
+ );
173
+ }
174
+ throw error;
175
+ }
176
+ const pageRows = parseCloudTreeRows(data);
177
+ rows2.push(...pageRows);
178
+ if (pageRows.length < TREE_PAGE_SIZE) break;
179
+ }
180
+ return rows2;
181
+ });
182
+ const files = [];
183
+ const folders = [];
184
+ for (const row of rows) {
185
+ if (row.kind === "file") {
186
+ files.push({
187
+ id: row.id,
188
+ ownerId: row.owner_id,
189
+ filePath: row.file_path,
190
+ fileName: row.file_name,
191
+ parentFolderId: row.parent_folder_id,
192
+ mimeType: row.mime_type,
193
+ // Phase 0 rename: `file_size` → `size_bytes`. The
194
+ // `CloudTreeFileRow` shape and Supabase row both carry the new
195
+ // name now. See docs/PYTHON_UPDATES.md §3.
196
+ fileSize: row.size_bytes,
197
+ visibility: row.visibility,
198
+ currentVersion: row.current_version,
199
+ createdAt: row.created_at,
200
+ updatedAt: row.updated_at,
201
+ deletedAt: row.deleted_at,
202
+ originDeviceId: row.origin_device_id,
203
+ metadata: row.metadata
204
+ });
205
+ } else {
206
+ folders.push({
207
+ id: row.id,
208
+ ownerId: row.owner_id,
209
+ folderPath: row.folder_path,
210
+ folderName: row.folder_name,
211
+ parentId: row.parent_id,
212
+ visibility: row.visibility,
213
+ createdAt: row.created_at,
214
+ updatedAt: row.updated_at,
215
+ deletedAt: row.deleted_at,
216
+ metadata: row.metadata
217
+ });
218
+ }
219
+ }
220
+ dispatch(upsertFiles(files));
221
+ dispatch(upsertFolders(folders));
222
+ const prior = getState().cloudFiles;
223
+ for (const [id, rec] of Object.entries(prior.filesById)) {
224
+ if (rec && !isUserVisibleFilePath(rec.filePath)) {
225
+ dispatch(removeFile({ id }));
226
+ }
227
+ }
228
+ for (const [id, rec] of Object.entries(prior.foldersById)) {
229
+ if (rec && rec.source.kind === "real" && !isUserVisibleFolderPath(rec.folderPath)) {
230
+ dispatch(removeFolder({ id }));
231
+ }
232
+ }
233
+ const fileIds = files.map((f) => f.id).filter(Boolean);
234
+ const folderIds = folders.map((f) => f.id).filter(Boolean);
235
+ const tree = buildTreeState({
236
+ fileIds,
237
+ folderIds,
238
+ filesById: Object.fromEntries(
239
+ files.map((f) => [
240
+ f.id,
241
+ {
242
+ id: f.id,
243
+ ownerId: f.ownerId,
244
+ filePath: f.filePath,
245
+ fileName: f.fileName,
246
+ mimeType: f.mimeType ?? null,
247
+ fileSize: f.fileSize ?? null,
248
+ checksum: null,
249
+ visibility: f.visibility,
250
+ currentVersion: f.currentVersion,
251
+ parentFolderId: f.parentFolderId ?? null,
252
+ metadata: f.metadata ?? {},
253
+ createdAt: f.createdAt,
254
+ updatedAt: f.updatedAt,
255
+ deletedAt: f.deletedAt,
256
+ // Tree-spine reconstruction is internal — the computed URL fields
257
+ // and the Phase-1b backend thumbnail_url are only populated when
258
+ // records arrive via the REST API. Default all to null here; the
259
+ // file grid (`MediaThumbnail`) falls through to `useFileAsset` →
260
+ // `Asset.variants["thumbnail_url"]` when these are absent.
261
+ publicUrl: null,
262
+ url: null,
263
+ cdnUrl: null,
264
+ downloadUrl: null,
265
+ thumbnailUrl: null,
266
+ source: { kind: "real" },
267
+ _dirty: false,
268
+ _dirtyFields: {},
269
+ _fieldHistory: {},
270
+ _loadedFields: {},
271
+ _loading: false,
272
+ _error: null,
273
+ _pendingRequestIds: []
274
+ }
275
+ ])
276
+ ),
277
+ foldersById: Object.fromEntries(
278
+ folders.map((f) => [
279
+ f.id,
280
+ {
281
+ id: f.id,
282
+ ownerId: f.ownerId,
283
+ folderPath: f.folderPath,
284
+ folderName: f.folderName,
285
+ parentId: f.parentId ?? null,
286
+ visibility: f.visibility,
287
+ metadata: f.metadata ?? {},
288
+ createdAt: f.createdAt,
289
+ updatedAt: f.updatedAt,
290
+ deletedAt: f.deletedAt,
291
+ // Tree-spine reconstruction is internal — public_url is only
292
+ // populated when records arrive via the API. Default to null
293
+ // here; surfaces that need a CDN URL fetch via useFileSrc.
294
+ publicUrl: null,
295
+ source: { kind: "real" },
296
+ _dirty: false,
297
+ _dirtyFields: {},
298
+ _fieldHistory: {},
299
+ _loadedFields: {},
300
+ _loading: false,
301
+ _error: null,
302
+ _pendingRequestIds: []
303
+ }
304
+ ])
305
+ )
306
+ });
307
+ dispatch(
308
+ replaceTree({
309
+ rootFolderIds: tree.rootFolderIds,
310
+ rootFileIds: tree.rootFileIds,
311
+ childrenByFolderId: tree.childrenByFolderId,
312
+ // This RPC returns the complete discoverable tree, not one lazy level.
313
+ // Mark every real folder so navigation never re-downloads up to 100k
314
+ // rows merely because the user opened a different folder.
315
+ fullyLoadedFolderIds: Object.fromEntries(
316
+ folderIds.map((id) => [id, true])
317
+ )
318
+ })
319
+ );
320
+ } catch (error) {
321
+ if (isFileTreeLoadDeferred(error)) {
322
+ dispatch(setTreeStatus({ status: "idle" }));
323
+ throw error;
324
+ }
325
+ const message = error instanceof Error ? error.message : "Could not load your file library.";
326
+ dispatch(setTreeStatus({ status: "error", error: message }));
327
+ throw error;
328
+ }
329
+ })();
330
+ _treeInFlight = run;
331
+ _treeInFlightUserId = userId;
332
+ try {
333
+ await run;
334
+ } finally {
335
+ if (_treeInFlight === run) {
336
+ _treeInFlight = null;
337
+ _treeInFlightUserId = null;
338
+ }
339
+ }
340
+ });
341
+ const reconcileTree = createAsyncThunk("cloudFiles/reconcileTree", async ({ userId }, { dispatch }) => {
342
+ await dispatch(loadUserFileTree({ userId })).unwrap();
343
+ });
344
+ const loadFolderContents = createAsyncThunk("cloudFiles/loadFolderContents", async ({ folderId }, { dispatch }) => {
345
+ if (folderId.startsWith("vfs:")) return;
346
+ const { data, error } = await getClaimsUser(supabase);
347
+ if (error) throw error;
348
+ if (!data.user?.id)
349
+ throw new Error("Cannot load folder contents without a user");
350
+ await dispatch(loadUserFileTree({ userId: data.user.id })).unwrap();
351
+ dispatch(markFolderFullyLoaded({ folderId }));
352
+ });
353
+ function isVirtualResourceId(id) {
354
+ return id.startsWith("vfs:");
355
+ }
356
+ const loadFileVersions = createAsyncThunk("cloudFiles/loadFileVersions", async ({ fileId }, { dispatch }) => {
357
+ if (isVirtualResourceId(fileId)) return;
358
+ const { data, error } = await filesDb(supabase).from("file_versions").select(FILE_VERSIONS_TABLE_COLUMNS).eq("file_id", fileId).order("version_number", { ascending: false });
359
+ if (error) throw pgErrorToError(error);
360
+ const versions = (data ?? []).map(
361
+ dbRowToCloudFileVersion
362
+ );
363
+ dispatch(upsertVersionsForFile({ fileId, versions }));
364
+ });
365
+ const loadPermissions = createAsyncThunk("cloudFiles/loadPermissions", async ({ resourceId }, { dispatch }) => {
366
+ if (isVirtualResourceId(resourceId)) return;
367
+ const { data, error } = await supabase.schema("iam").from("permissions").select("*").eq("resource_type", "file").eq("resource_id", resourceId);
368
+ if (error) throw pgErrorToError(error);
369
+ const permissions = (data ?? []).map(
370
+ dbRowToCloudFilePermission
371
+ );
372
+ dispatch(upsertPermissionsForResource({ resourceId, permissions }));
373
+ });
374
+ const loadShareLinks = createAsyncThunk(
375
+ "cloudFiles/loadShareLinks",
376
+ async ({ resourceId, resourceType = "file" }, { dispatch }) => {
377
+ if (isVirtualResourceId(resourceId)) return;
378
+ const links = await listCanonicalShareLinks(resourceType, resourceId);
379
+ const shareLinks = links.filter((l) => l.isActive).map(
380
+ (l) => canonicalShareLinkToCloudShareLink(l, resourceType, resourceId)
381
+ );
382
+ dispatch(upsertShareLinksForResource({ resourceId, shareLinks }));
383
+ }
384
+ );
385
+ const createFolder = createAsyncThunk("cloudFiles/createFolder", async (arg, { dispatch, getState }) => {
386
+ const folderName = arg.folderName.trim();
387
+ if (!folderName) {
388
+ throw new Error("Folder name cannot be empty.");
389
+ }
390
+ if (/[/\\]/.test(folderName)) {
391
+ throw new Error("Folder names cannot contain '/' or '\\'.");
392
+ }
393
+ if (/[\x00-\x1f\x7f]/.test(folderName)) {
394
+ throw new Error("Folder names cannot contain control characters.");
395
+ }
396
+ if (folderName.endsWith(".")) {
397
+ throw new Error("Folder names cannot end with '.'.");
398
+ }
399
+ {
400
+ const state = getState().cloudFiles;
401
+ const lowered = folderName.toLowerCase();
402
+ const collision = Object.values(state.foldersById).some(
403
+ (f) => !!f && !f.deletedAt && (f.parentId ?? null) === (arg.parentId ?? null) && f.folderName.toLowerCase() === lowered
404
+ );
405
+ if (collision) {
406
+ throw new Error(
407
+ `A folder named '${folderName}' already exists in this location.`
408
+ );
409
+ }
410
+ }
411
+ const requestId = newRequestId();
412
+ registerRequest({
413
+ requestId,
414
+ kind: "folder-create",
415
+ resourceId: null,
416
+ resourceType: "folder"
417
+ });
418
+ let folderPath = folderName;
419
+ if (arg.parentId !== null) {
420
+ const parent = getState().cloudFiles.foldersById[arg.parentId];
421
+ if (!parent) {
422
+ throw new Error(
423
+ "We couldn't create the folder here \u2014 this view is out of date. Refresh and try again."
424
+ );
425
+ }
426
+ folderPath = `${parent.folderPath}/${folderName}`;
427
+ }
428
+ const body = {
429
+ folder_path: folderPath,
430
+ visibility: arg.visibility ?? "personal",
431
+ metadata: arg.metadata ?? null
432
+ };
433
+ try {
434
+ const { data: row } = await Folders.createFolder(body, { requestId });
435
+ const folder = dbRowToCloudFolder(row);
436
+ dispatch(upsertFolder(folder));
437
+ dispatch(
438
+ attachChildToFolder({
439
+ parentFolderId: folder.parentId,
440
+ kind: "folder",
441
+ id: folder.id
442
+ })
443
+ );
444
+ return folder.id;
445
+ } catch (err) {
446
+ if (true) {
447
+ const e = err;
448
+ console.error("[createFolder] backend error", {
449
+ status: e.status ?? null,
450
+ code: e.code,
451
+ detail: e.detail,
452
+ userMessage: e.userMessage,
453
+ details: e.details,
454
+ sentBody: body
455
+ });
456
+ }
457
+ throw err;
458
+ } finally {
459
+ releaseRequest(requestId);
460
+ }
461
+ });
462
+ const updateFolder = createAsyncThunk("cloudFiles/updateFolder", async (arg, { dispatch, getState }) => {
463
+ const state = getState();
464
+ const folder = state.cloudFiles.foldersById[arg.folderId];
465
+ if (!folder)
466
+ throw new Error(
467
+ "We couldn't update that folder \u2014 this view is out of date. Refresh and try again."
468
+ );
469
+ const requestId = newRequestId();
470
+ registerRequest({
471
+ requestId,
472
+ kind: "folder-update",
473
+ resourceId: arg.folderId,
474
+ resourceType: "folder"
475
+ });
476
+ const patch = arg.patch;
477
+ const optimistic = {
478
+ ...folder,
479
+ ...patch.folderName !== void 0 ? { folderName: patch.folderName } : {},
480
+ ...patch.parentId !== void 0 ? { parentId: patch.parentId } : {},
481
+ ...patch.visibility !== void 0 ? { visibility: patch.visibility } : {},
482
+ ...patch.metadata !== void 0 ? { metadata: patch.metadata } : {}
483
+ };
484
+ dispatch(upsertFolder(optimistic));
485
+ if (patch.parentId !== void 0 && patch.parentId !== folder.parentId) {
486
+ dispatch(
487
+ detachChildFromFolder({
488
+ parentFolderId: folder.parentId,
489
+ kind: "folder",
490
+ id: folder.id
491
+ })
492
+ );
493
+ dispatch(
494
+ attachChildToFolder({
495
+ parentFolderId: patch.parentId,
496
+ kind: "folder",
497
+ id: folder.id
498
+ })
499
+ );
500
+ }
501
+ let targetFolderPath;
502
+ if (patch.folderName !== void 0 || patch.parentId !== void 0) {
503
+ const newName = patch.folderName ?? folder.folderName;
504
+ const newParentId = patch.parentId !== void 0 ? patch.parentId : folder.parentId;
505
+ if (newParentId) {
506
+ const newParent = state.cloudFiles.foldersById[newParentId];
507
+ if (!newParent) {
508
+ throw new Error(
509
+ "We couldn't move the folder there \u2014 this view is out of date. Refresh and try again."
510
+ );
511
+ }
512
+ targetFolderPath = `${newParent.folderPath}/${newName}`;
513
+ } else {
514
+ targetFolderPath = newName;
515
+ }
516
+ }
517
+ try {
518
+ const { data: row } = await Folders.patchFolder(
519
+ arg.folderId,
520
+ {
521
+ folder_path: targetFolderPath,
522
+ visibility: patch.visibility,
523
+ metadata: patch.metadata ?? null
524
+ },
525
+ { requestId }
526
+ );
527
+ dispatch(upsertFolder(dbRowToCloudFolder(row)));
528
+ } catch (err) {
529
+ dispatch(upsertFolder(toCloudFolderPartial(folder)));
530
+ if (patch.parentId !== void 0 && patch.parentId !== folder.parentId) {
531
+ dispatch(
532
+ detachChildFromFolder({
533
+ parentFolderId: patch.parentId,
534
+ kind: "folder",
535
+ id: folder.id
536
+ })
537
+ );
538
+ dispatch(
539
+ attachChildToFolder({
540
+ parentFolderId: folder.parentId,
541
+ kind: "folder",
542
+ id: folder.id
543
+ })
544
+ );
545
+ }
546
+ throw err;
547
+ } finally {
548
+ releaseRequest(requestId);
549
+ }
550
+ });
551
+ const deleteFolder = createAsyncThunk("cloudFiles/deleteFolder", async (arg, { dispatch, getState }) => {
552
+ const state = getState();
553
+ const folder = state.cloudFiles.foldersById[arg.folderId];
554
+ if (!folder) return;
555
+ const requestId = newRequestId();
556
+ registerRequest({
557
+ requestId,
558
+ kind: "folder-delete",
559
+ resourceId: arg.folderId,
560
+ resourceType: "folder"
561
+ });
562
+ try {
563
+ await softDeleteFolderDirect(arg.folderId);
564
+ dispatch(removeFolder({ id: arg.folderId }));
565
+ } finally {
566
+ releaseRequest(requestId);
567
+ }
568
+ });
569
+ const ensureFolderPath = createAsyncThunk("cloudFiles/ensureFolderPath", async (arg, { dispatch, getState }) => {
570
+ const normalizedPath = arg.folderPath.split("/").map((s) => s.trim()).filter(Boolean).join("/");
571
+ if (!normalizedPath) {
572
+ throw new Error("folderPath cannot be empty.");
573
+ }
574
+ const existing = Object.values(getState().cloudFiles.foldersById).find(
575
+ (folder) => folder.folderPath === normalizedPath && !folder.deletedAt
576
+ );
577
+ if (existing) {
578
+ return existing.id;
579
+ }
580
+ const requestId = newRequestId();
581
+ registerRequest({
582
+ requestId,
583
+ kind: "folder-create",
584
+ resourceId: null,
585
+ resourceType: "folder"
586
+ });
587
+ try {
588
+ const { data: row } = await Folders.ensureFolderPath(
589
+ normalizedPath,
590
+ arg.visibility ?? "personal",
591
+ { requestId }
592
+ );
593
+ const folder = dbRowToCloudFolder(row);
594
+ dispatch(upsertFolder(folder));
595
+ dispatch(
596
+ attachChildToFolder({
597
+ parentFolderId: folder.parentId,
598
+ kind: "folder",
599
+ id: folder.id
600
+ })
601
+ );
602
+ return folder.id;
603
+ } finally {
604
+ releaseRequest(requestId);
605
+ }
606
+ });
607
+ function announceUploadFailures(failed) {
608
+ if (failed.length === 0) return;
609
+ const reasons = Array.from(
610
+ new Set(failed.map((f) => f.error).filter((e) => e && e.trim().length > 0))
611
+ );
612
+ const title = failed.length === 1 ? `Couldn't upload ${failed[0].name}` : `Couldn't upload ${failed.length} files`;
613
+ toast.error(title, {
614
+ // The server's own sentence, never a sentence of ours. When several
615
+ // distinct things went wrong, each one gets said.
616
+ description: reasons.length > 0 ? reasons.join(" \xB7 ") : "The server refused the upload and gave no reason.",
617
+ id: `upload-failed:${failed.map((f) => f.name).join("|")}`,
618
+ duration: 12e3
619
+ });
620
+ }
621
+ const uploadFiles = createAsyncThunk("cloudFiles/uploadFiles", async (arg, { dispatch, getState }) => {
622
+ const concurrency = Math.max(1, arg.concurrency ?? 3);
623
+ const uploaded = [];
624
+ const failed = [];
625
+ const state = getState();
626
+ let prefix = "";
627
+ if (arg.folderPath) {
628
+ prefix = `${arg.folderPath.replace(/^\/+|\/+$/g, "")}/`;
629
+ } else if (arg.parentFolderId) {
630
+ const parentFolder = state.cloudFiles.foldersById[arg.parentFolderId];
631
+ if (parentFolder) {
632
+ prefix = `${parentFolder.folderPath}/`;
633
+ }
634
+ }
635
+ const existingNames = (() => {
636
+ const out = /* @__PURE__ */ new Set();
637
+ const targetPrefix = prefix;
638
+ for (const f of Object.values(state.cloudFiles.filesById)) {
639
+ if (!f) continue;
640
+ if (f.deletedAt) continue;
641
+ if (!f.filePath) continue;
642
+ const normalized = f.filePath.replace(/^\/+/, "");
643
+ if (!normalized.startsWith(targetPrefix)) continue;
644
+ const tail = normalized.slice(targetPrefix.length);
645
+ if (tail.includes("/")) continue;
646
+ if (tail) out.add(tail.toLowerCase());
647
+ }
648
+ return out;
649
+ })();
650
+ function uniqueName(originalName) {
651
+ const lowered = originalName.toLowerCase();
652
+ if (!existingNames.has(lowered)) {
653
+ existingNames.add(lowered);
654
+ return originalName;
655
+ }
656
+ const dot = originalName.lastIndexOf(".");
657
+ const isHidden = originalName.startsWith(".");
658
+ const stem = dot > 0 && !isHidden ? originalName.slice(0, dot) : originalName;
659
+ const ext = dot > 0 && !isHidden ? originalName.slice(dot) : "";
660
+ for (let i = 1; i < 1e3; i++) {
661
+ const candidate = `${stem} (${i})${ext}`;
662
+ if (!existingNames.has(candidate.toLowerCase())) {
663
+ existingNames.add(candidate.toLowerCase());
664
+ return candidate;
665
+ }
666
+ }
667
+ return originalName;
668
+ }
669
+ const overrides = arg.filenameOverrides ?? {};
670
+ const skipSet = new Set(arg.skipIndices ?? []);
671
+ const forceNewCopySet = new Set(arg.forceNewCopyIndices ?? []);
672
+ for (const [, name] of Object.entries(overrides)) {
673
+ if (typeof name === "string") existingNames.add(name.toLowerCase());
674
+ }
675
+ const queue = arg.files.map((file, index) => ({ index, file })).filter(({ index }) => !skipSet.has(index));
676
+ async function worker() {
677
+ while (queue.length) {
678
+ const next = queue.shift();
679
+ if (!next) return;
680
+ const { file, index } = next;
681
+ const overrideName = overrides[index];
682
+ const forcedCopy = forceNewCopySet.has(index);
683
+ const dedupKey = !overrideName && !forcedCopy ? uploadDedupKey(prefix, file.name, file.size) : null;
684
+ let dedupClaim = null;
685
+ if (dedupKey) {
686
+ const claim = claimUpload(dedupKey);
687
+ if (claim.shared) {
688
+ try {
689
+ const outcome = await claim.promise;
690
+ uploaded.push(outcome.fileId);
691
+ continue;
692
+ } catch {
693
+ }
694
+ } else {
695
+ dedupClaim = claim;
696
+ }
697
+ }
698
+ const requestId = newRequestId();
699
+ const targetName = typeof overrideName === "string" && overrideName ? overrideName : uniqueName(file.name);
700
+ dispatch(
701
+ trackUploadStart({
702
+ requestId,
703
+ fileName: targetName,
704
+ fileSize: file.size,
705
+ parentFolderId: arg.parentFolderId ?? null,
706
+ folderPath: arg.folderPath ?? null
707
+ })
708
+ );
709
+ registerRequest({
710
+ requestId,
711
+ kind: "upload",
712
+ resourceId: null,
713
+ resourceType: "file"
714
+ });
715
+ try {
716
+ const { data } = await Files.uploadFileWithProgress(
717
+ {
718
+ file,
719
+ filePath: `${prefix}${targetName}`,
720
+ visibility: arg.visibility ?? "personal",
721
+ shareWith: arg.shareWith,
722
+ shareLevel: arg.shareLevel,
723
+ changeSummary: arg.changeSummary,
724
+ metadata: arg.metadata,
725
+ options: arg.options,
726
+ ...forceNewCopySet.has(index) ? {
727
+ intent: "force_new_copy",
728
+ reason: "User selected Make a copy in the duplicate upload dialog"
729
+ } : {}
730
+ },
731
+ (ev) => dispatch(
732
+ updateUploadProgress({
733
+ requestId,
734
+ bytesUploaded: ev.loaded
735
+ })
736
+ ),
737
+ {
738
+ requestId,
739
+ // Idempotency key — same value across automatic retries of the
740
+ // same intended upload. The backend stores it in
741
+ // `metadata._idempotency_key` so retries don't create duplicate
742
+ // version rows. We reuse `requestId` because it's generated
743
+ // once per intended upload, before any retry.
744
+ idempotencyKey: requestId
745
+ }
746
+ );
747
+ dispatch(
748
+ upsertFile(
749
+ apiFileRecordToCloudFile({
750
+ id: data.file_id,
751
+ owner_id: "",
752
+ file_path: data.file_path,
753
+ file_name: data.file_path.split("/").pop() ?? targetName,
754
+ mime_type: file.type || null,
755
+ // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.
756
+ size_bytes: data.size_bytes,
757
+ checksum: data.checksum,
758
+ visibility: arg.visibility ?? "personal",
759
+ current_version: data.version_number,
760
+ parent_folder_id: arg.parentFolderId ?? null,
761
+ metadata: arg.metadata ?? {},
762
+ // P1-7: stamp the optimistic row with the current time instead of
763
+ // null. The upload just happened, so "now" is accurate for the
764
+ // recents filter / "sort by modified" until the realtime echo
765
+ // delivers the authoritative server timestamp. Null sorted the
766
+ // freshly-uploaded file to the wrong end of the list.
767
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
768
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
769
+ deleted_at: null
770
+ })
771
+ )
772
+ );
773
+ invalidateBlobCache(data.file_id);
774
+ invalidateOfficeExtraction(data.file_id);
775
+ dispatch(
776
+ attachChildToFolder({
777
+ parentFolderId: arg.parentFolderId ?? null,
778
+ kind: "file",
779
+ id: data.file_id
780
+ })
781
+ );
782
+ dispatch(
783
+ updateUploadStatus({
784
+ requestId,
785
+ status: "success",
786
+ fileId: data.file_id
787
+ })
788
+ );
789
+ uploaded.push(data.file_id);
790
+ dedupClaim?.settle(
791
+ Promise.resolve({ fileId: data.file_id, filePath: data.file_path })
792
+ );
793
+ } catch (err) {
794
+ const message = extractErrorMessage(err);
795
+ dispatch(
796
+ updateUploadStatus({
797
+ requestId,
798
+ status: "error",
799
+ error: message
800
+ })
801
+ );
802
+ failed.push({ name: file.name, error: message });
803
+ dedupClaim?.settle(Promise.reject(err));
804
+ } finally {
805
+ releaseRequest(requestId);
806
+ }
807
+ }
808
+ }
809
+ await Promise.all(
810
+ Array.from(
811
+ { length: Math.min(concurrency, arg.files.length) },
812
+ () => worker()
813
+ )
814
+ );
815
+ announceUploadFailures(failed);
816
+ return { uploaded, failed };
817
+ });
818
+ const clearUploadEntry = createAsyncThunk("cloudFiles/clearUpload", async ({ requestId }, { dispatch }) => {
819
+ dispatch(clearUpload({ requestId }));
820
+ });
821
+ const renameFile = createAsyncThunk(
822
+ "cloudFiles/rename",
823
+ async ({ fileId, newName }, { dispatch, getState }) => {
824
+ const record = getFileFromState(getState(), fileId);
825
+ if (!record)
826
+ throw new Error(
827
+ "We couldn't find that file in this view. Refresh and try again."
828
+ );
829
+ const requestId = newRequestId();
830
+ const seq = beginResourceOp(fileId);
831
+ const snapshot = {
832
+ fileName: record.fileName,
833
+ filePath: record.filePath
834
+ };
835
+ const pathParts = record.filePath.split("/");
836
+ pathParts[pathParts.length - 1] = newName;
837
+ const newPath = pathParts.join("/");
838
+ dispatch(setFileField({ id: fileId, field: "fileName", value: newName }));
839
+ dispatch(setFileField({ id: fileId, field: "filePath", value: newPath }));
840
+ dispatch(addFilePendingRequest({ id: fileId, requestId }));
841
+ registerRequest({
842
+ requestId,
843
+ kind: "rename",
844
+ resourceId: fileId,
845
+ resourceType: "file"
846
+ });
847
+ try {
848
+ const { data } = await Files.renameFile(
849
+ fileId,
850
+ { new_path: newPath },
851
+ { requestId }
852
+ );
853
+ if (isLatestResourceOp(fileId, seq)) {
854
+ dispatch(upsertFile(apiFileRecordToCloudFile(data)));
855
+ dispatch(markFileSaved({ id: fileId }));
856
+ }
857
+ } catch (err) {
858
+ if (isLatestResourceOp(fileId, seq)) {
859
+ dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));
860
+ const msg = extractErrorMessage(err);
861
+ dispatch(setFileError({ id: fileId, error: msg }));
862
+ }
863
+ throw err;
864
+ } finally {
865
+ dispatch(removeFilePendingRequest({ id: fileId, requestId }));
866
+ releaseRequest(requestId);
867
+ }
868
+ }
869
+ );
870
+ const moveFile = createAsyncThunk(
871
+ "cloudFiles/move",
872
+ async ({ fileId, newParentFolderId }, { dispatch, getState }) => {
873
+ const record = getFileFromState(getState(), fileId);
874
+ if (!record)
875
+ throw new Error(
876
+ "We couldn't find that file in this view. Refresh and try again."
877
+ );
878
+ const oldParentId = record.parentFolderId;
879
+ const requestId = newRequestId();
880
+ const seq = beginResourceOp(fileId);
881
+ const snapshot = {
882
+ parentFolderId: oldParentId
883
+ };
884
+ dispatch(
885
+ setFileField({
886
+ id: fileId,
887
+ field: "parentFolderId",
888
+ value: newParentFolderId
889
+ })
890
+ );
891
+ dispatch(
892
+ detachChildFromFolder({
893
+ parentFolderId: oldParentId,
894
+ kind: "file",
895
+ id: fileId
896
+ })
897
+ );
898
+ dispatch(
899
+ attachChildToFolder({
900
+ parentFolderId: newParentFolderId,
901
+ kind: "file",
902
+ id: fileId
903
+ })
904
+ );
905
+ dispatch(addFilePendingRequest({ id: fileId, requestId }));
906
+ registerRequest({
907
+ requestId,
908
+ kind: "move",
909
+ resourceId: fileId,
910
+ resourceType: "file"
911
+ });
912
+ try {
913
+ const targetFolder = newParentFolderId === null ? null : getState().cloudFiles.foldersById[newParentFolderId] ?? null;
914
+ const targetPrefix = targetFolder ? `${targetFolder.folderPath}/` : "";
915
+ const newPath = `${targetPrefix}${record.fileName}`;
916
+ const { data } = await Files.renameFile(
917
+ fileId,
918
+ { new_path: newPath },
919
+ { requestId }
920
+ );
921
+ if (isLatestResourceOp(fileId, seq)) {
922
+ dispatch(upsertFile(apiFileRecordToCloudFile(data)));
923
+ dispatch(markFileSaved({ id: fileId }));
924
+ }
925
+ } catch (err) {
926
+ if (isLatestResourceOp(fileId, seq)) {
927
+ dispatch(
928
+ detachChildFromFolder({
929
+ parentFolderId: newParentFolderId,
930
+ kind: "file",
931
+ id: fileId
932
+ })
933
+ );
934
+ dispatch(
935
+ attachChildToFolder({
936
+ parentFolderId: oldParentId,
937
+ kind: "file",
938
+ id: fileId
939
+ })
940
+ );
941
+ dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));
942
+ const msg = extractErrorMessage(err);
943
+ dispatch(setFileError({ id: fileId, error: msg }));
944
+ }
945
+ throw err;
946
+ } finally {
947
+ dispatch(removeFilePendingRequest({ id: fileId, requestId }));
948
+ releaseRequest(requestId);
949
+ }
950
+ }
951
+ );
952
+ const updateFileMetadata = createAsyncThunk(
953
+ "cloudFiles/updateMetadata",
954
+ async ({ fileId, patch }, { dispatch, getState }) => {
955
+ const record = getFileFromState(getState(), fileId);
956
+ if (!record)
957
+ throw new Error(
958
+ "We couldn't find that file in this view. Refresh and try again."
959
+ );
960
+ const requestId = newRequestId();
961
+ const seq = beginResourceOp(fileId);
962
+ const snapshot = {
963
+ visibility: record.visibility,
964
+ metadata: record.metadata
965
+ };
966
+ if (patch.visibility !== void 0) {
967
+ dispatch(
968
+ setFileField({
969
+ id: fileId,
970
+ field: "visibility",
971
+ value: patch.visibility
972
+ })
973
+ );
974
+ }
975
+ if (patch.metadata !== void 0) {
976
+ dispatch(
977
+ setFileField({
978
+ id: fileId,
979
+ field: "metadata",
980
+ value: patch.metadata
981
+ })
982
+ );
983
+ }
984
+ dispatch(addFilePendingRequest({ id: fileId, requestId }));
985
+ registerRequest({
986
+ requestId,
987
+ kind: "update",
988
+ resourceId: fileId,
989
+ resourceType: "file"
990
+ });
991
+ try {
992
+ const { data } = await Files.patchFile(
993
+ fileId,
994
+ {
995
+ visibility: patch.visibility,
996
+ metadata: patch.metadata
997
+ },
998
+ { requestId }
999
+ );
1000
+ if (isLatestResourceOp(fileId, seq)) {
1001
+ dispatch(upsertFile(apiFileRecordToCloudFile(data)));
1002
+ dispatch(markFileSaved({ id: fileId }));
1003
+ }
1004
+ } catch (err) {
1005
+ if (isLatestResourceOp(fileId, seq)) {
1006
+ dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));
1007
+ const msg = extractErrorMessage(err);
1008
+ dispatch(setFileError({ id: fileId, error: msg }));
1009
+ }
1010
+ throw err;
1011
+ } finally {
1012
+ dispatch(removeFilePendingRequest({ id: fileId, requestId }));
1013
+ releaseRequest(requestId);
1014
+ }
1015
+ }
1016
+ );
1017
+ const deleteFile = createAsyncThunk(
1018
+ "cloudFiles/delete",
1019
+ async ({ fileId }, { dispatch, getState }) => {
1020
+ const record = getFileFromState(getState(), fileId);
1021
+ if (!record) return;
1022
+ const parentFolderId = record.parentFolderId;
1023
+ const requestId = newRequestId();
1024
+ dispatch(removeFile({ id: fileId }));
1025
+ dispatch(
1026
+ detachChildFromFolder({
1027
+ parentFolderId,
1028
+ kind: "file",
1029
+ id: fileId
1030
+ })
1031
+ );
1032
+ invalidateBlobCache(fileId);
1033
+ invalidateOfficeExtraction(fileId);
1034
+ registerRequest({
1035
+ requestId,
1036
+ kind: "delete",
1037
+ resourceId: fileId,
1038
+ resourceType: "file"
1039
+ });
1040
+ try {
1041
+ await softDeleteFileDirect(fileId);
1042
+ } catch (err) {
1043
+ dispatch(upsertFile(toCloudFilePartial(record)));
1044
+ dispatch(
1045
+ attachChildToFolder({
1046
+ parentFolderId,
1047
+ kind: "file",
1048
+ id: fileId
1049
+ })
1050
+ );
1051
+ throw err;
1052
+ } finally {
1053
+ releaseRequest(requestId);
1054
+ }
1055
+ }
1056
+ );
1057
+ function toCloudFilePartial(record) {
1058
+ const {
1059
+ _dirty,
1060
+ _dirtyFields,
1061
+ _fieldHistory,
1062
+ _loadedFields,
1063
+ _loading,
1064
+ _error,
1065
+ _pendingRequestIds,
1066
+ ...domain
1067
+ } = record;
1068
+ return domain;
1069
+ }
1070
+ function toCloudFolderPartial(record) {
1071
+ const {
1072
+ _dirty,
1073
+ _dirtyFields,
1074
+ _fieldHistory,
1075
+ _loadedFields,
1076
+ _loading,
1077
+ _error,
1078
+ _pendingRequestIds,
1079
+ ...domain
1080
+ } = record;
1081
+ return domain;
1082
+ }
1083
+ const loadTrash = createAsyncThunk(
1084
+ "cloudFiles/loadTrash",
1085
+ async ({ userId }, { dispatch }) => {
1086
+ const rows = [];
1087
+ for (let page = 0; page < TREE_MAX_PAGES; page += 1) {
1088
+ const { data, error } = await supabase.rpc("get_user_file_tree", {
1089
+ p_user_id: userId,
1090
+ p_limit: TREE_PAGE_SIZE,
1091
+ p_offset: page * TREE_PAGE_SIZE,
1092
+ p_include_folders: true,
1093
+ p_include_deleted: true
1094
+ });
1095
+ if (error) throw error;
1096
+ const pageRows = parseCloudTreeRows(data);
1097
+ rows.push(...pageRows);
1098
+ if (pageRows.length < TREE_PAGE_SIZE) break;
1099
+ }
1100
+ const files = [];
1101
+ const folders = [];
1102
+ for (const row of rows) {
1103
+ if (!row.deleted_at) continue;
1104
+ if (row.kind === "file") {
1105
+ files.push({
1106
+ id: row.id,
1107
+ ownerId: row.owner_id,
1108
+ filePath: row.file_path,
1109
+ fileName: row.file_name,
1110
+ parentFolderId: row.parent_folder_id,
1111
+ mimeType: row.mime_type,
1112
+ fileSize: row.size_bytes,
1113
+ visibility: row.visibility,
1114
+ currentVersion: row.current_version,
1115
+ createdAt: row.created_at,
1116
+ updatedAt: row.updated_at,
1117
+ deletedAt: row.deleted_at,
1118
+ originDeviceId: row.origin_device_id
1119
+ });
1120
+ } else {
1121
+ folders.push({
1122
+ id: row.id,
1123
+ ownerId: row.owner_id,
1124
+ folderPath: row.folder_path,
1125
+ folderName: row.folder_name,
1126
+ parentId: row.parent_id,
1127
+ visibility: row.visibility,
1128
+ createdAt: row.created_at,
1129
+ updatedAt: row.updated_at,
1130
+ deletedAt: row.deleted_at
1131
+ });
1132
+ }
1133
+ }
1134
+ dispatch(upsertFiles(files));
1135
+ dispatch(upsertFolders(folders));
1136
+ }
1137
+ );
1138
+ const restoreFile = createAsyncThunk(
1139
+ "cloudFiles/restoreFile",
1140
+ async ({ fileId }, { dispatch, getState }) => {
1141
+ const record = getFileFromState(getState(), fileId);
1142
+ if (!record?.deletedAt) return;
1143
+ const requestId = newRequestId();
1144
+ dispatch(upsertFile({ ...toCloudFilePartial(record), deletedAt: null }));
1145
+ dispatch(
1146
+ attachChildToFolder({
1147
+ parentFolderId: record.parentFolderId,
1148
+ kind: "file",
1149
+ id: fileId
1150
+ })
1151
+ );
1152
+ registerRequest({
1153
+ requestId,
1154
+ kind: "file-restore",
1155
+ resourceId: fileId,
1156
+ resourceType: "file"
1157
+ });
1158
+ try {
1159
+ await restoreFileDirect(fileId);
1160
+ } catch (err) {
1161
+ dispatch(upsertFile(toCloudFilePartial(record)));
1162
+ dispatch(
1163
+ detachChildFromFolder({
1164
+ parentFolderId: record.parentFolderId,
1165
+ kind: "file",
1166
+ id: fileId
1167
+ })
1168
+ );
1169
+ throw err;
1170
+ } finally {
1171
+ releaseRequest(requestId);
1172
+ }
1173
+ }
1174
+ );
1175
+ const restoreFolder = createAsyncThunk("cloudFiles/restoreFolder", async ({ folderId }, { dispatch, getState }) => {
1176
+ const folder = getState().cloudFiles.foldersById[folderId];
1177
+ if (!folder?.deletedAt) return;
1178
+ await restoreFolderDirect(folderId);
1179
+ dispatch(upsertFolder({ ...toCloudFolderPartial(folder), deletedAt: null }));
1180
+ const userId = folder.ownerId;
1181
+ await dispatch(loadUserFileTree({ userId }));
1182
+ await dispatch(loadTrash({ userId }));
1183
+ });
1184
+ const restoreVersion = createAsyncThunk(
1185
+ "cloudFiles/restoreVersion",
1186
+ async ({ fileId, versionNumber }, { dispatch }) => {
1187
+ if (isVirtualResourceId(fileId)) return;
1188
+ dispatch(setFileLoading({ id: fileId, loading: true }));
1189
+ const requestId = newRequestId();
1190
+ registerRequest({
1191
+ requestId,
1192
+ kind: "restore-version",
1193
+ resourceId: fileId,
1194
+ resourceType: "file"
1195
+ });
1196
+ try {
1197
+ const { data } = await Versions.restoreVersion(fileId, versionNumber, {
1198
+ requestId
1199
+ });
1200
+ dispatch(upsertFile(apiFileRecordToCloudFile(data)));
1201
+ invalidateBlobCache(fileId);
1202
+ invalidateOfficeExtraction(fileId);
1203
+ await dispatch(loadFileVersions({ fileId })).unwrap();
1204
+ } finally {
1205
+ dispatch(setFileLoading({ id: fileId, loading: false }));
1206
+ releaseRequest(requestId);
1207
+ }
1208
+ }
1209
+ );
1210
+ const saveFileNewVersion = createAsyncThunk(
1211
+ "cloudFiles/saveFileNewVersion",
1212
+ async ({ fileId, content, changeSummary }, { dispatch, getState }) => {
1213
+ if (isVirtualResourceId(fileId)) {
1214
+ throw new Error(
1215
+ "saveFileNewVersion is for stored files; virtual files save through writeAny."
1216
+ );
1217
+ }
1218
+ let record = getState().cloudFiles.filesById[fileId];
1219
+ if (!record?.fileName) {
1220
+ const { data } = await Files.getFileMetadata(fileId);
1221
+ const fresh = apiFileRecordToCloudFile(data);
1222
+ dispatch(upsertFile(fresh));
1223
+ record = fresh;
1224
+ }
1225
+ const mimeType = record.mimeType ?? "text/plain";
1226
+ const body = typeof content === "string" ? new Blob([content], { type: mimeType }) : content;
1227
+ const upload = new File([body], record.fileName, { type: mimeType });
1228
+ dispatch(setFileLoading({ id: fileId, loading: true }));
1229
+ const requestId = newRequestId();
1230
+ registerRequest({
1231
+ requestId,
1232
+ kind: "upload",
1233
+ resourceId: fileId,
1234
+ resourceType: "file"
1235
+ });
1236
+ try {
1237
+ const reqOpts = { requestId, idempotencyKey: requestId };
1238
+ const summary = changeSummary ?? "Edited in place";
1239
+ const { data } = await Files.uploadNewVersion(
1240
+ fileId,
1241
+ { file: upload, changeSummary: summary },
1242
+ reqOpts
1243
+ );
1244
+ if (data.file_id !== fileId || data.is_new) {
1245
+ throw new Error(
1246
+ `Saving "${record.fileName}" did not update it \u2014 the files service wrote a separate file (${data.file_path}, id ${data.file_id}) instead of version ${record.currentVersion + 1}. Your original is unchanged; remove the extra file from the folder.`
1247
+ );
1248
+ }
1249
+ dispatch(
1250
+ upsertFile({
1251
+ id: fileId,
1252
+ currentVersion: data.version_number,
1253
+ fileSize: data.size_bytes,
1254
+ checksum: data.checksum,
1255
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1256
+ })
1257
+ );
1258
+ replaceWithSavedBytes(fileId, body, data.version_number);
1259
+ invalidateOfficeExtraction(fileId);
1260
+ void dispatch(loadFileVersions({ fileId })).unwrap().catch((err) => {
1261
+ console.error(
1262
+ "[saveFileNewVersion] saved, but the version list did not refresh:",
1263
+ err
1264
+ );
1265
+ });
1266
+ return { fileId, versionNumber: data.version_number };
1267
+ } finally {
1268
+ dispatch(setFileLoading({ id: fileId, loading: false }));
1269
+ releaseRequest(requestId);
1270
+ }
1271
+ }
1272
+ );
1273
+ function requireByGranteeType(granteeType) {
1274
+ if (granteeType === "public") {
1275
+ throw new Error(
1276
+ "Public access is toggled via the resource's visibility, not the by-grantee grant/revoke path."
1277
+ );
1278
+ }
1279
+ return granteeType ?? "user";
1280
+ }
1281
+ const grantPermission = createAsyncThunk("cloudFiles/grantPermission", async (arg, { dispatch }) => {
1282
+ if (isVirtualResourceId(arg.resourceId)) {
1283
+ throw new Error("Sharing isn't available for this source yet.");
1284
+ }
1285
+ const requestId = newRequestId();
1286
+ registerRequest({
1287
+ requestId,
1288
+ kind: "grant-permission",
1289
+ resourceId: arg.resourceId,
1290
+ resourceType: arg.resourceType
1291
+ });
1292
+ try {
1293
+ const body = {
1294
+ grantee_id: arg.granteeId,
1295
+ level: arg.level,
1296
+ grantee_type: requireByGranteeType(arg.granteeType),
1297
+ expires_at: arg.expiresAt ?? null
1298
+ };
1299
+ const { data } = arg.resourceType === "folder" ? await Permissions.grantFolderPermission(arg.resourceId, body, {
1300
+ requestId
1301
+ }) : await Permissions.grantFilePermission(arg.resourceId, body, {
1302
+ requestId
1303
+ });
1304
+ dispatch(
1305
+ upsertPermissionsForResource({
1306
+ resourceId: arg.resourceId,
1307
+ permissions: [rpcPermissionRowToCloudFilePermission(data)]
1308
+ })
1309
+ );
1310
+ void dispatch(loadPermissions({ resourceId: arg.resourceId }));
1311
+ } finally {
1312
+ releaseRequest(requestId);
1313
+ }
1314
+ });
1315
+ const revokePermission = createAsyncThunk("cloudFiles/revokePermission", async (arg, { dispatch }) => {
1316
+ if (isVirtualResourceId(arg.resourceId)) {
1317
+ throw new Error("Sharing isn't available for this source yet.");
1318
+ }
1319
+ const requestId = newRequestId();
1320
+ registerRequest({
1321
+ requestId,
1322
+ kind: "revoke-permission",
1323
+ resourceId: arg.resourceId,
1324
+ resourceType: arg.resourceType
1325
+ });
1326
+ try {
1327
+ const granteeType = requireByGranteeType(arg.granteeType);
1328
+ if (arg.resourceType === "folder") {
1329
+ await Permissions.revokeFolderPermission(
1330
+ arg.resourceId,
1331
+ arg.granteeId,
1332
+ { granteeType },
1333
+ { requestId }
1334
+ );
1335
+ } else {
1336
+ await Permissions.revokeFilePermission(
1337
+ arg.resourceId,
1338
+ arg.granteeId,
1339
+ { granteeType },
1340
+ { requestId }
1341
+ );
1342
+ }
1343
+ dispatch(
1344
+ removePermissionForResource({
1345
+ resourceId: arg.resourceId,
1346
+ granteeId: arg.granteeId,
1347
+ granteeType
1348
+ })
1349
+ );
1350
+ } finally {
1351
+ releaseRequest(requestId);
1352
+ }
1353
+ });
1354
+ const createShareLink = createAsyncThunk("cloudFiles/createShareLink", async (arg, { dispatch, getState }) => {
1355
+ if (isVirtualResourceId(arg.resourceId)) {
1356
+ throw new Error("Share links aren't available for this source yet.");
1357
+ }
1358
+ if (hasInFlight(arg.resourceId, "create-share-link")) {
1359
+ throw new Error("A share link is already being created for this item.");
1360
+ }
1361
+ const requestId = newRequestId();
1362
+ registerRequest({
1363
+ requestId,
1364
+ kind: "create-share-link",
1365
+ resourceId: arg.resourceId,
1366
+ resourceType: arg.resourceType
1367
+ });
1368
+ try {
1369
+ const created = await createCanonicalShareLink({
1370
+ resourceType: arg.resourceType,
1371
+ resourceId: arg.resourceId,
1372
+ permissionLevel: arg.permissionLevel,
1373
+ expiresAt: arg.expiresAt ?? null,
1374
+ maxUses: arg.maxUses ?? null
1375
+ });
1376
+ if (!created.success || !created.token) {
1377
+ throw new Error(created.error ?? "Failed to create share link");
1378
+ }
1379
+ await dispatch(
1380
+ loadShareLinks({
1381
+ resourceId: arg.resourceId,
1382
+ resourceType: arg.resourceType
1383
+ })
1384
+ ).unwrap();
1385
+ const link = (getState().cloudFiles.shareLinksByResourceId[arg.resourceId] ?? []).find((l) => l.shareToken === created.token);
1386
+ if (!link) {
1387
+ throw new Error("Share link was created but could not be loaded back");
1388
+ }
1389
+ return link;
1390
+ } finally {
1391
+ releaseRequest(requestId);
1392
+ }
1393
+ });
1394
+ const revokeShareLink = createAsyncThunk("cloudFiles/revokeShareLink", async ({ linkId }, { dispatch }) => {
1395
+ const requestId = newRequestId();
1396
+ registerRequest({
1397
+ requestId,
1398
+ kind: "revoke-share-link",
1399
+ resourceId: null,
1400
+ resourceType: null
1401
+ });
1402
+ try {
1403
+ const result = await revokeCanonicalShareLink(linkId);
1404
+ if (!result.success) {
1405
+ throw new Error(result.error ?? "Failed to revoke share link");
1406
+ }
1407
+ dispatch(removeShareLink({ linkId }));
1408
+ } finally {
1409
+ releaseRequest(requestId);
1410
+ }
1411
+ });
1412
+ const getFileUrl = createAsyncThunk("cloudFiles/getFileUrl", async ({ fileId }) => {
1413
+ if (isVirtualResourceId(fileId)) {
1414
+ throw new Error("Byte URLs aren't available for virtual sources");
1415
+ }
1416
+ const url = await fileHandler.use({ kind: "file_id", fileId }).as({ kind: "html_src" });
1417
+ if (!url) {
1418
+ throw new Error(`Could not resolve a URL for file ${fileId}`);
1419
+ }
1420
+ return { url };
1421
+ });
1422
+ function toastBulkPartialFailure(result, verb) {
1423
+ if (result.failed > 0) {
1424
+ const total = result.succeeded + result.failed;
1425
+ toast.error(`Couldn't ${verb} ${result.failed} of ${total} items`, {
1426
+ description: result.succeeded > 0 ? `${result.succeeded} succeeded. The rest were restored.` : "All items were restored."
1427
+ });
1428
+ }
1429
+ }
1430
+ const bulkDeleteFiles = createAsyncThunk("cloudFiles/bulkDeleteFiles", async (arg, { dispatch, getState }) => {
1431
+ if (arg.fileIds.length === 0) {
1432
+ return { results: [], succeeded: 0, failed: 0 };
1433
+ }
1434
+ const state = getState();
1435
+ const snapshots = /* @__PURE__ */ new Map();
1436
+ for (const id of arg.fileIds) {
1437
+ const file = state.cloudFiles.filesById[id];
1438
+ if (file) snapshots.set(id, file);
1439
+ }
1440
+ const requestId = newRequestId();
1441
+ registerRequest({
1442
+ requestId,
1443
+ kind: "bulk-delete-files",
1444
+ resourceId: null,
1445
+ resourceType: "file"
1446
+ });
1447
+ for (const id of arg.fileIds) {
1448
+ dispatch(removeFile({ id }));
1449
+ invalidateBlobCache(id);
1450
+ invalidateOfficeExtraction(id);
1451
+ }
1452
+ try {
1453
+ const { data } = await Files.bulkDeleteFiles(
1454
+ { file_ids: arg.fileIds },
1455
+ { requestId }
1456
+ );
1457
+ const result = data ?? {
1458
+ results: arg.fileIds.map((id) => ({ id, ok: true, error: null })),
1459
+ succeeded: arg.fileIds.length,
1460
+ failed: 0
1461
+ };
1462
+ for (const r of result.results) {
1463
+ if (r.ok) continue;
1464
+ const snap = snapshots.get(r.id);
1465
+ if (snap) dispatch(upsertFile(snap));
1466
+ }
1467
+ toastBulkPartialFailure(result, "delete");
1468
+ return result;
1469
+ } catch (err) {
1470
+ for (const snap of snapshots.values()) dispatch(upsertFile(snap));
1471
+ throw err;
1472
+ } finally {
1473
+ releaseRequest(requestId);
1474
+ }
1475
+ });
1476
+ const bulkMoveFiles = createAsyncThunk("cloudFiles/bulkMoveFiles", async (arg, { dispatch, getState }) => {
1477
+ if (arg.fileIds.length === 0) {
1478
+ return { results: [], succeeded: 0, failed: 0 };
1479
+ }
1480
+ const state = getState();
1481
+ const snapshots = /* @__PURE__ */ new Map();
1482
+ for (const id of arg.fileIds) {
1483
+ const file = state.cloudFiles.filesById[id];
1484
+ if (file) snapshots.set(id, file);
1485
+ }
1486
+ const requestId = newRequestId();
1487
+ registerRequest({
1488
+ requestId,
1489
+ kind: "bulk-move-files",
1490
+ resourceId: null,
1491
+ resourceType: "file"
1492
+ });
1493
+ for (const [id, file] of snapshots) {
1494
+ if (file.parentFolderId === arg.newParentFolderId) continue;
1495
+ dispatch(
1496
+ detachChildFromFolder({
1497
+ parentFolderId: file.parentFolderId,
1498
+ kind: "file",
1499
+ id
1500
+ })
1501
+ );
1502
+ dispatch(
1503
+ attachChildToFolder({
1504
+ parentFolderId: arg.newParentFolderId,
1505
+ kind: "file",
1506
+ id
1507
+ })
1508
+ );
1509
+ dispatch(upsertFile({ ...file, parentFolderId: arg.newParentFolderId }));
1510
+ }
1511
+ try {
1512
+ const { data } = await Files.bulkMoveFiles(
1513
+ {
1514
+ file_ids: arg.fileIds,
1515
+ new_parent_folder_id: arg.newParentFolderId
1516
+ },
1517
+ { requestId }
1518
+ );
1519
+ for (const r of data.results) {
1520
+ if (r.ok) continue;
1521
+ const snap = snapshots.get(r.id);
1522
+ if (!snap || snap.parentFolderId === arg.newParentFolderId) continue;
1523
+ dispatch(
1524
+ detachChildFromFolder({
1525
+ parentFolderId: arg.newParentFolderId,
1526
+ kind: "file",
1527
+ id: snap.id
1528
+ })
1529
+ );
1530
+ dispatch(
1531
+ attachChildToFolder({
1532
+ parentFolderId: snap.parentFolderId,
1533
+ kind: "file",
1534
+ id: snap.id
1535
+ })
1536
+ );
1537
+ dispatch(upsertFile(snap));
1538
+ }
1539
+ toastBulkPartialFailure(data, "move");
1540
+ return data;
1541
+ } catch (err) {
1542
+ for (const snap of snapshots.values()) {
1543
+ if (snap.parentFolderId === arg.newParentFolderId) continue;
1544
+ dispatch(
1545
+ detachChildFromFolder({
1546
+ parentFolderId: arg.newParentFolderId,
1547
+ kind: "file",
1548
+ id: snap.id
1549
+ })
1550
+ );
1551
+ dispatch(
1552
+ attachChildToFolder({
1553
+ parentFolderId: snap.parentFolderId,
1554
+ kind: "file",
1555
+ id: snap.id
1556
+ })
1557
+ );
1558
+ dispatch(upsertFile(snap));
1559
+ }
1560
+ throw err;
1561
+ } finally {
1562
+ releaseRequest(requestId);
1563
+ }
1564
+ });
1565
+ const bulkMoveFolders = createAsyncThunk("cloudFiles/bulkMoveFolders", async (arg, { dispatch, getState }) => {
1566
+ if (arg.folderIds.length === 0) {
1567
+ return { results: [], succeeded: 0, failed: 0 };
1568
+ }
1569
+ const state = getState();
1570
+ const snapshots = /* @__PURE__ */ new Map();
1571
+ for (const id of arg.folderIds) {
1572
+ const folder = state.cloudFiles.foldersById[id];
1573
+ if (folder) snapshots.set(id, folder);
1574
+ }
1575
+ const requestId = newRequestId();
1576
+ registerRequest({
1577
+ requestId,
1578
+ kind: "bulk-move-folders",
1579
+ resourceId: null,
1580
+ resourceType: "folder"
1581
+ });
1582
+ for (const [id, folder] of snapshots) {
1583
+ if (folder.parentId === arg.newParentId) continue;
1584
+ dispatch(
1585
+ detachChildFromFolder({
1586
+ parentFolderId: folder.parentId,
1587
+ kind: "folder",
1588
+ id
1589
+ })
1590
+ );
1591
+ dispatch(
1592
+ attachChildToFolder({
1593
+ parentFolderId: arg.newParentId,
1594
+ kind: "folder",
1595
+ id
1596
+ })
1597
+ );
1598
+ dispatch(upsertFolder({ ...folder, parentId: arg.newParentId }));
1599
+ }
1600
+ try {
1601
+ const { data } = await Folders.bulkMoveFolders(
1602
+ { folder_ids: arg.folderIds, new_parent_id: arg.newParentId },
1603
+ { requestId }
1604
+ );
1605
+ for (const r of data.results) {
1606
+ if (r.ok) continue;
1607
+ const snap = snapshots.get(r.id);
1608
+ if (!snap || snap.parentId === arg.newParentId) continue;
1609
+ dispatch(
1610
+ detachChildFromFolder({
1611
+ parentFolderId: arg.newParentId,
1612
+ kind: "folder",
1613
+ id: snap.id
1614
+ })
1615
+ );
1616
+ dispatch(
1617
+ attachChildToFolder({
1618
+ parentFolderId: snap.parentId,
1619
+ kind: "folder",
1620
+ id: snap.id
1621
+ })
1622
+ );
1623
+ dispatch(upsertFolder(snap));
1624
+ }
1625
+ toastBulkPartialFailure(data, "move");
1626
+ return data;
1627
+ } catch (err) {
1628
+ for (const snap of snapshots.values()) {
1629
+ if (snap.parentId === arg.newParentId) continue;
1630
+ dispatch(
1631
+ detachChildFromFolder({
1632
+ parentFolderId: arg.newParentId,
1633
+ kind: "folder",
1634
+ id: snap.id
1635
+ })
1636
+ );
1637
+ dispatch(
1638
+ attachChildToFolder({
1639
+ parentFolderId: snap.parentId,
1640
+ kind: "folder",
1641
+ id: snap.id
1642
+ })
1643
+ );
1644
+ dispatch(upsertFolder(snap));
1645
+ }
1646
+ throw err;
1647
+ } finally {
1648
+ releaseRequest(requestId);
1649
+ }
1650
+ });
1651
+ export {
1652
+ announceUploadFailures,
1653
+ bulkDeleteFiles,
1654
+ bulkMoveFiles,
1655
+ bulkMoveFolders,
1656
+ clearUploadEntry,
1657
+ createFolder,
1658
+ createShareLink,
1659
+ deleteFile,
1660
+ deleteFolder,
1661
+ ensureCloudFileFields,
1662
+ ensureFolderPath,
1663
+ getFileUrl,
1664
+ grantPermission,
1665
+ loadFileVersions,
1666
+ loadFolderContents,
1667
+ loadPermissions,
1668
+ loadShareLinks,
1669
+ loadTrash,
1670
+ loadUserFileTree,
1671
+ moveFile,
1672
+ reconcileTree,
1673
+ renameFile,
1674
+ restoreFile,
1675
+ restoreFolder,
1676
+ restoreVersion,
1677
+ revokePermission,
1678
+ revokeShareLink,
1679
+ saveFileNewVersion,
1680
+ updateFileMetadata,
1681
+ updateFolder,
1682
+ uploadFiles
1683
+ };
1684
+ //# sourceMappingURL=thunks.js.map