@brftech/filex-core 0.40.0 → 0.41.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.
- package/README.md +32 -25
- package/dist/ArchiveViewer-DxxjhYZD.js +124 -0
- package/dist/ArchiveViewer-DxxjhYZD.js.map +1 -0
- package/dist/CsvViewer-CyuwENI_.js +141 -0
- package/dist/CsvViewer-CyuwENI_.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-BIYsTX97.js} +48 -42
- package/dist/DrawioViewer-BIYsTX97.js.map +1 -0
- package/dist/EpubViewer-K9PjCMmS.js +163 -0
- package/dist/EpubViewer-K9PjCMmS.js.map +1 -0
- package/dist/IpynbViewer-DYU6Zgz2.js +184 -0
- package/dist/IpynbViewer-DYU6Zgz2.js.map +1 -0
- package/dist/MermaidViewer-Bp73aId8.js +137 -0
- package/dist/MermaidViewer-Bp73aId8.js.map +1 -0
- package/dist/PsdViewer-C2eLivw1.js +122 -0
- package/dist/PsdViewer-C2eLivw1.js.map +1 -0
- package/dist/TiffViewer-DsHg5gcz.js +142 -0
- package/dist/TiffViewer-DsHg5gcz.js.map +1 -0
- package/dist/Viewer3D-C_dc2HN9.js +75 -0
- package/dist/Viewer3D-C_dc2HN9.js.map +1 -0
- package/dist/filex-core.js +218 -20361
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +61 -62
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-vxMZpYb-.js +26700 -0
- package/dist/index-vxMZpYb-.js.map +1 -0
- package/dist/index.d.ts +4212 -862
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2154 -506
- package/src/components/AdvancedSearch.vue +870 -0
- package/src/components/Breadcrumb.vue +191 -19
- package/src/components/CommandPalette.vue +66 -22
- package/src/components/ConnectionsPanel.vue +6 -2
- package/src/components/ContextMenu.vue +39 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +951 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +172 -56
- package/src/components/GridView.vue +331 -82
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1173 -194
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +26 -1
- package/src/components/RecentlyOpened.vue +17 -2
- package/src/components/S3KeysPanel.vue +7 -4
- package/src/components/SSHKeysPanel.vue +7 -4
- package/src/components/ShortcutSettings.vue +6 -2
- package/src/components/ShortcutsHelp.vue +4 -1
- package/src/components/SideNav.vue +206 -140
- package/src/components/StorageFields.vue +19 -1
- package/src/components/TabBar.vue +87 -1
- package/src/components/TagPicker.vue +77 -6
- package/src/components/ThemeGallery.vue +13 -198
- package/src/components/ThemePalette.vue +244 -0
- package/src/components/TimeZoneDialog.vue +135 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +35 -14
- package/src/components/Toolbar.vue +953 -465
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +89 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useLocale.ts +382 -26
- package/src/composables/useRowTouch.ts +95 -0
- package/src/index.ts +177 -3
- package/src/lib/actionIcons.ts +330 -0
- package/src/lib/advSearch.ts +162 -0
- package/src/lib/dateGroups.ts +197 -0
- package/src/lib/destinationTree.ts +235 -0
- package/src/lib/downloadSelection.ts +154 -0
- package/src/lib/fileFilters.ts +319 -12
- package/src/lib/fileIcons.ts +299 -5
- package/src/lib/filePreview.ts +605 -0
- package/src/lib/listing.ts +72 -0
- package/src/lib/shareTtl.ts +31 -2
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +151 -116
- package/src/lib/timezone.ts +501 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +416 -26
- package/src/locales/tr.ts +409 -26
- package/src/modals/ConvertModal.vue +23 -3
- package/src/modals/DestinationPickerModal.vue +341 -0
- package/src/modals/Modal.vue +18 -4
- package/src/modals/NewDocumentModal.vue +525 -0
- package/src/modals/NewFolderModal.vue +15 -9
- package/src/modals/PermissionsModal.vue +557 -476
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5409 -8
- package/src/styles/variables.css +139 -65
- package/src/types/Connections.ts +9 -0
- package/src/types/ExplorerConfig.ts +144 -39
- package/src/types/FileNode.ts +28 -0
- package/src/viewers/ArchiveViewer.vue +46 -8
- package/src/viewers/CsvViewer.vue +19 -2
- package/src/viewers/DrawioViewer.vue +12 -1
- package/src/viewers/EpubViewer.vue +50 -6
- package/src/viewers/IpynbViewer.vue +19 -2
- package/src/viewers/MermaidViewer.vue +20 -3
- package/src/viewers/PdfViewer.vue +24 -4
- package/src/viewers/PsdViewer.vue +28 -4
- package/src/viewers/TiffViewer.vue +19 -2
- package/src/viewers/Viewer3D.vue +36 -2
- package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
- package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
- package/dist/CsvViewer-CqWeV8VO.js +0 -131
- package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
- package/dist/DrawioViewer-BNALOB04.js.map +0 -1
- package/dist/EpubViewer-BPmlImG7.js +0 -145
- package/dist/EpubViewer-BPmlImG7.js.map +0 -1
- package/dist/IpynbViewer-CDlQmuCV.js +0 -175
- package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
- package/dist/MermaidViewer-sfZWdR-d.js +0 -127
- package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
- package/dist/PsdViewer-DwuySZsb.js +0 -112
- package/dist/PsdViewer-DwuySZsb.js.map +0 -1
- package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
- package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
- package/dist/Viewer3D-CJJJgkuv.js +0 -60
- package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
- package/src/components/SecondaryPane.vue +0 -447
- package/src/modals/ShareModal.vue +0 -139
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* ViewSwitcher — the list / grid / gallery toggle.
|
|
4
4
|
*
|
|
5
5
|
* Lifted out of Toolbar.vue unchanged (same classes, same glyphs, same
|
|
6
|
-
* `role="tablist"` semantics
|
|
6
|
+
* `role="tablist"` semantics — the glyphs moved to lib/actionIcons with the
|
|
7
|
+
* rest of the emoji, see `gorunum:v1-icons`) because the `drive` profile puts it on the
|
|
7
8
|
* breadcrumb row instead of in the header, and two copies of a switcher is
|
|
8
9
|
* how the two rows end up offering different modes. One component, mounted
|
|
9
10
|
* in whichever row the profile asks for.
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
* block's `data-v` hash does not match in the web-component build).
|
|
16
17
|
*/
|
|
17
18
|
import { useLocale } from '../composables/useLocale';
|
|
19
|
+
import { actionIconSvg } from '../lib/actionIcons';
|
|
18
20
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
19
21
|
import type { ViewMode } from '../types/FileNode';
|
|
20
22
|
|
|
@@ -47,7 +49,8 @@ function offers(v: ViewMode): boolean {
|
|
|
47
49
|
data-testid="view-list"
|
|
48
50
|
@click="emit('update:viewMode', 'list')"
|
|
49
51
|
>
|
|
50
|
-
|
|
52
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/actionIcons -->
|
|
53
|
+
<span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('view-list')"></span>
|
|
51
54
|
</button>
|
|
52
55
|
<button
|
|
53
56
|
v-if="offers('grid')"
|
|
@@ -61,7 +64,8 @@ function offers(v: ViewMode): boolean {
|
|
|
61
64
|
data-testid="view-grid"
|
|
62
65
|
@click="emit('update:viewMode', 'grid')"
|
|
63
66
|
>
|
|
64
|
-
|
|
67
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/actionIcons -->
|
|
68
|
+
<span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('view-grid')"></span>
|
|
65
69
|
</button>
|
|
66
70
|
<button
|
|
67
71
|
v-if="offers('gallery')"
|
|
@@ -75,7 +79,8 @@ function offers(v: ViewMode): boolean {
|
|
|
75
79
|
data-testid="view-gallery"
|
|
76
80
|
@click="emit('update:viewMode', 'gallery')"
|
|
77
81
|
>
|
|
78
|
-
|
|
82
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/actionIcons -->
|
|
83
|
+
<span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('view-gallery')"></span>
|
|
79
84
|
</button>
|
|
80
85
|
</div>
|
|
81
86
|
</template>
|
|
@@ -53,11 +53,25 @@ export function connectionsBase(config: ExplorerConfig): string {
|
|
|
53
53
|
* The origin a client program should be pointed at.
|
|
54
54
|
*
|
|
55
55
|
* The instruction pages are only worth anything if they name the real
|
|
56
|
-
* deployment, so this resolves to an absolute
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* deployment, so this resolves to an absolute address, best evidence first:
|
|
57
|
+
*
|
|
58
|
+
* 1. `publicUrl` — what the SERVER says it is reached at
|
|
59
|
+
* (`/api/files/capabilities` → `public_url`, published only when the
|
|
60
|
+
* operator configured it or the request came in on a tenant's own host);
|
|
61
|
+
* 2. the configured apiBase when it is absolute (the desktop app, embeds
|
|
62
|
+
* that talk to filex directly);
|
|
63
|
+
* 3. the page's own origin (the admin SPA, which the same binary serves).
|
|
64
|
+
*
|
|
65
|
+
* ⚠⚠ The first rung is new and it is the fix. Without it an explorer embedded
|
|
66
|
+
* in ANOTHER app — which proxies `/api` to filex under its own origin, the way
|
|
67
|
+
* work.example.com and fishapp do — printed `https://<that app>/dav/`: an address
|
|
68
|
+
* that reaches the host application, never filex. It also left one guide page
|
|
69
|
+
* speaking with two voices, because its S3 endpoint and SFTP host were always
|
|
70
|
+
* the server's and only the WebDAV / mount lines were the page's.
|
|
59
71
|
*/
|
|
60
|
-
export function connectionsOrigin(config: ExplorerConfig): string {
|
|
72
|
+
export function connectionsOrigin(config: ExplorerConfig, publicUrl?: string | null): string {
|
|
73
|
+
const told = String(publicUrl ?? '').trim();
|
|
74
|
+
if (/^https?:\/\/[^/]/i.test(told)) return told.replace(/\/+$/, '');
|
|
61
75
|
const base = connectionsBase(config);
|
|
62
76
|
if (/^https?:\/\//i.test(base)) {
|
|
63
77
|
try {
|
|
@@ -80,6 +94,8 @@ export function useConnections(config: ExplorerConfig) {
|
|
|
80
94
|
/** Storage names a non-admin may see (manager root). */
|
|
81
95
|
const visible = shallowRef<string[]>([]);
|
|
82
96
|
const me = ref<ConnectionsUser | null>(null);
|
|
97
|
+
/** The server's own public address, when it has one to give (see connectionsOrigin). */
|
|
98
|
+
const publicUrl = ref<string | null>(null);
|
|
83
99
|
|
|
84
100
|
const loading = ref(false);
|
|
85
101
|
const loaded = ref(false);
|
|
@@ -102,6 +118,19 @@ export function useConnections(config: ExplorerConfig) {
|
|
|
102
118
|
loading.value = true;
|
|
103
119
|
error.value = null;
|
|
104
120
|
try {
|
|
121
|
+
// Where the server says it lives. Best-effort and in parallel: a server
|
|
122
|
+
// older than `public_url`, or one that has none to give, leaves the
|
|
123
|
+
// guides on the address the page loaded from — exactly as before.
|
|
124
|
+
const told = api
|
|
125
|
+
.capabilities()
|
|
126
|
+
.then((c) => {
|
|
127
|
+
const v = (c as { public_url?: unknown } | null)?.public_url;
|
|
128
|
+
publicUrl.value = typeof v === 'string' && v ? v : null;
|
|
129
|
+
})
|
|
130
|
+
.catch(() => {
|
|
131
|
+
publicUrl.value = null;
|
|
132
|
+
});
|
|
133
|
+
|
|
105
134
|
// Identity first: the guides need the caller's own e-mail (it IS the
|
|
106
135
|
// WebDAV username), and it is the one call every role may make.
|
|
107
136
|
try {
|
|
@@ -152,6 +181,7 @@ export function useConnections(config: ExplorerConfig) {
|
|
|
152
181
|
visible.value = [];
|
|
153
182
|
}
|
|
154
183
|
}
|
|
184
|
+
await told;
|
|
155
185
|
loaded.value = true;
|
|
156
186
|
} finally {
|
|
157
187
|
loading.value = false;
|
|
@@ -249,6 +279,7 @@ export function useConnections(config: ExplorerConfig) {
|
|
|
249
279
|
storages,
|
|
250
280
|
visible,
|
|
251
281
|
me,
|
|
282
|
+
publicUrl,
|
|
252
283
|
loading,
|
|
253
284
|
loaded,
|
|
254
285
|
error,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useExplorerTimeZone — what ONE explorer instance knows about the clock, fed
|
|
3
|
+
* into `lib/timezone`'s resolver.
|
|
4
|
+
*
|
|
5
|
+
* An explorer knows two of the four tiers and nobody else can know them for
|
|
6
|
+
* it: the zone its host configured (`config.timeZone`, tier `host`) and the
|
|
7
|
+
* account behind the credential it holds (tier `account`). The viewer tier is
|
|
8
|
+
* the browser's and the device tier is `Intl`'s; neither needs an instance.
|
|
9
|
+
*
|
|
10
|
+
* How the account is learned — nothing new on the server:
|
|
11
|
+
*
|
|
12
|
+
* 1. the credential's KIND, from `GET /api/files/capabilities`
|
|
13
|
+
* (`caller_kind`, migration 00030), which the explorer already fetches —
|
|
14
|
+
* or `config.callerKind`, which a host may set to answer sooner and which
|
|
15
|
+
* wins over the server's, exactly as it does for the panel's surfaces;
|
|
16
|
+
* 2. for a person, `GET /api/auth/me` — the route every role may call, open
|
|
17
|
+
* to a bearer token (MiddlewareWithToken) and to cross-origin callers
|
|
18
|
+
* (CORS allows `Authorization`), and already used by the Connections
|
|
19
|
+
* panel for the caller's own e-mail. `user.timezone` is the zone.
|
|
20
|
+
*
|
|
21
|
+
* ⚠⚠ An `app` token skips the tier. It authenticates AS its owner, so
|
|
22
|
+
* `/api/auth/me` would happily answer with that owner's zone — and the embeds
|
|
23
|
+
* we run authenticate every visitor with ONE such token. Imposing its owner's
|
|
24
|
+
* clock on all of them is the exact thing the owner ruled out.
|
|
25
|
+
*
|
|
26
|
+
* ⚠ Unknown kind also skips. When capabilities never lands (an old server, a
|
|
27
|
+
* host that routes nothing but the manager), nobody can say whether the key is
|
|
28
|
+
* a person's, and guessing "person" would be guessing the dangerous way.
|
|
29
|
+
*
|
|
30
|
+
* ⚠ Every failure is silent and falls through: a 401, a 404 from a host that
|
|
31
|
+
* does not proxy `/api/auth/me`, a network error. The tier is withdrawn and
|
|
32
|
+
* the browser's zone answers. A clock must never throw.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { onBeforeUnmount, provide, watch, type Ref } from 'vue';
|
|
36
|
+
import type { ExplorerConfig } from '../types/ExplorerConfig';
|
|
37
|
+
import type { Capabilities } from '../types/FileNode';
|
|
38
|
+
import { EXPLORER_CLOCK, releaseTimeZoneOwner, setAccountTimeZone, setHostTimeZone } from '../lib/timezone';
|
|
39
|
+
|
|
40
|
+
export interface ExplorerTimeZoneDeps {
|
|
41
|
+
config: () => ExplorerConfig;
|
|
42
|
+
/** The explorer's capabilities answer; `null` until (and unless) it lands. */
|
|
43
|
+
capabilities: Ref<Capabilities | null>;
|
|
44
|
+
/** `GET /api/auth/me` with this explorer's credential. */
|
|
45
|
+
fetchMe: () => Promise<{ user?: { timezone?: unknown } | null } | undefined>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useExplorerTimeZone(deps: ExplorerTimeZoneDeps): symbol {
|
|
49
|
+
const owner = Symbol('filex-explorer');
|
|
50
|
+
// Every component under this explorer reads its dates on this owner's
|
|
51
|
+
// tiers, not the page's newest (lib/timezone, timeZoneSourcesFor).
|
|
52
|
+
provide(EXPLORER_CLOCK, owner);
|
|
53
|
+
|
|
54
|
+
watch(
|
|
55
|
+
() => deps.config().timeZone,
|
|
56
|
+
(tz) => setHostTimeZone(owner, tz),
|
|
57
|
+
{ immediate: true },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const kind = (): 'user' | 'app' | undefined =>
|
|
61
|
+
deps.config().callerKind ?? deps.capabilities.value?.caller_kind;
|
|
62
|
+
|
|
63
|
+
let asked = false;
|
|
64
|
+
watch(
|
|
65
|
+
kind,
|
|
66
|
+
(k) => {
|
|
67
|
+
if (k !== 'user') {
|
|
68
|
+
// `app`: nobody's account. Unknown: cannot tell. Either way, withdraw.
|
|
69
|
+
setAccountTimeZone(owner, null);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (asked) return;
|
|
73
|
+
asked = true;
|
|
74
|
+
deps
|
|
75
|
+
.fetchMe()
|
|
76
|
+
.then((body) => {
|
|
77
|
+
// The kind may have changed while the request was out.
|
|
78
|
+
if (kind() !== 'user') return;
|
|
79
|
+
const tz = body?.user?.timezone;
|
|
80
|
+
setAccountTimeZone(owner, typeof tz === 'string' ? tz : null);
|
|
81
|
+
})
|
|
82
|
+
.catch(() => setAccountTimeZone(owner, null));
|
|
83
|
+
},
|
|
84
|
+
{ immediate: true },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
onBeforeUnmount(() => releaseTimeZoneOwner(owner));
|
|
88
|
+
return owner;
|
|
89
|
+
}
|
|
@@ -48,6 +48,17 @@ export interface PendingOpDto {
|
|
|
48
48
|
created_at: string | null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** Answer to `?action=newfile` — where the new document actually landed. */
|
|
52
|
+
export interface NewFileResponse {
|
|
53
|
+
/** Adapter-qualified path, ready to hand to the viewer. */
|
|
54
|
+
path: string;
|
|
55
|
+
/** Final basename, which may have gained the extension server-side. */
|
|
56
|
+
name: string;
|
|
57
|
+
ext: string;
|
|
58
|
+
size: number;
|
|
59
|
+
mime: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
export interface ManagerResponse {
|
|
52
63
|
adapter: string;
|
|
53
64
|
storages: string[];
|
|
@@ -504,6 +515,27 @@ export function useFileApi(config: ExplorerConfig) {
|
|
|
504
515
|
});
|
|
505
516
|
}
|
|
506
517
|
|
|
518
|
+
/**
|
|
519
|
+
* Create an empty document of a known type in `path`.
|
|
520
|
+
*
|
|
521
|
+
* `type` is an extension from `capabilities.newdoc_types` — the registry the
|
|
522
|
+
* SERVER compiled in, not a list the client keeps. That matters for the
|
|
523
|
+
* office formats: a .docx is a ZIP of XML parts, so "create an empty file"
|
|
524
|
+
* has to be answered by whoever holds the template bytes, and the client
|
|
525
|
+
* cannot manufacture one.
|
|
526
|
+
*
|
|
527
|
+
* `name` may or may not already carry the extension; the server appends it
|
|
528
|
+
* when it is missing. Throws on a name collision (409 NAME_TAKEN) — the
|
|
529
|
+
* dialog warns first, but this is the check.
|
|
530
|
+
*/
|
|
531
|
+
async function newFile(path: string, name: string, type: string): Promise<NewFileResponse> {
|
|
532
|
+
return jsonFetch<NewFileResponse>(managerUrl('newfile'), {
|
|
533
|
+
method: 'POST',
|
|
534
|
+
headers: { 'Content-Type': 'application/json' },
|
|
535
|
+
body: JSON.stringify({ path, name, type }),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
507
539
|
async function rename(path: string, item: string, name: string): Promise<ManagerResponse> {
|
|
508
540
|
return jsonFetch<ManagerResponse>(managerUrl('rename'), {
|
|
509
541
|
method: 'POST',
|
|
@@ -577,11 +609,18 @@ export function useFileApi(config: ExplorerConfig) {
|
|
|
577
609
|
/**
|
|
578
610
|
* Restore soft-deleted nodes by their node id. The filex backend restores
|
|
579
611
|
* one node per call (`POST {node_id}`), so we fan out and tally successes.
|
|
612
|
+
*
|
|
613
|
+
* `taken` names the entries the server refused because something already
|
|
614
|
+
* holds their original path (409 `EXISTS`). That refusal is the server
|
|
615
|
+
* protecting the file that holds the name — a restore used to overwrite it —
|
|
616
|
+
* so it is reported by name rather than folded into "0 items restored",
|
|
617
|
+
* which would read as if nothing had been tried.
|
|
580
618
|
*/
|
|
581
|
-
async function restoreIds(ids: number[]): Promise<{ restored: number }> {
|
|
619
|
+
async function restoreIds(ids: number[]): Promise<{ restored: number; taken: string[] }> {
|
|
582
620
|
const url = endpoints.trashRestore;
|
|
583
621
|
if (!url) throw new Error('trashRestore endpoint not configured');
|
|
584
622
|
let restored = 0;
|
|
623
|
+
const taken: string[] = [];
|
|
585
624
|
for (const id of ids) {
|
|
586
625
|
try {
|
|
587
626
|
await jsonFetch(url, {
|
|
@@ -590,11 +629,20 @@ export function useFileApi(config: ExplorerConfig) {
|
|
|
590
629
|
body: JSON.stringify({ node_id: id }),
|
|
591
630
|
});
|
|
592
631
|
restored++;
|
|
593
|
-
} catch {
|
|
594
|
-
|
|
632
|
+
} catch (err) {
|
|
633
|
+
const e = err as { status?: number; detail?: string };
|
|
634
|
+
if (e.status === 409) {
|
|
635
|
+
try {
|
|
636
|
+
const body = JSON.parse(e.detail ?? '') as { code?: string; name?: string };
|
|
637
|
+
if (body.code === 'EXISTS') taken.push(body.name || String(id));
|
|
638
|
+
} catch {
|
|
639
|
+
/* a 409 without the envelope is counted as a plain failure */
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/* any other failure: skip it, report the count that succeeded */
|
|
595
643
|
}
|
|
596
644
|
}
|
|
597
|
-
return { restored };
|
|
645
|
+
return { restored, taken };
|
|
598
646
|
}
|
|
599
647
|
|
|
600
648
|
/**
|
|
@@ -934,6 +982,7 @@ export function useFileApi(config: ExplorerConfig) {
|
|
|
934
982
|
quotaMe /* surucu:d1 */,
|
|
935
983
|
subfolders,
|
|
936
984
|
newFolder,
|
|
985
|
+
newFile,
|
|
937
986
|
rename,
|
|
938
987
|
move,
|
|
939
988
|
copy,
|