blixify-ui-web 1.2.111 → 1.2.113

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.
@@ -0,0 +1,1163 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
37
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
38
+ };
39
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
40
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
41
+ return new (P || (P = Promise))(function (resolve, reject) {
42
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
43
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
44
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
45
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
46
+ });
47
+ };
48
+ var __generator = (this && this.__generator) || function (thisArg, body) {
49
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
50
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
51
+ function verb(n) { return function (v) { return step([n, v]); }; }
52
+ function step(op) {
53
+ if (f) throw new TypeError("Generator is already executing.");
54
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
55
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
56
+ if (y = 0, t) op = [op[0] & 2, t.value];
57
+ switch (op[0]) {
58
+ case 0: case 1: t = op; break;
59
+ case 4: _.label++; return { value: op[1], done: false };
60
+ case 5: _.label++; y = op[1]; op = [0]; continue;
61
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
62
+ default:
63
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
64
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
65
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
66
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
67
+ if (t[2]) _.ops.pop();
68
+ _.trys.pop(); continue;
69
+ }
70
+ op = body.call(thisArg, _);
71
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
72
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
73
+ }
74
+ };
75
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
76
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
77
+ if (ar || !(i in from)) {
78
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
79
+ ar[i] = from[i];
80
+ }
81
+ }
82
+ return to.concat(ar || Array.prototype.slice.call(from));
83
+ };
84
+ Object.defineProperty(exports, "__esModule", { value: true });
85
+ exports.DriveTemplate = void 0;
86
+ var react_1 = require("@headlessui/react");
87
+ var HeroIcons = __importStar(require("@heroicons/react/24/outline"));
88
+ var react_2 = __importStar(require("react"));
89
+ var uuid_1 = require("uuid");
90
+ var button_1 = require("../../action/button");
91
+ var iconButton_1 = require("../../action/iconButton");
92
+ var popUp_1 = require("../../advanced/popUp");
93
+ var emptyState_1 = require("../../display/emptyState");
94
+ var filePreview_1 = require("../../display/filePreview");
95
+ var loading_1 = require("../../display/loading");
96
+ var modal_1 = require("../../display/modal");
97
+ var newList_1 = require("../../display/newList");
98
+ var textInput_1 = require("../../input/textInput");
99
+ var uploadInput_1 = require("../../input/uploadInput");
100
+ var blobModule_1 = require("../blobModule");
101
+ var readQuery_1 = require("../readQuery");
102
+ var utils_1 = require("../utils");
103
+ var writeQuery_1 = require("../writeQuery");
104
+ var driveUtils_1 = require("./driveUtils");
105
+ __exportStar(require("./driveUtils"), exports);
106
+ var LAYOUT_STORAGE_PREFIX = "blixify-drive-layout-";
107
+ var SEARCH_DEBOUNCE_MS = 400;
108
+ // INFO: the same cap the path walk uses — a deeper chain is treated as a loop
109
+ var MAX_DELETE_DEPTH = 64;
110
+ /**
111
+ * A Google Drive-style folder browser over a collection whose rows are files
112
+ * and folders.
113
+ *
114
+ * INFO: a standalone template rather than a DataTemplate view — a drive has one
115
+ * fixed data structure, and sitting beside a table view let users add blank
116
+ * records or edit the columns the drive depends on.
117
+ */
118
+ var DriveTemplate = function (props) {
119
+ var _a, _b, _c, _d, _e, _f, _g;
120
+ var _h = (0, react_2.useState)(""), innerFolderId = _h[0], setInnerFolderId = _h[1];
121
+ var _j = (0, react_2.useState)([]), rows = _j[0], setRows = _j[1];
122
+ var _k = (0, react_2.useState)(true), loading = _k[0], setLoading = _k[1];
123
+ var _l = (0, react_2.useState)(new Map()), folders = _l[0], setFolders = _l[1];
124
+ var _m = (0, react_2.useState)(""), searchText = _m[0], setSearchText = _m[1];
125
+ var _o = (0, react_2.useState)(""), searchQuery = _o[0], setSearchQuery = _o[1];
126
+ var _p = (0, react_2.useState)(function () {
127
+ try {
128
+ var stored = window.localStorage.getItem(LAYOUT_STORAGE_PREFIX + props.collectionId);
129
+ if (stored === "grid" || stored === "list")
130
+ return stored;
131
+ }
132
+ catch (err) { }
133
+ // INFO: a phone has no room for tiles and a menu per tile
134
+ return typeof window !== "undefined" && window.innerWidth < 640
135
+ ? "list"
136
+ : "grid";
137
+ }), layout = _p[0], setLayout = _p[1];
138
+ var _q = (0, react_2.useState)(false), newFolderOpen = _q[0], setNewFolderOpen = _q[1];
139
+ var _r = (0, react_2.useState)(""), folderName = _r[0], setFolderName = _r[1];
140
+ var _s = (0, react_2.useState)(null), renameRow = _s[0], setRenameRow = _s[1];
141
+ var _t = (0, react_2.useState)(""), renameValue = _t[0], setRenameValue = _t[1];
142
+ var _u = (0, react_2.useState)(null), moveRow = _u[0], setMoveRow = _u[1];
143
+ var _v = (0, react_2.useState)(null), deleteRow = _v[0], setDeleteRow = _v[1];
144
+ var _w = (0, react_2.useState)(null), deleteIds = _w[0], setDeleteIds = _w[1];
145
+ var _x = (0, react_2.useState)(null), previewRow = _x[0], setPreviewRow = _x[1];
146
+ var _y = (0, react_2.useState)(false), uploadOpen = _y[0], setUploadOpen = _y[1];
147
+ var _z = (0, react_2.useState)({
148
+ data: [],
149
+ upload: [],
150
+ remove: [],
151
+ }), uploadFiles = _z[0], setUploadFiles = _z[1];
152
+ var _0 = (0, react_2.useState)({}), uploadStatus = _0[0], setUploadStatus = _0[1];
153
+ var _1 = (0, react_2.useState)(false), working = _1[0], setWorking = _1[1];
154
+ var _2 = (0, react_2.useState)(false), dragging = _2[0], setDragging = _2[1];
155
+ var _3 = (0, react_2.useState)(""), modalError = _3[0], setModalError = _3[1];
156
+ // INFO: dragenter and dragleave fire for every child the pointer crosses, so a
157
+ // plain boolean flickers off over each tile; a depth count only reaches zero
158
+ // when the drag has really left the drive
159
+ var dragDepthRef = (0, react_2.useRef)(0);
160
+ // INFO: a slow folder answering after a faster one must not overwrite it
161
+ var fetchSeqRef = (0, react_2.useRef)(0);
162
+ var folderSeqRef = (0, react_2.useRef)(0);
163
+ // INFO: a folder's contents are counted before its delete is confirmed. A
164
+ // count that finishes after the dialog closed or moved to another item must
165
+ // not attach its ids to that item's Delete button
166
+ var deleteSeqRef = (0, react_2.useRef)(0);
167
+ // INFO: what the drive is showing right now. A refresh that fires after a
168
+ // long upload reads this rather than the folder open when the upload began,
169
+ // or it would load the old folder under the new folder's path
170
+ var viewRef = (0, react_2.useRef)({
171
+ folderId: "",
172
+ searchQuery: "",
173
+ additionalQuery: [],
174
+ });
175
+ var devSettings = props.devSettings;
176
+ var serverId = (0, utils_1.parseServerId)(devSettings.server);
177
+ var folderId = (_a = props.folderId) !== null && _a !== void 0 ? _a : innerFolderId;
178
+ var additionalQuery = (_b = props.additionalQuery) !== null && _b !== void 0 ? _b : [];
179
+ var additionalQueryKey = JSON.stringify(additionalQuery);
180
+ var searching = !!searchQuery;
181
+ var dark = !!props.darkMode;
182
+ // INFO: one set of surface and text classes so every tile, menu and message
183
+ // follows darkMode, not only the header
184
+ var textStrong = dark ? "text-white" : "text-gray-900";
185
+ var textBody = dark ? "text-gray-300" : "text-gray-700";
186
+ var surface = dark
187
+ ? "border-white/10 bg-white/5 hover:bg-white/10"
188
+ : "border-gray-200 bg-white hover:bg-gray-50";
189
+ var canCreate = !!((_c = props.permissions) === null || _c === void 0 ? void 0 : _c.create);
190
+ var canUpdate = !!((_d = props.permissions) === null || _d === void 0 ? void 0 : _d.update);
191
+ var canDelete = !!((_e = props.permissions) === null || _e === void 0 ? void 0 : _e.delete);
192
+ var uploadEndpoint = devSettings.uploadEndpoint || "/api/asset";
193
+ var readQuery = (0, react_2.useMemo)(function () {
194
+ return new readQuery_1.ReadQuery({
195
+ dbModule: props.lib.axios,
196
+ dbType: devSettings.server,
197
+ dbCollection: props.collectionId,
198
+ dbEndpoint: devSettings.dbEndpoint,
199
+ dbAPIToken: devSettings.dbAPIToken,
200
+ dbUserToken: devSettings.dbUserToken,
201
+ });
202
+ }, [
203
+ props.lib.axios,
204
+ props.collectionId,
205
+ devSettings.server,
206
+ devSettings.dbEndpoint,
207
+ devSettings.dbAPIToken,
208
+ devSettings.dbUserToken,
209
+ ]);
210
+ var writeQuery = (0, react_2.useMemo)(function () {
211
+ return new writeQuery_1.WriteQuery({
212
+ dbModule: props.lib.axios,
213
+ dbType: devSettings.server,
214
+ dbCollection: props.collectionId,
215
+ dbEndpoint: devSettings.dbEndpoint,
216
+ dbAPIToken: devSettings.dbAPIToken,
217
+ dbUserToken: devSettings.dbUserToken,
218
+ });
219
+ }, [
220
+ props.lib.axios,
221
+ props.collectionId,
222
+ devSettings.server,
223
+ devSettings.dbEndpoint,
224
+ devSettings.dbAPIToken,
225
+ devSettings.dbUserToken,
226
+ ]);
227
+ var sortedRows = (0, driveUtils_1.handleSortDriveRows)(rows);
228
+ var path = (0, driveUtils_1.handleBuildFolderPath)(folders, folderId);
229
+ viewRef.current = { folderId: folderId, searchQuery: searchQuery, additionalQuery: additionalQuery };
230
+ (0, react_2.useEffect)(function () {
231
+ var timer = setTimeout(function () { return setSearchQuery(searchText.trim()); }, SEARCH_DEBOUNCE_MS);
232
+ return function () { return clearTimeout(timer); };
233
+ }, [searchText]);
234
+ (0, react_2.useEffect)(function () {
235
+ handleFetchRows();
236
+ // eslint-disable-next-line react-hooks/exhaustive-deps
237
+ }, [props.collectionId, folderId, searchQuery, additionalQueryKey]);
238
+ (0, react_2.useEffect)(function () {
239
+ handleLoadFolders();
240
+ // eslint-disable-next-line react-hooks/exhaustive-deps
241
+ }, [props.collectionId, additionalQueryKey]);
242
+ /**
243
+ * INFO: the whole folder in one unpaged read — folders sort first on the
244
+ * client, which only holds if nothing is left on a later page. A search
245
+ * reads across the whole drive instead of the open folder.
246
+ */
247
+ var handleFetchRows = function () { return __awaiter(void 0, void 0, void 0, function () {
248
+ var seq, view, res, err_1;
249
+ var _a;
250
+ return __generator(this, function (_b) {
251
+ switch (_b.label) {
252
+ case 0:
253
+ seq = ++fetchSeqRef.current;
254
+ view = viewRef.current;
255
+ setLoading(true);
256
+ _b.label = 1;
257
+ case 1:
258
+ _b.trys.push([1, 3, , 4]);
259
+ return [4 /*yield*/, readQuery.call({
260
+ type: "list",
261
+ query: __spreadArray(__spreadArray([], view.additionalQuery, true), [
262
+ view.searchQuery
263
+ ? (0, driveUtils_1.handleBuildDriveSearchQuery)(view.searchQuery)
264
+ : (0, driveUtils_1.handleBuildDriveFolderQuery)(view.folderId),
265
+ ], false),
266
+ stopLimit: true,
267
+ })];
268
+ case 2:
269
+ res = _b.sent();
270
+ if (seq !== fetchSeqRef.current)
271
+ return [2 /*return*/];
272
+ setRows((_a = res === null || res === void 0 ? void 0 : res.data) !== null && _a !== void 0 ? _a : []);
273
+ return [3 /*break*/, 4];
274
+ case 3:
275
+ err_1 = _b.sent();
276
+ if (seq !== fetchSeqRef.current)
277
+ return [2 /*return*/];
278
+ setRows([]);
279
+ return [3 /*break*/, 4];
280
+ case 4:
281
+ setLoading(false);
282
+ return [2 /*return*/];
283
+ }
284
+ });
285
+ }); };
286
+ var handleLoadFolders = function () { return __awaiter(void 0, void 0, void 0, function () {
287
+ var seq, res, err_2;
288
+ var _a;
289
+ return __generator(this, function (_b) {
290
+ switch (_b.label) {
291
+ case 0:
292
+ seq = ++folderSeqRef.current;
293
+ _b.label = 1;
294
+ case 1:
295
+ _b.trys.push([1, 3, , 4]);
296
+ return [4 /*yield*/, readQuery.call({
297
+ type: "list",
298
+ query: __spreadArray(__spreadArray([], viewRef.current.additionalQuery, true), [
299
+ { type: "=", queryId: driveUtils_1.DRIVE_COLUMNS.kind, value: driveUtils_1.DRIVE_FOLDER },
300
+ ], false),
301
+ stopLimit: true,
302
+ })];
303
+ case 2:
304
+ res = _b.sent();
305
+ if (seq !== folderSeqRef.current)
306
+ return [2 /*return*/];
307
+ setFolders((0, driveUtils_1.handleMapDriveFolders)((_a = res === null || res === void 0 ? void 0 : res.data) !== null && _a !== void 0 ? _a : []));
308
+ return [3 /*break*/, 4];
309
+ case 3:
310
+ err_2 = _b.sent();
311
+ return [3 /*break*/, 4];
312
+ case 4: return [2 /*return*/];
313
+ }
314
+ });
315
+ }); };
316
+ var handleChangeFolder = function (nextFolderId) {
317
+ if (props.onChangeFolder)
318
+ props.onChangeFolder(nextFolderId);
319
+ else
320
+ setInnerFolderId(nextFolderId);
321
+ };
322
+ var handleChangeLayout = function (next) {
323
+ setLayout(next);
324
+ try {
325
+ window.localStorage.setItem(LAYOUT_STORAGE_PREFIX + props.collectionId, next);
326
+ }
327
+ catch (err) { }
328
+ };
329
+ var handleAfterWrite = function () { return __awaiter(void 0, void 0, void 0, function () {
330
+ return __generator(this, function (_a) {
331
+ switch (_a.label) {
332
+ case 0: return [4 /*yield*/, Promise.all([handleFetchRows(), handleLoadFolders()])];
333
+ case 1:
334
+ _a.sent();
335
+ return [2 /*return*/];
336
+ }
337
+ });
338
+ }); };
339
+ var handleBuildPrefill = function () {
340
+ var _a;
341
+ var prefill = {};
342
+ ((_a = props.createPrefill) !== null && _a !== void 0 ? _a : []).forEach(function (eachPrefill) {
343
+ prefill[eachPrefill.key] = eachPrefill.value;
344
+ });
345
+ return prefill;
346
+ };
347
+ var handleResolveFile = function (row) {
348
+ var _a;
349
+ var value = row === null || row === void 0 ? void 0 : row[driveUtils_1.DRIVE_COLUMNS.file];
350
+ var entry = Array.isArray(value) ? value[0] : value;
351
+ if (!(entry === null || entry === void 0 ? void 0 : entry.token) || !(entry === null || entry === void 0 ? void 0 : entry.fileName))
352
+ return null;
353
+ return {
354
+ fileName: (0, blobModule_1.renderImageUrlFromPath)(devSettings.imageEndpoint, props.collectionId, (0, driveUtils_1.handleGetRowId)(row), entry.fileName, entry.token, devSettings.bucketName),
355
+ label: String((_a = row === null || row === void 0 ? void 0 : row[driveUtils_1.DRIVE_COLUMNS.name]) !== null && _a !== void 0 ? _a : "") || undefined,
356
+ token: entry.token,
357
+ mimeType: (row === null || row === void 0 ? void 0 : row[driveUtils_1.DRIVE_COLUMNS.mimeType]) ||
358
+ entry.mimeType ||
359
+ (0, filePreview_1.handleGuessMimeType)(entry.fileName),
360
+ };
361
+ };
362
+ var handleOpenRow = function (row) {
363
+ if ((0, driveUtils_1.handleIsDriveFolder)(row)) {
364
+ // INFO: a search reads the whole drive, so opening a folder from its
365
+ // results has to end the search or the folder shows the results again
366
+ setSearchText("");
367
+ setSearchQuery("");
368
+ handleChangeFolder((0, driveUtils_1.handleGetRowId)(row));
369
+ }
370
+ else
371
+ setPreviewRow(row);
372
+ };
373
+ var handleCloseModals = function () {
374
+ deleteSeqRef.current += 1;
375
+ setNewFolderOpen(false);
376
+ setFolderName("");
377
+ setRenameRow(null);
378
+ setRenameValue("");
379
+ setMoveRow(null);
380
+ setDeleteRow(null);
381
+ setDeleteIds(null);
382
+ setModalError("");
383
+ };
384
+ var handleCreateFolder = function () { return __awaiter(void 0, void 0, void 0, function () {
385
+ var name, err_3;
386
+ var _a;
387
+ return __generator(this, function (_b) {
388
+ switch (_b.label) {
389
+ case 0:
390
+ // INFO: Enter and a phone's Done still submit while the button is disabled
391
+ if (working)
392
+ return [2 /*return*/];
393
+ name = folderName.trim();
394
+ if (!name)
395
+ return [2 /*return*/, setModalError("Name the folder.")];
396
+ if ((0, driveUtils_1.handleIsDuplicateFolderName)(folders, folderId, name))
397
+ return [2 /*return*/, setModalError("A folder named \"".concat(name, "\" is already here."))];
398
+ setWorking(true);
399
+ _b.label = 1;
400
+ case 1:
401
+ _b.trys.push([1, 4, , 5]);
402
+ return [4 /*yield*/, writeQuery.call("create", {
403
+ data: __assign(__assign({}, handleBuildPrefill()), (_a = {}, _a[serverId] = (0, uuid_1.v4)(), _a[driveUtils_1.DRIVE_COLUMNS.name] = name, _a[driveUtils_1.DRIVE_COLUMNS.kind] = driveUtils_1.DRIVE_FOLDER, _a[driveUtils_1.DRIVE_COLUMNS.parentId] = folderId, _a)),
404
+ })];
405
+ case 2:
406
+ _b.sent();
407
+ handleCloseModals();
408
+ return [4 /*yield*/, handleAfterWrite()];
409
+ case 3:
410
+ _b.sent();
411
+ return [3 /*break*/, 5];
412
+ case 4:
413
+ err_3 = _b.sent();
414
+ setModalError("The folder could not be created.");
415
+ return [3 /*break*/, 5];
416
+ case 5:
417
+ setWorking(false);
418
+ return [2 /*return*/];
419
+ }
420
+ });
421
+ }); };
422
+ var handleOpenRename = function (row) {
423
+ var _a;
424
+ setRenameRow(row);
425
+ setRenameValue(String((_a = row === null || row === void 0 ? void 0 : row[driveUtils_1.DRIVE_COLUMNS.name]) !== null && _a !== void 0 ? _a : ""));
426
+ setModalError("");
427
+ };
428
+ var handleRename = function () { return __awaiter(void 0, void 0, void 0, function () {
429
+ var name, rowId, err_4;
430
+ var _a;
431
+ var _b;
432
+ return __generator(this, function (_c) {
433
+ switch (_c.label) {
434
+ case 0:
435
+ if (working)
436
+ return [2 /*return*/];
437
+ name = renameValue.trim();
438
+ if (!name)
439
+ return [2 /*return*/, setModalError("A name is required.")];
440
+ rowId = (0, driveUtils_1.handleGetRowId)(renameRow);
441
+ if ((0, driveUtils_1.handleIsDriveFolder)(renameRow) &&
442
+ (0, driveUtils_1.handleIsDuplicateFolderName)(folders, String((_b = renameRow === null || renameRow === void 0 ? void 0 : renameRow[driveUtils_1.DRIVE_COLUMNS.parentId]) !== null && _b !== void 0 ? _b : ""), name, rowId))
443
+ return [2 /*return*/, setModalError("A folder named \"".concat(name, "\" is already here."))];
444
+ setWorking(true);
445
+ _c.label = 1;
446
+ case 1:
447
+ _c.trys.push([1, 4, , 5]);
448
+ return [4 /*yield*/, writeQuery.call("update", {
449
+ id: rowId,
450
+ data: (_a = {}, _a[driveUtils_1.DRIVE_COLUMNS.name] = name, _a),
451
+ })];
452
+ case 2:
453
+ _c.sent();
454
+ handleCloseModals();
455
+ return [4 /*yield*/, handleAfterWrite()];
456
+ case 3:
457
+ _c.sent();
458
+ return [3 /*break*/, 5];
459
+ case 4:
460
+ err_4 = _c.sent();
461
+ setModalError("The name could not be saved.");
462
+ return [3 /*break*/, 5];
463
+ case 5:
464
+ setWorking(false);
465
+ return [2 /*return*/];
466
+ }
467
+ });
468
+ }); };
469
+ var handleMove = function (targetFolderId) { return __awaiter(void 0, void 0, void 0, function () {
470
+ var rowId, err_5;
471
+ var _a;
472
+ var _b;
473
+ return __generator(this, function (_c) {
474
+ switch (_c.label) {
475
+ case 0:
476
+ if (working)
477
+ return [2 /*return*/];
478
+ rowId = (0, driveUtils_1.handleGetRowId)(moveRow);
479
+ if ((0, driveUtils_1.handleIsDriveFolder)(moveRow) &&
480
+ (0, driveUtils_1.handleIsDuplicateFolderName)(folders, targetFolderId, String((_b = moveRow === null || moveRow === void 0 ? void 0 : moveRow[driveUtils_1.DRIVE_COLUMNS.name]) !== null && _b !== void 0 ? _b : ""), rowId))
481
+ return [2 /*return*/, setModalError("A folder with this name is already there.")];
482
+ setWorking(true);
483
+ _c.label = 1;
484
+ case 1:
485
+ _c.trys.push([1, 4, , 5]);
486
+ return [4 /*yield*/, writeQuery.call("update", {
487
+ id: rowId,
488
+ data: (_a = {}, _a[driveUtils_1.DRIVE_COLUMNS.parentId] = targetFolderId, _a),
489
+ })];
490
+ case 2:
491
+ _c.sent();
492
+ handleCloseModals();
493
+ return [4 /*yield*/, handleAfterWrite()];
494
+ case 3:
495
+ _c.sent();
496
+ return [3 /*break*/, 5];
497
+ case 4:
498
+ err_5 = _c.sent();
499
+ setModalError("The item could not be moved.");
500
+ return [3 /*break*/, 5];
501
+ case 5:
502
+ setWorking(false);
503
+ return [2 /*return*/];
504
+ }
505
+ });
506
+ }); };
507
+ /**
508
+ * Every row below a folder, read one level at a time.
509
+ *
510
+ * INFO: only the folder's own subtree is read — by parentId, one request per
511
+ * level, keeping just the id and folder columns — so counting a folder costs
512
+ * its size, not the size of the whole drive.
513
+ */
514
+ var handleReadSubtreeIds = function (rootId, query) { return __awaiter(void 0, void 0, void 0, function () {
515
+ var found, seen, frontier, _loop_1, depth;
516
+ var _a;
517
+ return __generator(this, function (_b) {
518
+ switch (_b.label) {
519
+ case 0:
520
+ found = [];
521
+ seen = new Set([rootId]);
522
+ frontier = [rootId];
523
+ _loop_1 = function (depth) {
524
+ var res, next;
525
+ return __generator(this, function (_c) {
526
+ switch (_c.label) {
527
+ case 0: return [4 /*yield*/, readQuery.call({
528
+ type: "list",
529
+ query: __spreadArray(__spreadArray([], query, true), [
530
+ { type: "in", queryId: driveUtils_1.DRIVE_COLUMNS.parentId, value: frontier },
531
+ ], false),
532
+ attributeIds: [serverId, driveUtils_1.DRIVE_COLUMNS.parentId],
533
+ stopLimit: true,
534
+ })];
535
+ case 1:
536
+ res = _c.sent();
537
+ next = [];
538
+ ((_a = res === null || res === void 0 ? void 0 : res.data) !== null && _a !== void 0 ? _a : []).forEach(function (eachRow) {
539
+ var id = (0, driveUtils_1.handleGetRowId)(eachRow);
540
+ if (!id || seen.has(id))
541
+ return;
542
+ seen.add(id);
543
+ found.push(id);
544
+ next.push(id);
545
+ });
546
+ frontier = next;
547
+ return [2 /*return*/];
548
+ }
549
+ });
550
+ };
551
+ depth = 0;
552
+ _b.label = 1;
553
+ case 1:
554
+ if (!(frontier.length > 0 && depth < MAX_DELETE_DEPTH)) return [3 /*break*/, 4];
555
+ return [5 /*yield**/, _loop_1(depth)];
556
+ case 2:
557
+ _b.sent();
558
+ _b.label = 3;
559
+ case 3:
560
+ depth += 1;
561
+ return [3 /*break*/, 1];
562
+ case 4: return [2 /*return*/, found];
563
+ }
564
+ });
565
+ }); };
566
+ /**
567
+ * INFO: a folder's contents are counted before anything is deleted, so the
568
+ * confirmation says what will go. A scoped member's folder can hold rows
569
+ * outside their slice; deleting the folder would strand those rows under a
570
+ * parent that no longer exists, where no view can reach them, so such a
571
+ * folder is refused. When the unscoped count cannot be read the delete is
572
+ * refused too, rather than guessed.
573
+ */
574
+ var handleOpenDelete = function (row) { return __awaiter(void 0, void 0, void 0, function () {
575
+ var seq, rowId, visibleIds, err_6, allIds, err_7;
576
+ return __generator(this, function (_a) {
577
+ switch (_a.label) {
578
+ case 0:
579
+ seq = ++deleteSeqRef.current;
580
+ setDeleteRow(row);
581
+ setModalError("");
582
+ if (!(0, driveUtils_1.handleIsDriveFolder)(row)) {
583
+ setDeleteIds([(0, driveUtils_1.handleGetRowId)(row)]);
584
+ return [2 /*return*/];
585
+ }
586
+ setDeleteIds(null);
587
+ rowId = (0, driveUtils_1.handleGetRowId)(row);
588
+ visibleIds = [];
589
+ _a.label = 1;
590
+ case 1:
591
+ _a.trys.push([1, 3, , 4]);
592
+ return [4 /*yield*/, handleReadSubtreeIds(rowId, additionalQuery)];
593
+ case 2:
594
+ visibleIds = _a.sent();
595
+ return [3 /*break*/, 4];
596
+ case 3:
597
+ err_6 = _a.sent();
598
+ if (seq === deleteSeqRef.current)
599
+ setModalError("The folder's contents could not be read.");
600
+ return [2 /*return*/];
601
+ case 4:
602
+ if (!(additionalQuery.length > 0)) return [3 /*break*/, 8];
603
+ _a.label = 5;
604
+ case 5:
605
+ _a.trys.push([5, 7, , 8]);
606
+ return [4 /*yield*/, handleReadSubtreeIds(rowId, [])];
607
+ case 6:
608
+ allIds = _a.sent();
609
+ if (allIds.length > visibleIds.length) {
610
+ if (seq === deleteSeqRef.current)
611
+ setModalError("This folder holds items you do not have access to, so it cannot be deleted.");
612
+ return [2 /*return*/];
613
+ }
614
+ return [3 /*break*/, 8];
615
+ case 7:
616
+ err_7 = _a.sent();
617
+ if (seq === deleteSeqRef.current)
618
+ setModalError("Whether this folder holds items you cannot see could not be checked, so it cannot be deleted.");
619
+ return [2 /*return*/];
620
+ case 8:
621
+ if (seq !== deleteSeqRef.current)
622
+ return [2 /*return*/];
623
+ setDeleteIds(__spreadArray([rowId], visibleIds, true));
624
+ return [2 /*return*/];
625
+ }
626
+ });
627
+ }); };
628
+ var handleDelete = function () { return __awaiter(void 0, void 0, void 0, function () {
629
+ var form, err_8;
630
+ return __generator(this, function (_a) {
631
+ switch (_a.label) {
632
+ case 0:
633
+ if (!(deleteIds === null || deleteIds === void 0 ? void 0 : deleteIds.length))
634
+ return [2 /*return*/];
635
+ setWorking(true);
636
+ _a.label = 1;
637
+ case 1:
638
+ _a.trys.push([1, 5, , 6]);
639
+ return [4 /*yield*/, writeQuery.call("batchDelete", { id: deleteIds })];
640
+ case 2:
641
+ _a.sent();
642
+ form = new FormData();
643
+ form.append("data[assetCollectionName]", props.collectionId);
644
+ form.append("data[assetParentId]", JSON.stringify(deleteIds));
645
+ if (devSettings.bucketName)
646
+ form.append("data[bucketName]", devSettings.bucketName);
647
+ return [4 /*yield*/, props.lib
648
+ .axios({
649
+ data: form,
650
+ method: "post",
651
+ url: "".concat(uploadEndpoint, "/batchDeleteDirectory"),
652
+ headers: { "Content-Type": "multipart/form-data" },
653
+ })
654
+ .catch(function () { })];
655
+ case 3:
656
+ _a.sent();
657
+ handleCloseModals();
658
+ return [4 /*yield*/, handleAfterWrite()];
659
+ case 4:
660
+ _a.sent();
661
+ return [3 /*break*/, 6];
662
+ case 5:
663
+ err_8 = _a.sent();
664
+ setModalError("The item could not be deleted.");
665
+ return [3 /*break*/, 6];
666
+ case 6:
667
+ setWorking(false);
668
+ return [2 /*return*/];
669
+ }
670
+ });
671
+ }); };
672
+ var handleUploadToStorage = function (rowId, storedName, file) { return __awaiter(void 0, void 0, void 0, function () {
673
+ var dataForm, res;
674
+ var _a;
675
+ return __generator(this, function (_b) {
676
+ switch (_b.label) {
677
+ case 0:
678
+ dataForm = new FormData();
679
+ dataForm.append("file", file);
680
+ dataForm.append("data[assetCollectionName]", props.collectionId);
681
+ dataForm.append("data[assetParentId]", rowId);
682
+ dataForm.append("data[assetFileName]", storedName);
683
+ if (devSettings.bucketName)
684
+ dataForm.append("data[bucketName]", devSettings.bucketName);
685
+ return [4 /*yield*/, props.lib.axios({
686
+ method: "post",
687
+ url: "".concat(uploadEndpoint, "/upload"),
688
+ data: dataForm,
689
+ headers: { "Content-Type": "multipart/form-data" },
690
+ })];
691
+ case 1:
692
+ res = _b.sent();
693
+ return [2 /*return*/, (_a = res === null || res === void 0 ? void 0 : res.data) === null || _a === void 0 ? void 0 : _a.data];
694
+ }
695
+ });
696
+ }); };
697
+ /**
698
+ * INFO: one row per file, each uploaded into its own row's storage folder
699
+ * first — a row is only created for a file that stored, and one failure does
700
+ * not stop the rest.
701
+ */
702
+ var handleUploadFiles = function (queued) { return __awaiter(void 0, void 0, void 0, function () {
703
+ var failed, _loop_2, _i, queued_1, eachFile;
704
+ return __generator(this, function (_a) {
705
+ switch (_a.label) {
706
+ case 0:
707
+ if (!queued.length)
708
+ return [2 /*return*/];
709
+ setWorking(true);
710
+ setModalError("");
711
+ failed = 0;
712
+ _loop_2 = function (eachFile) {
713
+ var file, rowId, storedName, token, err_9;
714
+ var _b;
715
+ return __generator(this, function (_c) {
716
+ switch (_c.label) {
717
+ case 0:
718
+ setUploadStatus(function (current) {
719
+ var _a;
720
+ return (__assign(__assign({}, current), (_a = {}, _a[eachFile.token] = "uploading", _a)));
721
+ });
722
+ _c.label = 1;
723
+ case 1:
724
+ _c.trys.push([1, 4, , 5]);
725
+ file = eachFile.fileName;
726
+ rowId = (0, uuid_1.v4)();
727
+ storedName = "".concat((0, uuid_1.v4)(), "-").concat(file.name);
728
+ return [4 /*yield*/, handleUploadToStorage(rowId, storedName, file)];
729
+ case 2:
730
+ token = _c.sent();
731
+ if (!token)
732
+ throw new Error("No token returned");
733
+ return [4 /*yield*/, writeQuery.call("create", {
734
+ data: __assign(__assign({}, handleBuildPrefill()), (_b = {}, _b[serverId] = rowId, _b[driveUtils_1.DRIVE_COLUMNS.name] = file.name, _b[driveUtils_1.DRIVE_COLUMNS.kind] = driveUtils_1.DRIVE_FILE, _b[driveUtils_1.DRIVE_COLUMNS.parentId] = folderId, _b[driveUtils_1.DRIVE_COLUMNS.file] = [{ token: token, fileName: storedName }], _b[driveUtils_1.DRIVE_COLUMNS.size] = file.size, _b[driveUtils_1.DRIVE_COLUMNS.mimeType] = file.type, _b)),
735
+ })];
736
+ case 3:
737
+ _c.sent();
738
+ setUploadStatus(function (current) {
739
+ var _a;
740
+ return (__assign(__assign({}, current), (_a = {}, _a[eachFile.token] = "done", _a)));
741
+ });
742
+ return [3 /*break*/, 5];
743
+ case 4:
744
+ err_9 = _c.sent();
745
+ failed += 1;
746
+ setUploadStatus(function (current) {
747
+ var _a;
748
+ return (__assign(__assign({}, current), (_a = {}, _a[eachFile.token] = "failed", _a)));
749
+ });
750
+ return [3 /*break*/, 5];
751
+ case 5: return [2 /*return*/];
752
+ }
753
+ });
754
+ };
755
+ _i = 0, queued_1 = queued;
756
+ _a.label = 1;
757
+ case 1:
758
+ if (!(_i < queued_1.length)) return [3 /*break*/, 4];
759
+ eachFile = queued_1[_i];
760
+ return [5 /*yield**/, _loop_2(eachFile)];
761
+ case 2:
762
+ _a.sent();
763
+ _a.label = 3;
764
+ case 3:
765
+ _i++;
766
+ return [3 /*break*/, 1];
767
+ case 4:
768
+ setWorking(false);
769
+ handleFetchRows();
770
+ if (failed === 0)
771
+ handleCloseUpload();
772
+ else
773
+ setModalError("".concat(failed, " file(s) could not be uploaded."));
774
+ return [2 /*return*/];
775
+ }
776
+ });
777
+ }); };
778
+ var handleUploadAll = function () {
779
+ var _a;
780
+ return handleUploadFiles(((_a = uploadFiles.upload) !== null && _a !== void 0 ? _a : []).filter(function (eachFile) {
781
+ return uploadStatus[eachFile.token] !== "done" &&
782
+ eachFile.fileName instanceof File;
783
+ }));
784
+ };
785
+ /**
786
+ * INFO: a drop starts uploading at once — dragging files onto a folder is
787
+ * already the decision, so a confirm step would only be a second click. The
788
+ * dialog opens to show each file's progress and closes when all have stored.
789
+ */
790
+ var handleDropFiles = function (files) {
791
+ if (working || !files.length)
792
+ return;
793
+ var queued = files.map(function (eachFile) { return ({
794
+ token: (0, uuid_1.v4)(),
795
+ fileName: eachFile,
796
+ }); });
797
+ setUploadFiles({ data: [], upload: queued, remove: [] });
798
+ setUploadStatus(Object.fromEntries(queued.map(function (eachFile) { return [eachFile.token, "waiting"]; })));
799
+ setModalError("");
800
+ setUploadOpen(true);
801
+ handleUploadFiles(queued);
802
+ };
803
+ var handleOpenUpload = function () {
804
+ setUploadFiles({ data: [], upload: [], remove: [] });
805
+ setUploadStatus({});
806
+ setModalError("");
807
+ setUploadOpen(true);
808
+ };
809
+ var handleCloseUpload = function () {
810
+ setUploadOpen(false);
811
+ setUploadFiles({ data: [], upload: [], remove: [] });
812
+ setUploadStatus({});
813
+ setModalError("");
814
+ };
815
+ // INFO: only a drag carrying files, only for someone who may create, and not
816
+ // while the upload dialog is open — it has its own drop zone
817
+ var handleIsFileDrag = function (event) {
818
+ var _a, _b;
819
+ return canCreate &&
820
+ !uploadOpen &&
821
+ Array.from((_b = (_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.types) !== null && _b !== void 0 ? _b : []).includes("Files");
822
+ };
823
+ var handleFormatRowPath = function (row) {
824
+ var _a;
825
+ if (!searching)
826
+ return "";
827
+ var parentId = String((_a = row === null || row === void 0 ? void 0 : row[driveUtils_1.DRIVE_COLUMNS.parentId]) !== null && _a !== void 0 ? _a : "");
828
+ var rowPath = (0, driveUtils_1.handleFormatFolderPath)((0, driveUtils_1.handleBuildFolderPath)(folders, parentId));
829
+ return rowPath ? "In ".concat(rowPath) : "At the top level";
830
+ };
831
+ var renderDropOverlay = function () {
832
+ if (!dragging)
833
+ return null;
834
+ var target = path.length ? path[path.length - 1].name : "Drive";
835
+ return (react_2.default.createElement("div", { "data-testid": "drive-drop-overlay", className: "pointer-events-none absolute inset-0 z-10 flex flex-col items-center justify-center gap-y-2 rounded-lg border-2 border-dashed border-primary-400 bg-primary-50/90" },
836
+ react_2.default.createElement(HeroIcons.ArrowUpTrayIcon, { className: "h-8 w-8 text-primary-500" }),
837
+ react_2.default.createElement("p", { className: "text-sm font-medium text-primary-700" },
838
+ "Drop to upload to ",
839
+ target)));
840
+ };
841
+ var renderMenu = function (row) {
842
+ var items = __spreadArray(__spreadArray([], (canUpdate
843
+ ? [
844
+ { text: "Rename", onClick: function () { return handleOpenRename(row); } },
845
+ {
846
+ text: "Move to…",
847
+ onClick: function () {
848
+ setMoveRow(row);
849
+ setModalError("");
850
+ },
851
+ },
852
+ ]
853
+ : []), true), (canDelete
854
+ ? [{ text: "Delete", onClick: function () { return handleOpenDelete(row); } }]
855
+ : []), true);
856
+ if (!items.length)
857
+ return null;
858
+ return (react_2.default.createElement(DriveItemMenu, { darkMode: dark, testId: "drive-menu-".concat((0, driveUtils_1.handleGetRowId)(row)), items: items }));
859
+ };
860
+ var renderIconLabel = function (label) { return (react_2.default.createElement("span", { className: "sr-only" }, label)); };
861
+ // INFO: the same row DataTemplate draws above a list — title on the left, a
862
+ // w-64 labelled search and icon actions on the right — so a drive page and a
863
+ // table page line up
864
+ var renderHeader = function () { return (react_2.default.createElement("div", { className: "flex py-2 w-full flex-row flex-wrap sm:flex-nowrap items-end justify-between sm:justify-start" },
865
+ (props.title || props.description) && (react_2.default.createElement("div", { className: "flex flex-col" },
866
+ props.title && (react_2.default.createElement("div", { className: "flex items-center gap-2" },
867
+ react_2.default.createElement("p", { className: "text-xl font-medium ".concat(props.darkMode ? "text-white" : "") }, props.title))),
868
+ props.description && (react_2.default.createElement("span", { className: "text-sm text-gray-500" }, props.description)))),
869
+ react_2.default.createElement("div", { className: "hidden sm:block grow" }),
870
+ react_2.default.createElement("div", { className: "ml-auto flex flex-row items-end" },
871
+ react_2.default.createElement("div", { className: "flex-1 sm:flex-none sm:w-64 min-w-0 mr-3" },
872
+ react_2.default.createElement(textInput_1.TextInput, { containerClassName: "w-full", label: "Search", placeholder: "Search", id: "drive-search", type: "text", value: searchText, onChange: function (event) { return setSearchText(event.target.value); }, darkMode: props.darkMode, showMagnifyIcon: true })),
873
+ react_2.default.createElement("div", { className: "flex flex-row items-end gap-x-2" },
874
+ react_2.default.createElement(iconButton_1.IconButton, { testId: "drive-layout", icon: layout === "grid" ? (react_2.default.createElement(react_2.default.Fragment, null,
875
+ react_2.default.createElement(HeroIcons.ListBulletIcon, { className: "h-7 w-7 text-primary-500" }),
876
+ renderIconLabel("List view"))) : (react_2.default.createElement(react_2.default.Fragment, null,
877
+ react_2.default.createElement(HeroIcons.Squares2X2Icon, { className: "h-7 w-7 text-primary-500" }),
878
+ renderIconLabel("Grid view"))), bgColor: props.darkMode ? "bg-white/10" : "bg-white", onClick: function () {
879
+ return handleChangeLayout(layout === "grid" ? "list" : "grid");
880
+ } }),
881
+ canCreate && (react_2.default.createElement(react_2.default.Fragment, null,
882
+ react_2.default.createElement(iconButton_1.IconButton, { testId: "drive-new-folder", icon: react_2.default.createElement(react_2.default.Fragment, null,
883
+ react_2.default.createElement(HeroIcons.FolderPlusIcon, { className: "h-7 w-7 text-primary-500" }),
884
+ renderIconLabel("New folder")), bgColor: props.darkMode ? "bg-white/10" : "bg-white", onClick: function () {
885
+ setFolderName("");
886
+ setModalError("");
887
+ setNewFolderOpen(true);
888
+ } }),
889
+ react_2.default.createElement(iconButton_1.IconButton, { testId: "drive-upload-open", icon: react_2.default.createElement(react_2.default.Fragment, null,
890
+ react_2.default.createElement(HeroIcons.ArrowUpTrayIcon, { className: "h-7 w-7 text-white" }),
891
+ renderIconLabel("Upload")), background: "bg-primary-500 hover:bg-primary-600", onClick: handleOpenUpload }))))))); };
892
+ var renderPath = function () {
893
+ var currentClass = props.darkMode ? "text-white" : "text-gray-900";
894
+ return (react_2.default.createElement("nav", { "aria-label": "Drive path", className: "flex min-w-0 flex-row flex-wrap items-center gap-x-1 py-2 text-sm" },
895
+ react_2.default.createElement("button", { type: "button", className: "font-medium ".concat(folderId && !searching
896
+ ? "text-primary-600 hover:underline"
897
+ : currentClass), onClick: function () {
898
+ setSearchText("");
899
+ setSearchQuery("");
900
+ handleChangeFolder("");
901
+ } }, "Drive"),
902
+ !searching &&
903
+ path.map(function (eachFolder, eachIndex) { return (react_2.default.createElement(react_2.default.Fragment, { key: eachFolder.id },
904
+ react_2.default.createElement(HeroIcons.ChevronRightIcon, { className: "h-4 w-4 shrink-0 text-gray-400" }),
905
+ react_2.default.createElement("button", { type: "button", className: "truncate font-medium ".concat(eachIndex === path.length - 1
906
+ ? currentClass
907
+ : "text-primary-600 hover:underline"), onClick: function () { return handleChangeFolder(eachFolder.id); } }, eachFolder.name))); }),
908
+ searching && (react_2.default.createElement("span", { className: "ml-2 text-gray-500" }, "\u00B7 Search results"))));
909
+ };
910
+ var renderFolderIcon = function (sizeClass) { return (react_2.default.createElement(HeroIcons.FolderIcon, { className: "".concat(sizeClass, " shrink-0 text-primary-400") })); };
911
+ // INFO: folders as compact rows above the file tiles, the way a file browser
912
+ // lays them out — a folder has no picture, so a tile-sized card for it is
913
+ // mostly empty space
914
+ var renderGrid = function () {
915
+ var folderRows = sortedRows.filter(function (eachRow) {
916
+ return (0, driveUtils_1.handleIsDriveFolder)(eachRow);
917
+ });
918
+ var fileRows = sortedRows.filter(function (eachRow) { return !(0, driveUtils_1.handleIsDriveFolder)(eachRow); });
919
+ return (react_2.default.createElement("div", { className: "flex flex-col gap-y-4" },
920
+ folderRows.length > 0 && (react_2.default.createElement("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4" }, folderRows.map(function (eachRow) {
921
+ var _a;
922
+ var rowId = (0, driveUtils_1.handleGetRowId)(eachRow);
923
+ return (react_2.default.createElement("div", { key: rowId, "data-testid": "drive-item-".concat(rowId), className: "flex min-w-0 flex-row items-center gap-x-2 rounded-lg border pr-1 ".concat(surface) },
924
+ react_2.default.createElement("button", { type: "button", onClick: function () { return handleOpenRow(eachRow); }, className: "flex min-w-0 grow flex-col items-start py-2 pl-3 text-left" },
925
+ react_2.default.createElement("span", { className: "flex w-full min-w-0 flex-row items-center gap-x-2" },
926
+ renderFolderIcon("h-5 w-5"),
927
+ react_2.default.createElement("span", { className: "truncate text-sm font-medium ".concat(textStrong) }, String((_a = eachRow === null || eachRow === void 0 ? void 0 : eachRow[driveUtils_1.DRIVE_COLUMNS.name]) !== null && _a !== void 0 ? _a : ""))),
928
+ searching && (react_2.default.createElement("span", { className: "w-full truncate pl-7 text-xs text-gray-500" }, handleFormatRowPath(eachRow)))),
929
+ renderMenu(eachRow)));
930
+ }))),
931
+ fileRows.length > 0 && (react_2.default.createElement("div", { className: "grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4" }, fileRows.map(function (eachRow) {
932
+ var _a;
933
+ var rowId = (0, driveUtils_1.handleGetRowId)(eachRow);
934
+ var file = handleResolveFile(eachRow);
935
+ var menu = renderMenu(eachRow);
936
+ return (react_2.default.createElement("div", { key: rowId, "data-testid": "drive-item-".concat(rowId), className: "relative min-w-0" },
937
+ file ? (react_2.default.createElement(filePreview_1.FilePreview, { variant: "tile", darkMode: dark, file: file, onClick: function () { return handleOpenRow(eachRow); } })) : (react_2.default.createElement("button", { type: "button", onClick: function () { return handleOpenRow(eachRow); }, className: "flex aspect-video w-full flex-col items-center justify-center gap-y-2 rounded-lg border ".concat(surface) },
938
+ react_2.default.createElement(HeroIcons.DocumentIcon, { className: "h-8 w-8 text-gray-400" }),
939
+ react_2.default.createElement("span", { className: "w-full truncate px-2 text-sm ".concat(textStrong) }, String((_a = eachRow === null || eachRow === void 0 ? void 0 : eachRow[driveUtils_1.DRIVE_COLUMNS.name]) !== null && _a !== void 0 ? _a : "")))),
940
+ searching && (react_2.default.createElement("p", { className: "mt-1 truncate text-xs text-gray-500" }, handleFormatRowPath(eachRow))),
941
+ menu && (react_2.default.createElement("div", { className: "absolute right-1 top-1 rounded-md shadow-sm ".concat(dark ? "bg-gray-900/90" : "bg-white/90") }, menu))));
942
+ })))));
943
+ };
944
+ var renderList = function () { return (react_2.default.createElement(newList_1.NewList, { list: sortedRows.map(function (eachRow) {
945
+ var _a;
946
+ var isFolder = (0, driveUtils_1.handleIsDriveFolder)(eachRow);
947
+ var description = [
948
+ handleFormatRowPath(eachRow),
949
+ isFolder ? "" : (0, driveUtils_1.handleFormatFileSize)(eachRow === null || eachRow === void 0 ? void 0 : eachRow[driveUtils_1.DRIVE_COLUMNS.size]),
950
+ ]
951
+ .filter(function (eachPart) { return !!eachPart; })
952
+ .join(" · ");
953
+ var menu = renderMenu(eachRow);
954
+ return __assign(__assign({ id: (0, driveUtils_1.handleGetRowId)(eachRow), title: String((_a = eachRow === null || eachRow === void 0 ? void 0 : eachRow[driveUtils_1.DRIVE_COLUMNS.name]) !== null && _a !== void 0 ? _a : ""), icon: isFolder ? (renderFolderIcon("h-6 w-6")) : (react_2.default.createElement(HeroIcons.DocumentIcon, { className: "h-6 w-6 text-gray-400" })) }, (description ? { description: description } : {})), (menu ? { action: menu } : {}));
955
+ }), hidePagination: true, onClickItem: function (item) {
956
+ return handleOpenRow(sortedRows.find(function (eachRow) { return (0, driveUtils_1.handleGetRowId)(eachRow) === item.id; }));
957
+ }, darkMode: props.darkMode })); };
958
+ var renderContent = function () {
959
+ if (loading)
960
+ return (react_2.default.createElement("div", { className: "flex flex-col items-center py-10" },
961
+ react_2.default.createElement(loading_1.Loading, null)));
962
+ if (!sortedRows.length)
963
+ return (react_2.default.createElement(emptyState_1.EmptyState, { darkMode: dark, text: searching ? "Nothing matches the search" : "This folder is empty", icon: renderFolderIcon("h-10 w-10") }));
964
+ return layout === "grid" ? renderGrid() : renderList();
965
+ };
966
+ var renderNameForm = function (value, onChange, onSubmit, submitText, placeholder) { return (
967
+ // INFO: a form so Enter — and a phone keyboard's Done, which submits rather
968
+ // than sending a keydown — commits; stopPropagation keeps the submit from
969
+ // reaching a form this view sits inside
970
+ react_2.default.createElement("form", { onSubmit: function (event) {
971
+ event.preventDefault();
972
+ event.stopPropagation();
973
+ onSubmit();
974
+ }, className: "flex flex-col gap-y-3" },
975
+ react_2.default.createElement(textInput_1.TextInput, { id: "drive-name", darkMode: dark, type: "text", label: "", placeholder: placeholder, value: value, autoFocus: true, onChange: function (event) { return onChange(event.target.value); } }),
976
+ modalError && react_2.default.createElement("p", { className: "text-sm text-red-600" }, modalError),
977
+ react_2.default.createElement("div", { className: "flex flex-row justify-end gap-x-3" },
978
+ react_2.default.createElement(button_1.Button, { text: "Cancel", type: "light", size: "small", onClick: handleCloseModals }),
979
+ react_2.default.createElement(button_1.Button, { text: working ? "Saving…" : submitText, type: "normal", size: "small", disable: working, onClick: onSubmit })))); };
980
+ var renderMoveContent = function () {
981
+ var _a;
982
+ if (!moveRow)
983
+ return null;
984
+ var rowId = (0, driveUtils_1.handleGetRowId)(moveRow);
985
+ var currentParent = String((_a = moveRow === null || moveRow === void 0 ? void 0 : moveRow[driveUtils_1.DRIVE_COLUMNS.parentId]) !== null && _a !== void 0 ? _a : "");
986
+ var targets = (0, driveUtils_1.handleGetMoveTargets)(folders, rowId, (0, driveUtils_1.handleIsDriveFolder)(moveRow))
987
+ .map(function (eachFolder) { return ({
988
+ folder: eachFolder,
989
+ label: (0, driveUtils_1.handleFormatFolderPath)((0, driveUtils_1.handleBuildFolderPath)(folders, eachFolder.id)),
990
+ }); })
991
+ .sort(function (a, b) {
992
+ return a.label.localeCompare(b.label, undefined, {
993
+ numeric: true,
994
+ sensitivity: "base",
995
+ });
996
+ });
997
+ var options = __spreadArray([
998
+ { id: "", label: "Drive (top level)" }
999
+ ], targets.map(function (eachTarget) { return ({
1000
+ id: eachTarget.folder.id,
1001
+ label: eachTarget.label,
1002
+ }); }), true).filter(function (eachOption) { return eachOption.id !== currentParent; });
1003
+ return (react_2.default.createElement("div", { className: "flex flex-col gap-y-3" },
1004
+ modalError && react_2.default.createElement("p", { className: "text-sm text-red-600" }, modalError),
1005
+ react_2.default.createElement("div", { className: "max-h-80 overflow-y-auto" },
1006
+ react_2.default.createElement(newList_1.NewList, { darkMode: dark, list: options.map(function (eachOption) { return ({
1007
+ id: eachOption.id || "__root__",
1008
+ title: eachOption.label,
1009
+ icon: renderFolderIcon("h-5 w-5"),
1010
+ }); }), hidePagination: true, onClickItem: function (item) {
1011
+ return !working && handleMove(item.id === "__root__" ? "" : item.id);
1012
+ } }))));
1013
+ };
1014
+ var renderDeleteContent = function () {
1015
+ var _a, _b;
1016
+ if (!deleteRow)
1017
+ return null;
1018
+ var isFolder = (0, driveUtils_1.handleIsDriveFolder)(deleteRow);
1019
+ var name = String((_a = deleteRow === null || deleteRow === void 0 ? void 0 : deleteRow[driveUtils_1.DRIVE_COLUMNS.name]) !== null && _a !== void 0 ? _a : "");
1020
+ var inside = ((_b = deleteIds === null || deleteIds === void 0 ? void 0 : deleteIds.length) !== null && _b !== void 0 ? _b : 1) - 1;
1021
+ return (react_2.default.createElement("div", { className: "flex flex-col gap-y-3" },
1022
+ react_2.default.createElement("p", { className: "text-sm ".concat(textBody) }, !isFolder
1023
+ ? "Delete \"".concat(name, "\"? This cannot be undone.")
1024
+ : deleteIds === null
1025
+ ? modalError
1026
+ ? "\"".concat(name, "\" was not deleted.")
1027
+ : "Counting what is inside…"
1028
+ : inside > 0
1029
+ ? "Delete \"".concat(name, "\" and the ").concat(inside, " item(s) inside it? This cannot be undone.")
1030
+ : "Delete the empty folder \"".concat(name, "\"?")),
1031
+ modalError && react_2.default.createElement("p", { className: "text-sm text-red-600" }, modalError),
1032
+ react_2.default.createElement("div", { className: "flex flex-row justify-end gap-x-3" },
1033
+ react_2.default.createElement(button_1.Button, { text: "Cancel", type: "light", size: "small", onClick: handleCloseModals }),
1034
+ react_2.default.createElement(button_1.Button, { text: working ? "Deleting…" : "Delete", type: "danger", size: "small", disable: working || deleteIds === null, onClick: handleDelete }))));
1035
+ };
1036
+ var renderUploadContent = function () {
1037
+ var _a;
1038
+ var queued = (_a = uploadFiles.upload) !== null && _a !== void 0 ? _a : [];
1039
+ return (react_2.default.createElement("div", { className: "flex flex-col gap-y-3" },
1040
+ react_2.default.createElement(uploadInput_1.UploadInput, { darkMode: dark, lib: props.lib, id: "drive-upload", label: "", type: "multi", files: uploadFiles, disabled: working, onChange: function (_a) {
1041
+ var value = _a.value, action = _a.action;
1042
+ if (action === "upload")
1043
+ setUploadFiles(function (current) {
1044
+ var _a;
1045
+ return (__assign(__assign({}, current), { upload: __spreadArray(__spreadArray([], ((_a = current.upload) !== null && _a !== void 0 ? _a : []), true), value, true) }));
1046
+ });
1047
+ if (action === "remove")
1048
+ setUploadFiles(function (current) {
1049
+ var _a;
1050
+ return (__assign(__assign({}, current), { upload: ((_a = current.upload) !== null && _a !== void 0 ? _a : []).filter(function (eachFile) { return eachFile.token !== value.token; }) }));
1051
+ });
1052
+ } }),
1053
+ queued.length > 0 && Object.keys(uploadStatus).length > 0 && (react_2.default.createElement("ul", { className: "flex flex-col gap-y-1 text-sm" }, queued.map(function (eachFile) {
1054
+ var _a, _b;
1055
+ return (react_2.default.createElement("li", { key: eachFile.token, className: "flex flex-row gap-x-2" },
1056
+ react_2.default.createElement("span", { className: "truncate ".concat(textBody) }, (_b = (_a = eachFile.fileName) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : ""),
1057
+ react_2.default.createElement("span", { className: "grow" }),
1058
+ react_2.default.createElement("span", { className: uploadStatus[eachFile.token] === "failed"
1059
+ ? "text-red-600"
1060
+ : "text-gray-500" }, uploadStatus[eachFile.token] === "done"
1061
+ ? "Uploaded"
1062
+ : uploadStatus[eachFile.token] === "uploading"
1063
+ ? "Uploading…"
1064
+ : uploadStatus[eachFile.token] === "failed"
1065
+ ? "Failed"
1066
+ : "Waiting")));
1067
+ }))),
1068
+ modalError && react_2.default.createElement("p", { className: "text-sm text-red-600" }, modalError),
1069
+ react_2.default.createElement("div", { className: "flex flex-row justify-end gap-x-3" },
1070
+ react_2.default.createElement(button_1.Button, { text: "Close", type: "light", size: "small", disable: working, onClick: handleCloseUpload }),
1071
+ react_2.default.createElement(button_1.Button, { text: working ? "Uploading…" : "Upload ".concat(queued.length || "").trim(), type: "normal", size: "small", disable: working || queued.length === 0, onClick: handleUploadAll }))));
1072
+ };
1073
+ var renderPreviewContent = function () {
1074
+ var _a, _b, _c;
1075
+ var file = previewRow ? handleResolveFile(previewRow) : null;
1076
+ if (!file)
1077
+ return null;
1078
+ var kind = (0, filePreview_1.handleResolveFileKind)(file);
1079
+ var details = [
1080
+ (0, driveUtils_1.handleFormatFileSize)(previewRow === null || previewRow === void 0 ? void 0 : previewRow[driveUtils_1.DRIVE_COLUMNS.size]),
1081
+ (_a = file.mimeType) !== null && _a !== void 0 ? _a : "",
1082
+ ]
1083
+ .filter(function (eachPart) { return !!eachPart; })
1084
+ .join(" · ");
1085
+ return (react_2.default.createElement("div", { className: "flex flex-col gap-y-3" },
1086
+ kind === "image" && (react_2.default.createElement("img", { src: file.fileName, alt: (_b = file.label) !== null && _b !== void 0 ? _b : "", className: "max-h-[70vh] w-full object-contain" })),
1087
+ kind === "pdf" && (react_2.default.createElement("iframe", { title: (_c = file.label) !== null && _c !== void 0 ? _c : "Preview", src: file.fileName, className: "h-[70vh] w-full" })),
1088
+ kind === "video" && (react_2.default.createElement("video", { src: file.fileName, controls: true, className: "max-h-[70vh] w-full" })),
1089
+ kind !== "image" && kind !== "pdf" && kind !== "video" && (react_2.default.createElement("p", { className: "text-sm ".concat(textBody) }, "This file has no preview.")),
1090
+ react_2.default.createElement("div", { className: "flex flex-row items-center justify-end gap-x-3" },
1091
+ details && (react_2.default.createElement("p", { className: "mr-auto text-sm text-gray-500" }, details)),
1092
+ react_2.default.createElement("a", { href: file.fileName, target: "_blank", rel: "noopener noreferrer", className: "rounded-md bg-primary-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-primary-700" }, "Download"))));
1093
+ };
1094
+ return (react_2.default.createElement("div", {
1095
+ // INFO: a minimum height so an empty or near-empty folder is still a
1096
+ // drop target worth aiming at
1097
+ className: "relative flex min-h-[240px] flex-col", "data-testid": "drive-view", onDragEnter: function (event) {
1098
+ if (!handleIsFileDrag(event))
1099
+ return;
1100
+ event.preventDefault();
1101
+ dragDepthRef.current += 1;
1102
+ setDragging(true);
1103
+ }, onDragOver: function (event) {
1104
+ if (handleIsFileDrag(event))
1105
+ event.preventDefault();
1106
+ }, onDragLeave: function (event) {
1107
+ if (!handleIsFileDrag(event))
1108
+ return;
1109
+ dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
1110
+ if (dragDepthRef.current === 0)
1111
+ setDragging(false);
1112
+ }, onDrop: function (event) {
1113
+ var _a, _b;
1114
+ dragDepthRef.current = 0;
1115
+ setDragging(false);
1116
+ if (!handleIsFileDrag(event) || !((_b = (_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) === null || _b === void 0 ? void 0 : _b.length))
1117
+ return;
1118
+ event.preventDefault();
1119
+ handleDropFiles(Array.from(event.dataTransfer.files));
1120
+ } },
1121
+ renderHeader(),
1122
+ renderPath(),
1123
+ react_2.default.createElement("div", { className: "mt-2" }, renderContent()),
1124
+ renderDropOverlay(),
1125
+ react_2.default.createElement(modal_1.Modal, { darkMode: dark, open: newFolderOpen, title: "New folder", onClose: handleCloseModals, renderContent: function () {
1126
+ return renderNameForm(folderName, setFolderName, handleCreateFolder, "Create", "Folder name");
1127
+ } }),
1128
+ react_2.default.createElement(modal_1.Modal, { darkMode: dark, open: !!renameRow, title: "Rename", onClose: handleCloseModals, renderContent: function () {
1129
+ return renderNameForm(renameValue, setRenameValue, handleRename, "Save", "Name");
1130
+ } }),
1131
+ react_2.default.createElement(modal_1.Modal, { darkMode: dark, open: !!moveRow, title: "Move \"".concat(String((_f = moveRow === null || moveRow === void 0 ? void 0 : moveRow[driveUtils_1.DRIVE_COLUMNS.name]) !== null && _f !== void 0 ? _f : ""), "\" to"), onClose: handleCloseModals, renderContent: renderMoveContent }),
1132
+ react_2.default.createElement(modal_1.Modal, { darkMode: dark, open: !!deleteRow, title: "Delete", onClose: handleCloseModals, renderContent: renderDeleteContent }),
1133
+ react_2.default.createElement(modal_1.Modal, { darkMode: dark, open: uploadOpen, title: "Upload files", maxWidth: "2xl",
1134
+ // INFO: no closing mid-upload — the rows still being written belong to
1135
+ // the folder the upload started in
1136
+ onClose: working ? undefined : handleCloseUpload, renderContent: renderUploadContent }),
1137
+ react_2.default.createElement(modal_1.Modal, { darkMode: dark, open: !!previewRow, title: String((_g = previewRow === null || previewRow === void 0 ? void 0 : previewRow[driveUtils_1.DRIVE_COLUMNS.name]) !== null && _g !== void 0 ? _g : ""), maxWidth: "4xl", onClose: function () { return setPreviewRow(null); }, renderContent: renderPreviewContent })));
1138
+ };
1139
+ exports.DriveTemplate = DriveTemplate;
1140
+ /**
1141
+ * The per-item action menu.
1142
+ *
1143
+ * INFO: portalled — the list layout sits in NewList's rounded card, which clips
1144
+ * overflow, so an in-place panel was cut off at the row's edge. Button's own
1145
+ * popUpMenuItems has no portal and a full-size trigger, hence a small one here.
1146
+ */
1147
+ var DriveItemMenu = function (props) {
1148
+ var popUpRef = (0, react_2.useRef)(null);
1149
+ return (react_2.default.createElement("div", { "data-testid": props.testId, onClick: function (event) { return event.stopPropagation(); } },
1150
+ react_2.default.createElement(popUp_1.PopUp, { ref: popUpRef, portal: true, anchor: "right", direction: "bottom", lib: { Transition: react_1.Transition }, ctaComp: react_2.default.createElement("button", { type: "button", "aria-label": "Actions", className: "rounded-md p-1.5 ".concat(props.darkMode
1151
+ ? "text-gray-300 hover:bg-white/10 hover:text-white"
1152
+ : "text-gray-500 hover:bg-gray-100 hover:text-gray-700") },
1153
+ react_2.default.createElement(HeroIcons.EllipsisVerticalIcon, { className: "h-4 w-4" })), renderComp: react_2.default.createElement("div", { className: "flex min-w-[140px] flex-col divide-y overflow-hidden rounded-md shadow-md ".concat(props.darkMode ? "divide-white/10 bg-gray-900" : "bg-white") }, props.items.map(function (eachItem) { return (react_2.default.createElement("button", { key: eachItem.text, type: "button", className: "px-4 py-2 text-left text-sm ".concat(props.darkMode ? "hover:bg-white/10" : "hover:bg-gray-100", " ").concat(eachItem.text === "Delete"
1154
+ ? "text-red-600"
1155
+ : props.darkMode
1156
+ ? "text-white"
1157
+ : "text-gray-900"), onClick: function () {
1158
+ var _a;
1159
+ (_a = popUpRef.current) === null || _a === void 0 ? void 0 : _a.handleOnClose();
1160
+ eachItem.onClick();
1161
+ } }, eachItem.text)); })) })));
1162
+ };
1163
+ //# sourceMappingURL=index.js.map