@brftech/filex-core 0.42.2 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/dist/ArchiveViewer-BqU761Wk.js +149 -0
- package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
- package/dist/CsvViewer-ByHVK5ga.js +172 -0
- package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
- package/dist/{DrawioViewer-CC6rJXeP.js → DrawioViewer-Bgrj8-zG.js} +45 -44
- package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
- package/dist/{EpubViewer-CAoTSXTO.js → EpubViewer-DJwUKlHW.js} +50 -50
- package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
- package/dist/MermaidViewer-CEDTD_pp.js +136 -0
- package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
- package/dist/PsdViewer-CZcxUpZO.js +121 -0
- package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
- package/dist/TiffViewer-BwI62CbQ.js +141 -0
- package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
- package/dist/{Viewer3D-CdZeK4j4.js → Viewer3D-DoeZcuMm.js} +16 -16
- package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
- package/dist/filex-core.js +475 -219
- package/dist/filex-core.umd.cjs +165 -108
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/fonts/caveat-latin-ext.woff2 +0 -0
- package/dist/fonts/caveat-latin.woff2 +0 -0
- package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/dist/fonts/dancing-script-latin.woff2 +0 -0
- package/dist/fonts/homemade-apple-latin.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin.woff2 +0 -0
- package/dist/index-DuWnGuv5.js +35901 -0
- package/dist/index-DuWnGuv5.js.map +1 -0
- package/dist/index.d.ts +5227 -553
- package/dist/pdf-CaPYsOUo.js +21302 -0
- package/dist/pdf-CaPYsOUo.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +1055 -253
- package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
- package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
- package/src/assets/fonts/README.md +58 -0
- package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
- package/src/assets/fonts/caveat-latin.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
- package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
- package/src/components/AdvancedSearch.vue +49 -0
- package/src/components/Breadcrumb.vue +5 -5
- package/src/components/ChoiceButtons.vue +152 -0
- package/src/components/CommandPalette.vue +5 -5
- package/src/components/ConnectionGuideView.vue +1 -1
- package/src/components/ConnectionNotice.vue +59 -0
- package/src/components/ConnectionsPanel.vue +30 -568
- package/src/components/ContextMenu.vue +31 -5
- package/src/components/DataTable.vue +1331 -0
- package/src/components/FilePane.vue +3 -0
- package/src/components/FilterBar.vue +12 -5
- package/src/components/GalleryView.vue +50 -2
- package/src/components/GridView.vue +51 -3
- package/src/components/HomeView.vue +18 -6
- package/src/components/InspectorPanel.vue +134 -17
- package/src/components/ListView.vue +366 -900
- package/src/components/LogoMark.vue +57 -0
- package/src/components/NFSExportsPanel.vue +76 -28
- package/src/components/OnboardingTour.vue +19 -7
- package/src/components/OperationsCenter.vue +77 -1
- package/src/components/PendingOpsTray.vue +59 -5
- package/src/components/PresenceBar.vue +7 -7
- package/src/components/ProductVersion.vue +31 -0
- package/src/components/QuickLook.vue +10 -14
- package/src/components/RecentlyOpened.vue +31 -15
- package/src/components/RecoveryKeyModal.vue +1 -1
- package/src/components/RowActions.vue +134 -0
- package/src/components/S3KeysPanel.vue +98 -62
- package/src/components/SSHKeysPanel.vue +79 -30
- package/src/components/SideNav.vue +260 -96
- package/src/components/StarButton.vue +1 -0
- package/src/components/StorageFields.vue +74 -6
- package/src/components/StorageTags.vue +62 -0
- package/src/components/TabBar.vue +1 -1
- package/src/components/TagKindIcon.vue +56 -0
- package/src/components/TagPicker.vue +226 -40
- package/src/components/ThemePalette.vue +4 -4
- package/src/components/TimeZonePicker.vue +29 -10
- package/src/components/TokensPanel.vue +82 -31
- package/src/components/Toolbar.vue +28 -6
- package/src/components/plugin/PluginConfirmModal.vue +54 -0
- package/src/components/plugin/PluginInspectorSection.vue +149 -0
- package/src/components/plugin/PluginPageView.vue +339 -0
- package/src/components/plugin/PluginViewModal.vue +159 -0
- package/src/components/plugin/SurfaceConversation.vue +76 -0
- package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
- package/src/components/plugin/SurfaceRenderer.vue +299 -0
- package/src/components/plugin/SurfaceSections.vue +91 -0
- package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
- package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
- package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
- package/src/components/plugin/nodes/SurfaceList.vue +127 -0
- package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
- package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
- package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
- package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
- package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
- package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
- package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
- package/src/components/public/PublicLanguagePicker.vue +58 -0
- package/src/components/public/PublicLinkPage.vue +286 -0
- package/src/components/public/PublicLinkPreview.vue +85 -0
- package/src/components/public/PublicPinGate.vue +108 -0
- package/src/components/public/PublicRequestBody.vue +204 -0
- package/src/components/public/PublicShareBody.vue +188 -0
- package/src/components/public/PublicShell.vue +332 -0
- package/src/composables/useConnections.ts +8 -2
- package/src/composables/useFileApi.ts +183 -44
- package/src/composables/useLocale.ts +277 -41
- package/src/composables/useNFSExports.ts +8 -10
- package/src/composables/useOperations.ts +44 -3
- package/src/composables/usePendingOps.ts +59 -8
- package/src/composables/usePluginActions.ts +121 -0
- package/src/composables/usePluginSurface.ts +223 -0
- package/src/composables/usePublicBranding.ts +172 -0
- package/src/composables/usePublicLink.ts +573 -0
- package/src/composables/usePublicPage.ts +61 -0
- package/src/composables/useS3Keys.ts +17 -12
- package/src/composables/useSSHKeys.ts +8 -10
- package/src/composables/useTokens.ts +8 -12
- package/src/composables/useUploadChunked.ts +18 -8
- package/src/composables/useViewerFetch.ts +6 -3
- package/src/index.ts +404 -8
- package/src/lib/actionIcons.ts +46 -1
- package/src/lib/advSearch.ts +14 -7
- package/src/lib/appLock.ts +130 -0
- package/src/lib/connection.ts +110 -0
- package/src/lib/destinationTree.ts +19 -3
- package/src/lib/direction.ts +353 -0
- package/src/lib/dragGhost.ts +6 -0
- package/src/lib/dropLimits.ts +69 -0
- package/src/lib/errorWords.ts +394 -0
- package/src/lib/externalReach.ts +5 -1
- package/src/lib/fileFilters.ts +7 -2
- package/src/lib/fileIcons.ts +4 -0
- package/src/lib/internalPaths.ts +77 -0
- package/src/lib/listInput.ts +64 -0
- package/src/lib/listing.ts +58 -23
- package/src/lib/modalStack.ts +26 -0
- package/src/lib/nodeRow.ts +18 -3
- package/src/lib/pdfFieldRules.ts +126 -0
- package/src/lib/pdfFields.ts +589 -0
- package/src/lib/pdfFieldsGeom.ts +203 -0
- package/src/lib/pdfjsLoader.ts +51 -0
- package/src/lib/personName.ts +47 -0
- package/src/lib/pluginApplies.ts +123 -0
- package/src/lib/pluginLabel.ts +72 -0
- package/src/lib/pluginMenu.ts +253 -0
- package/src/lib/pluginPage.ts +58 -0
- package/src/lib/plural.ts +181 -0
- package/src/lib/popupLayer.ts +103 -0
- package/src/lib/prefs.ts +563 -0
- package/src/lib/productVersion.ts +35 -0
- package/src/lib/publicLayout.ts +39 -0
- package/src/lib/serviceGate.ts +104 -0
- package/src/lib/shareTtl.ts +25 -11
- package/src/lib/signFonts.ts +85 -0
- package/src/lib/signaturePad.ts +111 -0
- package/src/lib/sortOrder.ts +100 -38
- package/src/lib/star.ts +7 -1
- package/src/lib/surfaceCell.ts +41 -0
- package/src/lib/surfaceConditions.ts +156 -0
- package/src/lib/surfaceOpen.ts +106 -0
- package/src/lib/surfaceSteps.ts +68 -0
- package/src/lib/surfaceValues.ts +191 -0
- package/src/lib/symlink.ts +150 -0
- package/src/lib/tableColumns.ts +493 -0
- package/src/lib/tableEnv.ts +34 -0
- package/src/lib/tablePrefs.ts +84 -0
- package/src/lib/tags.ts +121 -12
- package/src/lib/themes.ts +286 -18
- package/src/lib/tour.ts +152 -0
- package/src/lib/trashEmpty.ts +111 -0
- package/src/lib/trashTimeLeft.ts +23 -0
- package/src/lib/uiLocales.ts +460 -0
- package/src/lib/viewPrefs.ts +696 -660
- package/src/lib/viewPrefsHttp.ts +78 -0
- package/src/locales/en.ts +515 -117
- package/src/locales/resolve.ts +17 -8
- package/src/locales/tr.ts +536 -152
- package/src/modals/ConvertModal.vue +62 -11
- package/src/modals/DestinationPickerModal.vue +59 -16
- package/src/modals/Modal.vue +113 -23
- package/src/modals/NewDocumentModal.vue +16 -6
- package/src/modals/NewFolderModal.vue +8 -0
- package/src/modals/PermissionsModal.vue +272 -163
- package/src/modals/PreviewModal.vue +119 -20
- package/src/modals/RenameModal.vue +37 -1
- package/src/styles/base.css +2890 -260
- package/src/styles/sign-fonts.css +118 -0
- package/src/styles/variables.css +60 -0
- package/src/types/Connections.ts +17 -0
- package/src/types/ExplorerConfig.ts +117 -1
- package/src/types/FileNode.ts +50 -0
- package/src/types/Plugins.ts +494 -0
- package/src/types/Public.ts +212 -0
- package/src/viewers/ArchiveViewer.vue +93 -48
- package/src/viewers/CsvViewer.vue +99 -53
- package/src/viewers/DrawioViewer.vue +35 -9
- package/src/viewers/EpubViewer.vue +7 -8
- package/src/viewers/IpynbViewer.vue +3 -1
- package/src/viewers/MermaidViewer.vue +4 -5
- package/src/viewers/PdfViewer.vue +8 -17
- package/src/viewers/PsdViewer.vue +2 -4
- package/src/viewers/TiffViewer.vue +4 -5
- package/src/viewers/Viewer3D.vue +1 -1
- package/LICENSE +0 -21
- package/dist/ArchiveViewer-BUQWLhwX.js +0 -124
- package/dist/ArchiveViewer-BUQWLhwX.js.map +0 -1
- package/dist/CsvViewer-Bg2_aqzc.js +0 -141
- package/dist/CsvViewer-Bg2_aqzc.js.map +0 -1
- package/dist/DrawioViewer-CC6rJXeP.js.map +0 -1
- package/dist/EpubViewer-CAoTSXTO.js.map +0 -1
- package/dist/IpynbViewer-D0GwGpnE.js +0 -184
- package/dist/IpynbViewer-D0GwGpnE.js.map +0 -1
- package/dist/MermaidViewer-DGwAvRZg.js +0 -137
- package/dist/MermaidViewer-DGwAvRZg.js.map +0 -1
- package/dist/PsdViewer-DhGweA0m.js +0 -122
- package/dist/PsdViewer-DhGweA0m.js.map +0 -1
- package/dist/TiffViewer-D0AurvAh.js +0 -142
- package/dist/TiffViewer-D0AurvAh.js.map +0 -1
- package/dist/Viewer3D-CdZeK4j4.js.map +0 -1
- package/dist/index-BArI-B1M.js +0 -26911
- package/dist/index-BArI-B1M.js.map +0 -1
- package/dist/useViewerFetch-czqbd2Lj.js +0 -25
- package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
- package/src/composables/useScrolledX.ts +0 -21
|
@@ -190,6 +190,8 @@ const props = withDefaults(
|
|
|
190
190
|
folderKey?: string;
|
|
191
191
|
/** Print each row's parent folder (a search, a cross-folder view). */
|
|
192
192
|
showParentPath?: boolean;
|
|
193
|
+
/** These rows are the Trash (ListView `trash`: deleted-on, deleted-from, time left). */
|
|
194
|
+
trash?: boolean;
|
|
193
195
|
/** Paths that were cut — dimmed, and never dragged. */
|
|
194
196
|
clipped?: Set<string>;
|
|
195
197
|
/** A second narrowing composed AFTER this pane's own filter row (the
|
|
@@ -910,6 +912,7 @@ watch(panePath, () => {
|
|
|
910
912
|
:selected="selected"
|
|
911
913
|
:clipped="clipped"
|
|
912
914
|
:show-parent-path="showParentPath"
|
|
915
|
+
:trash="trash"
|
|
913
916
|
:locale="locale"
|
|
914
917
|
:loading="paneLoading"
|
|
915
918
|
:folder-key="folderKey"
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
*/
|
|
69
69
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
70
70
|
import { useLocale } from '../composables/useLocale';
|
|
71
|
+
import { clampAlongInline, inlineStartX } from '../lib/direction';
|
|
71
72
|
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
72
73
|
import type { FileNode } from '../types/FileNode';
|
|
73
74
|
import type { DriveFilters, ModifiedFilter, SizeFilter, TypeFilter } from '../lib/fileFilters';
|
|
@@ -153,7 +154,9 @@ const emit = defineEmits<{
|
|
|
153
154
|
(e: 'select-all'): void;
|
|
154
155
|
}>();
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
// ⚠ RTL: `dir` goes on the teleported popover (under <body> it would take the
|
|
158
|
+
// host page's direction) and steers where it hangs from its chip.
|
|
159
|
+
const { t, dir } = useLocale(() => props.locale);
|
|
157
160
|
|
|
158
161
|
/** surucu:d1-scope — everything except the name box is drawn only in `full`. */
|
|
159
162
|
const full = computed(() => (props.mode ?? 'full') === 'full');
|
|
@@ -356,7 +359,9 @@ async function toggle(id: PopId) {
|
|
|
356
359
|
return;
|
|
357
360
|
}
|
|
358
361
|
const r = anchorEls.value[id]?.getBoundingClientRect();
|
|
359
|
-
|
|
362
|
+
// ⚠ RTL: hung from the chip's START edge — its right side in RTL.
|
|
363
|
+
const anchor = r ? inlineStartX(r, dir.value) : 8;
|
|
364
|
+
pos.value = { x: anchor, y: r ? r.bottom + 6 : 8 };
|
|
360
365
|
openPop.value = id;
|
|
361
366
|
await nextTick();
|
|
362
367
|
// Keep it on screen: a chip near the right edge would otherwise open a panel
|
|
@@ -364,9 +369,10 @@ async function toggle(id: PopId) {
|
|
|
364
369
|
const panel = panelEl.value;
|
|
365
370
|
if (panel) {
|
|
366
371
|
const box = panel.getBoundingClientRect();
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
372
|
+
/* ⚠ RTL: it runs toward the inline END of the chip (leftward in RTL) and is
|
|
373
|
+
pushed back from that edge — lib/direction; LTR is the arithmetic this
|
|
374
|
+
block always did. */
|
|
375
|
+
pos.value = { ...pos.value, x: clampAlongInline(anchor, box.width, window.innerWidth, dir.value) };
|
|
370
376
|
if (box.bottom > window.innerHeight - 8) {
|
|
371
377
|
const r2 = anchorEls.value[id]?.getBoundingClientRect();
|
|
372
378
|
pos.value = { ...pos.value, y: Math.max(8, (r2 ? r2.top : 0) - box.height - 6) };
|
|
@@ -675,6 +681,7 @@ onBeforeUnmount(() => {
|
|
|
675
681
|
v-if="openPop"
|
|
676
682
|
ref="panelEl"
|
|
677
683
|
class="fe-filterpop"
|
|
684
|
+
:dir="dir"
|
|
678
685
|
:class="{
|
|
679
686
|
'fe--theme-light': theme === 'light',
|
|
680
687
|
'fe--theme-dark': theme === 'dark',
|
|
@@ -13,6 +13,8 @@ import { hasInternalDrag } from '../lib/dragOut';
|
|
|
13
13
|
import type { FileNode } from '../types/FileNode';
|
|
14
14
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
15
15
|
import { useLocale } from '../composables/useLocale';
|
|
16
|
+
import { lockOf, lockWords } from '../lib/appLock';
|
|
17
|
+
import { linkWordsFor } from '../lib/symlink'; /* issue #34 — a link that will not open */
|
|
16
18
|
import { checkMod, clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
|
|
17
19
|
import ItemCheck from './ItemCheck.vue';
|
|
18
20
|
import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
groupingActive,
|
|
31
33
|
} from '../lib/dateGroups'; /* gruplama */
|
|
32
34
|
import { useSortStore, type ListingOrder } from '../lib/sortOrder'; /* gruplama */
|
|
35
|
+
import { contentDir } from '../lib/direction';
|
|
33
36
|
|
|
34
37
|
const props = withDefaults(defineProps<{
|
|
35
38
|
files: FileNode[];
|
|
@@ -98,6 +101,25 @@ const emit = defineEmits<{
|
|
|
98
101
|
const { t, formatSize, nodeDisplayName, formatDate, formatMonthYear, zonedYearMonth, toDate } =
|
|
99
102
|
useLocale(() => props.locale);
|
|
100
103
|
|
|
104
|
+
/* App plugins — an app's hold on a row (docs/APP-PLUGINS-API.md → "File
|
|
105
|
+
locks"). The listing already caps `perm` at viewer, so the write verbs are
|
|
106
|
+
gone without this; the badge is what stops that reading as a bug. ⚠ One
|
|
107
|
+
source of words for every view and the details panel (lib/appLock). */
|
|
108
|
+
function lockTitleOf(n: FileNode): string {
|
|
109
|
+
return lockWords(lockOf(n), { t, formatDate, locale: props.locale });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* issue #34 — a symlink the server will not follow. Same shape as the lock
|
|
113
|
+
badge above and for the same reason: the words live in ONE module
|
|
114
|
+
(lib/symlink), so the row, the details panel and the toast that explains a
|
|
115
|
+
refused open cannot drift apart. `null` for every ordinary row — a link
|
|
116
|
+
whose target is inside the root was already followed and arrives as that
|
|
117
|
+
target, so it is not one of these. */
|
|
118
|
+
function linkOf(n: FileNode) {
|
|
119
|
+
return linkWordsFor(n, { t });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
101
123
|
/* gruplama — this pane's sort, by injection, so the gallery cannot disagree
|
|
102
124
|
* with the list beside it about whether a date heading is honest right now. */
|
|
103
125
|
const sort = useSortStore();
|
|
@@ -305,6 +327,7 @@ function metaFor(n: FileNode): string {
|
|
|
305
327
|
v-for="p in previewFor(n)"
|
|
306
328
|
:key="'fprev'"
|
|
307
329
|
class="fe-fprev"
|
|
330
|
+
:dir="contentDir(p.kind)"
|
|
308
331
|
:class="'fe-fprev--' + p.kind"
|
|
309
332
|
:style="p.kind === 'table' ? { '--fprev-cols': String(p.cols) } : undefined"
|
|
310
333
|
aria-hidden="true"
|
|
@@ -401,11 +424,36 @@ function metaFor(n: FileNode): string {
|
|
|
401
424
|
v-if="showParentPath && parentDirOf(n.path)"
|
|
402
425
|
class="fe-gal__meta-line fe-gal__meta-line--path"
|
|
403
426
|
:title="parentDirOf(n.path)"
|
|
404
|
-
>{{ parentDirOf(n.path) }}</span>
|
|
427
|
+
><bdi>{{ parentDirOf(n.path) }}</bdi></span>
|
|
405
428
|
</div>
|
|
406
429
|
</div>
|
|
407
430
|
<div class="fe-gal__label" :title="n.basename">
|
|
408
|
-
{{ nodeDisplayName(n) }}
|
|
431
|
+
<bdi>{{ nodeDisplayName(n) }}</bdi>
|
|
432
|
+
</div>
|
|
433
|
+
<!-- issue #34 — a symlink the server will not follow. Its own line: a
|
|
434
|
+
gallery card has no caption row to hang it off, and a badge that
|
|
435
|
+
only appeared in two of the three views is how the same fix reaches
|
|
436
|
+
some people and not others. -->
|
|
437
|
+
<div v-if="linkOf(n)" class="fe-gal__meta-line">
|
|
438
|
+
<span
|
|
439
|
+
class="fe-symlink"
|
|
440
|
+
:class="'fe-symlink--' + linkOf(n)!.state"
|
|
441
|
+
role="img"
|
|
442
|
+
:title="linkOf(n)!.why"
|
|
443
|
+
:aria-label="linkOf(n)!.why"
|
|
444
|
+
data-testid="symlink-badge"
|
|
445
|
+
:data-link-state="linkOf(n)!.state"
|
|
446
|
+
><span class="fe-symlink__glyph" aria-hidden="true">🔗</span>{{ linkOf(n)!.badge }}</span>
|
|
447
|
+
</div>
|
|
448
|
+
<div v-if="lockTitleOf(n)" class="fe-gal__meta-line">
|
|
449
|
+
<span
|
|
450
|
+
v-if="lockTitleOf(n)"
|
|
451
|
+
class="fe-applock"
|
|
452
|
+
role="img"
|
|
453
|
+
:title="lockTitleOf(n)"
|
|
454
|
+
:aria-label="lockTitleOf(n)"
|
|
455
|
+
data-testid="lock-badge"
|
|
456
|
+
><span class="fe-applock__glyph" aria-hidden="true">🔒</span>{{ t('applock.badge') }}</span>
|
|
409
457
|
</div>
|
|
410
458
|
</div>
|
|
411
459
|
</template>
|
|
@@ -20,6 +20,8 @@ import type { FileNode } from '../types/FileNode';
|
|
|
20
20
|
import { hasInternalDrag } from '../lib/dragOut';
|
|
21
21
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
22
22
|
import { useLocale } from '../composables/useLocale';
|
|
23
|
+
import { lockOf, lockWords } from '../lib/appLock';
|
|
24
|
+
import { linkWordsFor } from '../lib/symlink'; /* issue #34 — a link that will not open */
|
|
23
25
|
import { checkMod, clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
|
|
24
26
|
import ItemCheck from './ItemCheck.vue';
|
|
25
27
|
import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
|
|
@@ -38,6 +40,7 @@ import { useSortStore, type ListingOrder } from '../lib/sortOrder'; /* gruplama
|
|
|
38
40
|
import StarButton from './StarButton.vue';
|
|
39
41
|
import { snippetSegments } from '../lib/snippet'; /* bul:s3 */
|
|
40
42
|
import { applyDragGhost } from '../lib/dragGhost'; /* wiring:c4 */
|
|
43
|
+
import { contentDir } from '../lib/direction';
|
|
41
44
|
|
|
42
45
|
const props = withDefaults(defineProps<{
|
|
43
46
|
files: FileNode[];
|
|
@@ -152,6 +155,25 @@ const emit = defineEmits<{
|
|
|
152
155
|
const { t, formatSize, nodeDisplayName, formatDate, formatMonthYear, zonedYearMonth, toDate } =
|
|
153
156
|
useLocale(() => props.locale);
|
|
154
157
|
|
|
158
|
+
/* App plugins — an app's hold on a row (docs/APP-PLUGINS-API.md → "File
|
|
159
|
+
locks"). The listing already caps `perm` at viewer, so the write verbs are
|
|
160
|
+
gone without this; the badge is what stops that reading as a bug. ⚠ One
|
|
161
|
+
source of words for every view and the details panel (lib/appLock). */
|
|
162
|
+
function lockTitleOf(n: FileNode): string {
|
|
163
|
+
return lockWords(lockOf(n), { t, formatDate, locale: props.locale });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* issue #34 — a symlink the server will not follow. Same shape as the lock
|
|
167
|
+
badge above and for the same reason: the words live in ONE module
|
|
168
|
+
(lib/symlink), so the row, the details panel and the toast that explains a
|
|
169
|
+
refused open cannot drift apart. `null` for every ordinary row — a link
|
|
170
|
+
whose target is inside the root was already followed and arrives as that
|
|
171
|
+
target, so it is not one of these. */
|
|
172
|
+
function linkOf(n: FileNode) {
|
|
173
|
+
return linkWordsFor(n, { t });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
155
177
|
const ordered = computed<FileNode[]>(() => [...props.files].sort(byFoldersFirst));
|
|
156
178
|
watch(
|
|
157
179
|
ordered,
|
|
@@ -459,6 +481,7 @@ function snippetTitle(snippet: string): string {
|
|
|
459
481
|
v-for="p in previewFor(n)"
|
|
460
482
|
:key="'fprev'"
|
|
461
483
|
class="fe-fprev"
|
|
484
|
+
:dir="contentDir(p.kind)"
|
|
462
485
|
:class="'fe-fprev--' + p.kind"
|
|
463
486
|
:style="p.kind === 'table' ? { '--fprev-cols': String(p.cols) } : undefined"
|
|
464
487
|
aria-hidden="true"
|
|
@@ -576,7 +599,7 @@ function snippetTitle(snippet: string): string {
|
|
|
576
599
|
</span>
|
|
577
600
|
<div class="fe-grid__main">
|
|
578
601
|
<div class="fe-grid__label" :title="n.basename">
|
|
579
|
-
{{ nodeDisplayName(n) }}
|
|
602
|
+
<bdi>{{ nodeDisplayName(n) }}</bdi>
|
|
580
603
|
<span
|
|
581
604
|
v-if="keepBadgeFor && keepBadgeFor(n)"
|
|
582
605
|
:class="['fe-keepbadge', 'fe-keepbadge--' + keepBadgeFor(n)]"
|
|
@@ -585,7 +608,32 @@ function snippetTitle(snippet: string): string {
|
|
|
585
608
|
:aria-label="t('keep.badge_' + keepBadgeFor(n))"
|
|
586
609
|
>{{ keepGlyph(keepBadgeFor(n)!) }}</span>
|
|
587
610
|
</div>
|
|
588
|
-
|
|
611
|
+
<!-- The whole caption on hover: the card has room for "110 B • Sep 22,
|
|
612
|
+
2026" and not for every language's date (German "22. Sept. 2026"
|
|
613
|
+
lost its year to the ellipsis, v0.43.0 translator pass). -->
|
|
614
|
+
<div class="fe-grid__meta" :title="captionFor(n)">
|
|
615
|
+
<span
|
|
616
|
+
v-if="lockTitleOf(n)"
|
|
617
|
+
class="fe-applock"
|
|
618
|
+
role="img"
|
|
619
|
+
:title="lockTitleOf(n)"
|
|
620
|
+
:aria-label="lockTitleOf(n)"
|
|
621
|
+
data-testid="lock-badge"
|
|
622
|
+
><span class="fe-applock__glyph" aria-hidden="true">🔒</span>{{ t('applock.badge') }}</span>
|
|
623
|
+
<!-- issue #34 — a symlink the server will not follow. Same badge,
|
|
624
|
+
same words, same module as the list and the gallery. -->
|
|
625
|
+
<span
|
|
626
|
+
v-if="linkOf(n)"
|
|
627
|
+
class="fe-symlink"
|
|
628
|
+
:class="'fe-symlink--' + linkOf(n)!.state"
|
|
629
|
+
role="img"
|
|
630
|
+
:title="linkOf(n)!.why"
|
|
631
|
+
:aria-label="linkOf(n)!.why"
|
|
632
|
+
data-testid="symlink-badge"
|
|
633
|
+
:data-link-state="linkOf(n)!.state"
|
|
634
|
+
><span class="fe-symlink__glyph" aria-hidden="true">🔗</span>{{ linkOf(n)!.badge }}</span>
|
|
635
|
+
{{ captionFor(n) }}
|
|
636
|
+
</div>
|
|
589
637
|
<!-- A card at a storage's root has no parent folder to name: no line,
|
|
590
638
|
not an em dash — the same rule the gallery follows and the list's
|
|
591
639
|
Location cell (tablo:t1). The dash read as a stray character under
|
|
@@ -594,7 +642,7 @@ function snippetTitle(snippet: string): string {
|
|
|
594
642
|
v-if="showParentPath && parentDirOf(n.path)"
|
|
595
643
|
class="fe-grid__parent"
|
|
596
644
|
:title="parentDirOf(n.path)"
|
|
597
|
-
>{{ parentDirOf(n.path) }}</div>
|
|
645
|
+
><bdi>{{ parentDirOf(n.path) }}</bdi></div>
|
|
598
646
|
<!-- bul:s3 — content snippet («» → <mark> via TEXT segments, no innerHTML) -->
|
|
599
647
|
<div v-if="cardSnippet(n)" class="fe-grid__snippet" :title="snippetTitle(cardSnippet(n))">
|
|
600
648
|
<template v-for="(seg, si) in snippetSegments(cardSnippet(n))" :key="si">
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* hash does not match in the web-component build, so the rules would silently
|
|
31
31
|
* stop applying in every embed.
|
|
32
32
|
*/
|
|
33
|
+
import StorageTags from './StorageTags.vue';
|
|
33
34
|
import { computed, ref } from 'vue';
|
|
34
35
|
import type { FileNode } from '../types/FileNode';
|
|
35
36
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
@@ -144,12 +145,9 @@ const storageTile = fileIconTile({ type: 'dir', mime_type: 'inode/storage' });
|
|
|
144
145
|
* with the wrong size line says something false.
|
|
145
146
|
*/
|
|
146
147
|
function storageCaption(s: HomeStorage): string {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
: t('conn.guide.storage');
|
|
151
|
-
// A read-only mount says so on its card too (issue #30).
|
|
152
|
-
return s.readOnly ? `${base} · ${t('sidenav.storage.readOnly')}` : base;
|
|
148
|
+
return typeof s.usedBytes === 'number' && s.usedBytes >= 0
|
|
149
|
+
? t('drive.storage.used_unlimited', { used: formatSize(s.usedBytes) })
|
|
150
|
+
: t('conn.guide.storage');
|
|
153
151
|
}
|
|
154
152
|
|
|
155
153
|
/**
|
|
@@ -202,6 +200,20 @@ function openNode(n: FileNode) {
|
|
|
202
200
|
<span class="fe-home__storage-main">
|
|
203
201
|
<span class="fe-home__storage-label">{{ s.label || s.name }}</span>
|
|
204
202
|
<span class="fe-home__storage-meta">{{ storageCaption(s) }}</span>
|
|
203
|
+
<!-- A read-only mount says so on its card too (issue #30) — on a
|
|
204
|
+
line of its own, the panel's own mark (StorageTags, the one
|
|
205
|
+
read-only tag of every surface). ⚠ It used to be glued to
|
|
206
|
+
the size ("81 GB belegt · Nur lesen") inside a fixed 124 px
|
|
207
|
+
caption, and in German the words that mattered were the ones
|
|
208
|
+
the ellipsis ate (translator measurement, v0.43.0). A tag of
|
|
209
|
+
its own fits any language that fits the panel's. -->
|
|
210
|
+
<StorageTags
|
|
211
|
+
v-if="s.readOnly"
|
|
212
|
+
read-only
|
|
213
|
+
:locale="locale"
|
|
214
|
+
class="fe-home__storage-ro"
|
|
215
|
+
data-testid="home-storage-readonly"
|
|
216
|
+
/>
|
|
205
217
|
</span>
|
|
206
218
|
</button>
|
|
207
219
|
</div>
|
|
@@ -26,11 +26,18 @@
|
|
|
26
26
|
import { computed, ref, watch } from 'vue';
|
|
27
27
|
import type { FileApi, Grant, NodeVersion } from '../composables/useFileApi';
|
|
28
28
|
import type { FileNode, ShareInfo } from '../types/FileNode';
|
|
29
|
-
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
29
|
+
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
30
|
+
import type { PluginViewRow } from '../types/Plugins';
|
|
31
|
+
import { localeTag, useLocale } from '../composables/useLocale';
|
|
32
|
+
import { fileIconTile, isStorageRow, typeLabelFor } from '../lib/fileIcons';
|
|
32
33
|
import { actionIconSvg } from '../lib/actionIcons';
|
|
34
|
+
import { appliesToNodes } from '../lib/pluginApplies';
|
|
35
|
+
import { lockedRefusal, lockOf, lockWords } from '../lib/appLock';
|
|
36
|
+
import { linkWordsFor } from '../lib/symlink'; /* issue #34 */
|
|
37
|
+
import { personInitial as initialOfPerson, personName as nameOfPerson } from '../lib/personName';
|
|
33
38
|
import TagPicker from './TagPicker.vue';
|
|
39
|
+
import type { TagKind } from '../lib/tags';
|
|
40
|
+
import PluginInspectorSection from './plugin/PluginInspectorSection.vue';
|
|
34
41
|
|
|
35
42
|
const props = withDefaults(
|
|
36
43
|
defineProps<{
|
|
@@ -59,6 +66,16 @@ const props = withDefaults(
|
|
|
59
66
|
locale: LocaleCode;
|
|
60
67
|
/** Narrow/embed mode → full-size overlay presentation. */
|
|
61
68
|
narrow?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Is the reader an administrator (`capabilities.caller_admin`)?
|
|
71
|
+
*
|
|
72
|
+
* ⚠ Only the NODE ID row depends on it (Burak, 2026-09-23). The id is a
|
|
73
|
+
* support handle — what an administrator quotes in Admin → File history or
|
|
74
|
+
* an audit row — and it means nothing to anybody else; on a shared surface
|
|
75
|
+
* it was a number in front of every reader. Absent = not an administrator,
|
|
76
|
+
* which is the safe answer for an embed that never asks the server.
|
|
77
|
+
*/
|
|
78
|
+
callerAdmin?: boolean;
|
|
62
79
|
/** Authenticated thumbnail resolver (useThumbs.src). Optional. */
|
|
63
80
|
thumbSrc?: (n: FileNode) => string | null;
|
|
64
81
|
/* === surucu:d1 — the Drive shell's details panel ===================== */
|
|
@@ -96,6 +113,17 @@ const props = withDefaults(
|
|
|
96
113
|
apiBase?: string;
|
|
97
114
|
authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
98
115
|
authCredentials?: RequestCredentials;
|
|
116
|
+
/* === App plugins (docs/APP-PLUGINS-API.md → Placements, `inspector`) ===
|
|
117
|
+
* The `views[]` rows with placement `inspector`, as the host fetched them
|
|
118
|
+
* (`usePluginActions.views`); `[]` while the feature is off. One collapsible
|
|
119
|
+
* section per row whose `applies` rule accepts the selected SINGLE item —
|
|
120
|
+
* the client-side mirror the menu uses (lib/pluginApplies), re-checked by
|
|
121
|
+
* the server when the view loads. */
|
|
122
|
+
pluginViews?: PluginViewRow[];
|
|
123
|
+
/** Resolved theme, for the dialogs a plugin surface can open. */
|
|
124
|
+
theme?: ThemeMode;
|
|
125
|
+
/** Storage names a surface's file-chooser may span. */
|
|
126
|
+
storages?: string[];
|
|
99
127
|
}>(),
|
|
100
128
|
{
|
|
101
129
|
/* ⚠ ON by default, and that is the owner's call of 2026-09-12, not a
|
|
@@ -111,6 +139,7 @@ const props = withDefaults(
|
|
|
111
139
|
narrow: false,
|
|
112
140
|
dirPerm: '',
|
|
113
141
|
thumbSrc: undefined,
|
|
142
|
+
pluginViews: () => [],
|
|
114
143
|
},
|
|
115
144
|
);
|
|
116
145
|
|
|
@@ -141,7 +170,16 @@ const emit = defineEmits<{
|
|
|
141
170
|
* is a `.tag~<name>` sentinel, how to leave whatever view is on screen to get
|
|
142
171
|
* there, and what that does to the tab strip.
|
|
143
172
|
*/
|
|
144
|
-
(e: 'open-tag', tag: string): void;
|
|
173
|
+
(e: 'open-tag', tag: string, kind: TagKind): void;
|
|
174
|
+
/** App plugins — an inspector view's event enqueued a job: the raw ops row. */
|
|
175
|
+
(e: 'plugin-op', op: Record<string, unknown>): void;
|
|
176
|
+
/**
|
|
177
|
+
* v3 §3.0 — an app's inspector screen answered "go to this file".
|
|
178
|
+
*
|
|
179
|
+
* ⚠ Carried UP with the plugin's name rather than acted on: the panel
|
|
180
|
+
* cannot navigate, and the explorer that can is the one holding it.
|
|
181
|
+
*/
|
|
182
|
+
(e: 'plugin-open', payload: { plugin: string; req: { path: string; action?: string; view?: string } }): void;
|
|
145
183
|
}>();
|
|
146
184
|
|
|
147
185
|
const { t, formatSize, formatDate: formatDateOf, nodeDisplayName } = useLocale(
|
|
@@ -153,6 +191,17 @@ const single = computed<FileNode | null>(() =>
|
|
|
153
191
|
props.nodes.length === 1 ? props.nodes[0] : null,
|
|
154
192
|
);
|
|
155
193
|
const isMulti = computed(() => props.nodes.length > 1);
|
|
194
|
+
|
|
195
|
+
/* App plugins — the app holding this file, in the same words the row's badge
|
|
196
|
+
* uses (lib/appLock). ⚠ It goes ABOVE the facts, not among them: the panel's
|
|
197
|
+
* "Permission: viewer" line is TRUE and misleading on its own, because the
|
|
198
|
+
* reason is not a permission the owner can change. */
|
|
199
|
+
const lockLine = computed(() => lockWords(lockOf(single.value), { t, formatDate: formatDateOf, locale: props.locale }));
|
|
200
|
+
/* issue #34 — a symlink the server will not follow. It goes in the SAME place
|
|
201
|
+
* and for the same reason as the lock above: this panel's "Size: 0 bytes" and
|
|
202
|
+
* "Type: file" are both true and both misleading on their own, and the reason
|
|
203
|
+
* is not something the person can read off any of the facts below. */
|
|
204
|
+
const linkNote = computed(() => linkWordsFor(single.value, { t }));
|
|
156
205
|
const isFile = computed(() => single.value?.type === 'file');
|
|
157
206
|
const nodeId = computed<number | null>(() =>
|
|
158
207
|
typeof single.value?.id === 'number' ? (single.value.id as number) : null,
|
|
@@ -168,6 +217,15 @@ const thumb = computed<string | null>(() =>
|
|
|
168
217
|
single.value && isFile.value && props.thumbSrc ? props.thumbSrc(single.value) : null,
|
|
169
218
|
);
|
|
170
219
|
|
|
220
|
+
/** App plugins — the inspector views whose rule accepts the selected item. */
|
|
221
|
+
const pluginSections = computed<PluginViewRow[]>(() => {
|
|
222
|
+
const n = single.value;
|
|
223
|
+
if (!n || isStorageRow(n) || n.trashed === true) return [];
|
|
224
|
+
return (props.pluginViews ?? []).filter(
|
|
225
|
+
(v) => v.placement === 'inspector' && appliesToNodes(v.applies, [n], v.plugin),
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
171
229
|
/* === gorunum:v4-dialogs — what the head says =============================
|
|
172
230
|
*
|
|
173
231
|
* The panel used to open with the word "Details" and put the item's own name
|
|
@@ -289,9 +347,15 @@ const effectivePerm = computed<string>(() => {
|
|
|
289
347
|
if (typeof own === 'string' && own !== '') return own;
|
|
290
348
|
return props.dirPerm || '';
|
|
291
349
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
350
|
+
/**
|
|
351
|
+
* "Manage permissions" — for an OWNER only.
|
|
352
|
+
*
|
|
353
|
+
* ⚠ It used to be offered to editors too, and an editor cannot read the grant
|
|
354
|
+
* list at all (`GET /api/files/permissions` answers 403): the button opened a
|
|
355
|
+
* dialog with no permissions in it, and the refusal was swallowed (QA,
|
|
356
|
+
* 2026-09-21). An editor shares from the row's "Share" like everybody else.
|
|
357
|
+
*/
|
|
358
|
+
const canManagePerms = computed(() => effectivePerm.value === 'owner');
|
|
295
359
|
function permLabel(level: string): string {
|
|
296
360
|
return t(`inspector.perm.${level}`) === `inspector.perm.${level}`
|
|
297
361
|
? level
|
|
@@ -383,7 +447,11 @@ async function confirmRestore(v: NodeVersion): Promise<void> {
|
|
|
383
447
|
emit('changed');
|
|
384
448
|
await loadVersions(++refreshSeq);
|
|
385
449
|
} catch (err) {
|
|
386
|
-
|
|
450
|
+
// ⚠ A 423 is an app's freeze (the signing app, while signatures are
|
|
451
|
+
// collected): say which app and why, in the reader's language — the
|
|
452
|
+
// server's own text is English and names a path.
|
|
453
|
+
const held = lockedRefusal(err);
|
|
454
|
+
emit('toast', held ? lockWords(held, { t, formatDate: formatDateOf, locale: props.locale }) : (err as Error).message);
|
|
387
455
|
} finally {
|
|
388
456
|
versionBusy.value = false;
|
|
389
457
|
}
|
|
@@ -460,13 +528,17 @@ async function loadPeople(): Promise<void> {
|
|
|
460
528
|
}
|
|
461
529
|
}
|
|
462
530
|
|
|
531
|
+
/* One rule for naming a person (lib/personName). */
|
|
532
|
+
function grantPerson(g: Grant) {
|
|
533
|
+
return { name: g.user_name, display_name: g.user_display_name, email: g.user_email };
|
|
534
|
+
}
|
|
535
|
+
|
|
463
536
|
function personName(g: Grant): string {
|
|
464
|
-
return g
|
|
537
|
+
return nameOfPerson(grantPerson(g)) || `#${g.user_id}`;
|
|
465
538
|
}
|
|
466
539
|
|
|
467
540
|
function personInitial(g: Grant): string {
|
|
468
|
-
|
|
469
|
-
return n ? n[0].toUpperCase() : '?';
|
|
541
|
+
return initialOfPerson(grantPerson(g), localeTag(props.locale)) || '?';
|
|
470
542
|
}
|
|
471
543
|
|
|
472
544
|
/** The share-link row: the first live link, or the Create button. */
|
|
@@ -602,7 +674,7 @@ function relativeTime(iso: string): string {
|
|
|
602
674
|
];
|
|
603
675
|
try {
|
|
604
676
|
const rtf = new Intl.RelativeTimeFormat(
|
|
605
|
-
props.locale
|
|
677
|
+
localeTag(props.locale),
|
|
606
678
|
{ numeric: 'auto' },
|
|
607
679
|
);
|
|
608
680
|
for (const [unit, secs] of units) {
|
|
@@ -640,7 +712,7 @@ watch(
|
|
|
640
712
|
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
|
|
641
713
|
<span class="fe-inspector__head-icon" aria-hidden="true" v-html="headIcon"></span>
|
|
642
714
|
<div class="fe-inspector__head-text">
|
|
643
|
-
<h2 class="fe-inspector__title" :title="headName">{{ headName }}</h2>
|
|
715
|
+
<h2 class="fe-inspector__title" :title="headName"><bdi>{{ headName }}</bdi></h2>
|
|
644
716
|
<p class="fe-inspector__caption">{{ headCaption }}</p>
|
|
645
717
|
<!-- pane:p1 — the held-subject line. Drawn ONLY while this panel is
|
|
646
718
|
describing the last selected thing rather than a live selection,
|
|
@@ -703,6 +775,24 @@ watch(
|
|
|
703
775
|
<section v-if="!tabs || tab === 'details'" class="fe-inspector__section">
|
|
704
776
|
<h3 class="fe-inspector__heading">{{ t('inspector.section.general') }}</h3>
|
|
705
777
|
|
|
778
|
+
<!-- App plugins — an app is holding this file read-only. -->
|
|
779
|
+
<p v-if="lockLine" class="fe-inspector__lock" role="note" data-testid="inspector-lock">
|
|
780
|
+
<span aria-hidden="true">🔒</span>
|
|
781
|
+
<span><strong>{{ t('applock.inspector') }}</strong><br />{{ lockLine }}</span>
|
|
782
|
+
</p>
|
|
783
|
+
|
|
784
|
+
<!-- issue #34 — a link the server will not follow. -->
|
|
785
|
+
<p
|
|
786
|
+
v-if="linkNote"
|
|
787
|
+
class="fe-inspector__linknote"
|
|
788
|
+
role="note"
|
|
789
|
+
data-testid="inspector-symlink"
|
|
790
|
+
:data-link-state="linkNote.state"
|
|
791
|
+
>
|
|
792
|
+
<span aria-hidden="true">🔗</span>
|
|
793
|
+
<span><strong>{{ t('symlink.inspector') }}</strong><br />{{ linkNote.why }}</span>
|
|
794
|
+
</p>
|
|
795
|
+
|
|
706
796
|
<!-- Multi selection → summary. The head already counts them, so this
|
|
707
797
|
is the one fact the head does not carry. -->
|
|
708
798
|
<dl v-if="isMulti" class="fe-inspector__meta">
|
|
@@ -740,7 +830,7 @@ watch(
|
|
|
740
830
|
<div class="fe-inspector__row">
|
|
741
831
|
<dt>{{ t('inspector.path') }}</dt>
|
|
742
832
|
<dd class="fe-inspector__pathcell">
|
|
743
|
-
<span class="fe-inspector__path" :title="single.path">{{ single.path }}</span>
|
|
833
|
+
<span class="fe-inspector__path" :title="single.path"><bdi>{{ single.path }}</bdi></span>
|
|
744
834
|
<button
|
|
745
835
|
type="button"
|
|
746
836
|
class="fe-inspector__copy"
|
|
@@ -772,8 +862,14 @@ watch(
|
|
|
772
862
|
technical facts about a file already are (Path, MIME, ETag), and
|
|
773
863
|
it is still one click to copy. Drawn only when the row actually
|
|
774
864
|
has one: a client-synthesized row (a multi-storage folder) has
|
|
775
|
-
no backend id, and an empty "ID —" teaches nothing.
|
|
776
|
-
|
|
865
|
+
no backend id, and an empty "ID —" teaches nothing.
|
|
866
|
+
|
|
867
|
+
⚠⚠ And only for an ADMINISTRATOR (Burak, 2026-09-23): the id
|
|
868
|
+
is the handle admin screens take (Admin → File history, an
|
|
869
|
+
audit row), and it says nothing to anyone else. Here, in the
|
|
870
|
+
shared panel, so every surface that draws it — the explorer,
|
|
871
|
+
the Drive shell, an embed — answers the same way. -->
|
|
872
|
+
<div v-if="callerAdmin && typeof single.id === 'number'" class="fe-inspector__row">
|
|
777
873
|
<dt>{{ t('inspector.nodeId') }}</dt>
|
|
778
874
|
<dd class="fe-inspector__pathcell">
|
|
779
875
|
<span class="fe-inspector__path">{{ single.id }}</span>
|
|
@@ -831,11 +927,32 @@ watch(
|
|
|
831
927
|
:auth-headers="authHeaders"
|
|
832
928
|
:auth-credentials="authCredentials"
|
|
833
929
|
@change="(tags: string[]) => emit('tags-changed', tags)"
|
|
834
|
-
@open="(tag: string) => emit('open-tag', tag)
|
|
930
|
+
@open="(tag: string, kind: TagKind) => emit('open-tag', tag, kind) /* etiket:k2 —
|
|
931
|
+
the KIND travels with the name: a file can carry a personal
|
|
932
|
+
and a team tag of one name, and the chip that was clicked
|
|
933
|
+
decides which view opens. */"
|
|
835
934
|
@error="() => emit('toast', t('inspector.error'))"
|
|
836
935
|
/>
|
|
837
936
|
</section>
|
|
838
937
|
|
|
938
|
+
<!-- ══ App plugins — one collapsible section per matching `inspector`
|
|
939
|
+
view. Lazy: nothing is requested until the section is opened. -->
|
|
940
|
+
<template v-if="single && (!tabs || tab === 'details')">
|
|
941
|
+
<PluginInspectorSection
|
|
942
|
+
v-for="v in pluginSections"
|
|
943
|
+
:key="`${v.plugin}/${v.id}`"
|
|
944
|
+
:api="api"
|
|
945
|
+
:view="v"
|
|
946
|
+
:path="single.path"
|
|
947
|
+
:locale="locale"
|
|
948
|
+
:theme="theme"
|
|
949
|
+
:storages="storages"
|
|
950
|
+
@op="(op) => emit('plugin-op', op)"
|
|
951
|
+
@toast="(m) => emit('toast', m)"
|
|
952
|
+
@open="(req) => emit('plugin-open', { plugin: v.plugin, req })"
|
|
953
|
+
/>
|
|
954
|
+
</template>
|
|
955
|
+
|
|
839
956
|
<!-- ══ Sürümler ══ -->
|
|
840
957
|
<section
|
|
841
958
|
v-if="(versionsState === 'ok' || versionsState === 'error') && (!tabs || tab === 'activity')"
|