@noya-app/noya-file-explorer 0.0.15 → 0.0.17

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,329 @@
1
+ import {
2
+ createResourceTree,
3
+ Resource,
4
+ ResourceMap,
5
+ ResourceTree,
6
+ rootResource,
7
+ } from "@noya-app/noya-schemas";
8
+ import { path } from "imfs";
9
+ import { ancestorPaths } from "tree-visit";
10
+
11
+ export type FileKindFilter = "assets" | "directories" | "resources" | "all";
12
+ export type ExpandedMap = Record<string, boolean | undefined>;
13
+
14
+ export type GetVisibleItemsOptions = {
15
+ expandedMap: ExpandedMap;
16
+ fileKindFilter: FileKindFilter;
17
+ rootItemId: string;
18
+ tree: ResourceTree;
19
+ showAllDescendants: boolean;
20
+ showRootItem: boolean;
21
+ };
22
+
23
+ export const getVisibleItems = ({
24
+ expandedMap,
25
+ fileKindFilter,
26
+ rootItemId,
27
+ tree,
28
+ showAllDescendants,
29
+ showRootItem,
30
+ }: GetVisibleItemsOptions) => {
31
+ const filteredItems: Resource[] = [];
32
+ const relativeRootItem =
33
+ tree.find(rootResource, (item) => item.id === rootItemId) ?? rootResource;
34
+
35
+ tree.visit(relativeRootItem, (item) => {
36
+ if (relativeRootItem.id === item.id) {
37
+ if (showRootItem) {
38
+ filteredItems.push(item);
39
+ }
40
+ return;
41
+ }
42
+ if (item.type === "file" && fileKindFilter === "all") {
43
+ filteredItems.push(item);
44
+ }
45
+ if (
46
+ item.type === "asset" &&
47
+ (fileKindFilter === "assets" || fileKindFilter === "all")
48
+ ) {
49
+ filteredItems.push(item);
50
+ }
51
+ if (
52
+ item.type === "directory" &&
53
+ (fileKindFilter === "directories" || fileKindFilter === "all")
54
+ ) {
55
+ filteredItems.push(item);
56
+ }
57
+ if (
58
+ item.type === "resource" &&
59
+ (fileKindFilter === "resources" || fileKindFilter === "all")
60
+ ) {
61
+ filteredItems.push(item);
62
+ }
63
+ if (!expandedMap[item.id] || !showAllDescendants) return "skip";
64
+ });
65
+ return filteredItems;
66
+ };
67
+
68
+ /**
69
+ * Validates if a basename follows typical file naming conventions
70
+ * Disallows:
71
+ * - Path separators (/ and \)
72
+ * - Characters typically invalid in filesystems (<, >, :, ", |, ?, *)
73
+ * - Empty names
74
+ */
75
+ export const basenameValidator = (basename: string): boolean => {
76
+ // Check for empty string
77
+ if (!basename || basename.trim() === "") return false;
78
+
79
+ // Simple regex matching common invalid filesystem characters
80
+ const invalidCharsRegex = /[/\\<>:"|?*]/;
81
+ return !invalidCharsRegex.test(basename);
82
+ };
83
+
84
+ export const validateResourceRename = ({
85
+ basename,
86
+ selectedItemPath,
87
+ media,
88
+ }: {
89
+ basename: string;
90
+ selectedItemPath: string;
91
+ media: ResourceMap;
92
+ }): boolean => {
93
+ if (!basenameValidator(basename)) return false;
94
+ const newItemPath = path.join(path.dirname(selectedItemPath), basename);
95
+ // traverse the tree and check if there is already a path with the new name
96
+ const newPathExists = media[newItemPath];
97
+ if (newPathExists) return false;
98
+ return true;
99
+ };
100
+
101
+ export const movePathsIntoTarget = ({
102
+ media,
103
+ sourceItemPaths,
104
+ targetItemPath,
105
+ tree,
106
+ }: {
107
+ media: ResourceMap;
108
+ sourceItemPaths: string[];
109
+ targetItemPath: string;
110
+ tree: ResourceTree;
111
+ }) => {
112
+ const ancestors = ancestorPaths(
113
+ sourceItemPaths.map((path) => path.split("/"))
114
+ );
115
+ const mediaClone = { ...media };
116
+ for (const ancestor of ancestors) {
117
+ const ancestorPath = ancestor.join("/");
118
+ const ancestorItem = mediaClone[ancestorPath];
119
+ const newAncestorPath = path.join(
120
+ targetItemPath,
121
+ path.basename(ancestorPath)
122
+ );
123
+ if (!ancestorItem) continue;
124
+ const descendantPaths = tree
125
+ .flat(ancestorItem)
126
+ .map((item) => tree.idToPathMap.get(item.id));
127
+ for (const descendantPath of descendantPaths) {
128
+ if (!descendantPath) continue;
129
+ const newDescendantPath = descendantPath.replace(
130
+ ancestorPath,
131
+ newAncestorPath
132
+ );
133
+ const newPathIsValid = validateResourceRename({
134
+ basename: path.basename(descendantPath),
135
+ selectedItemPath: newDescendantPath,
136
+ media,
137
+ });
138
+ if (newPathIsValid) {
139
+ mediaClone[newDescendantPath] = mediaClone[descendantPath];
140
+ delete mediaClone[descendantPath];
141
+ }
142
+ }
143
+ }
144
+ return mediaClone;
145
+ };
146
+
147
+ export const moveUpAFolder = ({
148
+ tree,
149
+ media,
150
+ selectedIds,
151
+ }: {
152
+ tree: ResourceTree;
153
+ media: ResourceMap;
154
+ selectedIds: string[];
155
+ }) => {
156
+ const indexPath = tree.findPath(
157
+ rootResource,
158
+ (item) => item.id === selectedIds[0]
159
+ );
160
+ if (!indexPath) return;
161
+
162
+ // Get the grandparent folder (where items will be moved to)
163
+ const grandparentFolder = tree.access(
164
+ rootResource,
165
+ indexPath.slice(0, indexPath.length - 2)
166
+ );
167
+ const grandparentFolderPath = tree.idToPathMap.get(grandparentFolder.id);
168
+ if (!grandparentFolderPath) return;
169
+
170
+ const sourceItemPaths = selectedIds
171
+ .map((id) => tree.idToPathMap.get(id))
172
+ .filter((path): path is string => Boolean(path));
173
+ return movePathsIntoTarget({
174
+ media,
175
+ targetItemPath: grandparentFolderPath,
176
+ sourceItemPaths,
177
+ tree,
178
+ });
179
+ };
180
+
181
+ export const getDepthMap = (
182
+ item: Resource,
183
+ tree: ResourceTree,
184
+ showAllDescendants: boolean
185
+ ): Record<string, number> => {
186
+ const depthMap: Record<string, number> = {};
187
+ tree.visit(item, (item, indexPath) => {
188
+ if (showAllDescendants) {
189
+ depthMap[item.id] = Math.max(indexPath.length - 1, 0);
190
+ } else {
191
+ depthMap[item.id] = 0;
192
+ }
193
+ });
194
+ return depthMap;
195
+ };
196
+
197
+ export const updateExpandedMap = ({
198
+ item,
199
+ expanded,
200
+ expandable,
201
+ expandedMap,
202
+ tree,
203
+ }: {
204
+ item: Resource;
205
+ expanded: boolean;
206
+ expandable: boolean;
207
+ expandedMap: ExpandedMap;
208
+ tree: ResourceTree;
209
+ }): ExpandedMap => {
210
+ const newExpandedMap = { ...expandedMap };
211
+ const inner = (item: Resource, expanded: boolean) => {
212
+ if (!expandable) return {};
213
+ // Don't allow toggling the fake root
214
+ if (item.id === rootResource.id) return {};
215
+ if (!expanded) {
216
+ // recursively collapse all children
217
+ const children = tree.getChildren(item, []);
218
+ children.forEach((child) => inner(child, false));
219
+ }
220
+ newExpandedMap[item.id] = expanded;
221
+ };
222
+ inner(item, expanded);
223
+ return newExpandedMap;
224
+ };
225
+
226
+ export const deleteResources = ({
227
+ selectedIds,
228
+ media,
229
+ tree,
230
+ }: {
231
+ selectedIds: string[];
232
+ media: ResourceMap;
233
+ tree: ResourceTree;
234
+ }): ResourceMap => {
235
+ const itemsToDelete = selectedIds.flatMap((mediaItemId) => {
236
+ const mediaItem = tree.resourcesWithRoot.find(
237
+ (item) => item.id === mediaItemId
238
+ );
239
+ if (!mediaItem) return [];
240
+ return tree.flat(mediaItem);
241
+ });
242
+ const itemKeysToDelete = new Set(
243
+ itemsToDelete.map((item) => tree.idToPathMap.get(item.id))
244
+ );
245
+
246
+ return Object.fromEntries(
247
+ Object.entries(media).filter(([key]) => !itemKeysToDelete.has(key))
248
+ );
249
+ };
250
+
251
+ export const moveMediaInsideFolder = ({
252
+ sourceItemIds,
253
+ targetItemId,
254
+ media,
255
+ tree,
256
+ }: {
257
+ sourceItemIds: string[];
258
+ targetItemId: string;
259
+ media: ResourceMap;
260
+ tree: ResourceTree;
261
+ }) => {
262
+ const targetItemPath = tree.idToPathMap.get(targetItemId);
263
+ if (!targetItemPath) return media;
264
+
265
+ const sourceItemPaths = sourceItemIds
266
+ .map((id) => tree.idToPathMap.get(id))
267
+ .filter((path): path is string => Boolean(path));
268
+
269
+ return movePathsIntoTarget({
270
+ media,
271
+ sourceItemPaths,
272
+ targetItemPath,
273
+ tree,
274
+ });
275
+ };
276
+
277
+ export const getParentDirectories = (
278
+ resourceMap: ResourceMap,
279
+ folderId: string
280
+ ) => {
281
+ const tree = createResourceTree(resourceMap);
282
+
283
+ const indexPath = tree.findPath(rootResource, (item) => item.id === folderId);
284
+
285
+ if (!indexPath) return [rootResource];
286
+
287
+ return tree.accessPath(rootResource, indexPath);
288
+ };
289
+
290
+ export const renameResourceAndDescendantPaths = ({
291
+ newName,
292
+ selectedItemPath,
293
+ media,
294
+ tree,
295
+ }: {
296
+ newName: string;
297
+ selectedItemPath: string;
298
+ media: ResourceMap;
299
+ tree: ResourceTree;
300
+ }) => {
301
+ const mediaClone = { ...media };
302
+ const selectedItem = mediaClone[selectedItemPath];
303
+ if (!selectedItem) return mediaClone;
304
+
305
+ const parentPath = path.dirname(selectedItemPath);
306
+ const newItemPath = path.join(parentPath, newName);
307
+
308
+ const descendants = tree
309
+ .flat(selectedItem)
310
+ .map((item) => tree.idToPathMap.get(item.id));
311
+
312
+ // Update all descendants' paths
313
+ for (const descendantPath of descendants) {
314
+ if (!descendantPath) continue;
315
+
316
+ // Use the same approach as movePathsIntoTarget
317
+ const newDescendantPath = descendantPath.replace(
318
+ selectedItemPath,
319
+ newItemPath
320
+ );
321
+ mediaClone[newDescendantPath] = {
322
+ ...mediaClone[descendantPath],
323
+ path: newDescendantPath,
324
+ };
325
+ delete mediaClone[descendantPath];
326
+ }
327
+
328
+ return mediaClone;
329
+ };
@@ -1,8 +0,0 @@
1
- const byteSizeUnits = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
2
-
3
- export function formatByteSize(size: number) {
4
- const unitIndex = Math.floor(Math.log(size) / Math.log(1024));
5
- const unit = byteSizeUnits[unitIndex];
6
- const value = size / Math.pow(1024, unitIndex);
7
- return `${value.toFixed(1)} ${unit}`;
8
- }