@brftech/filex-core 0.2.0 → 0.4.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/dist/{ArchiveViewer-DCZ1FZYV.js → ArchiveViewer-Dgvl43gn.js} +2 -2
- package/dist/{ArchiveViewer-DCZ1FZYV.js.map → ArchiveViewer-Dgvl43gn.js.map} +1 -1
- package/dist/{CsvViewer-DMvnp82x.js → CsvViewer-gWO5pc8F.js} +2 -2
- package/dist/{CsvViewer-DMvnp82x.js.map → CsvViewer-gWO5pc8F.js.map} +1 -1
- package/dist/{DrawioViewer-B5Io0yTO.js → DrawioViewer-DUAlRIAb.js} +2 -2
- package/dist/{DrawioViewer-B5Io0yTO.js.map → DrawioViewer-DUAlRIAb.js.map} +1 -1
- package/dist/{EpubViewer-z6RZd3K8.js → EpubViewer-DIXzuQWK.js} +2 -2
- package/dist/{EpubViewer-z6RZd3K8.js.map → EpubViewer-DIXzuQWK.js.map} +1 -1
- package/dist/{IpynbViewer-DfL4EauH.js → IpynbViewer-CJ-6pU_Z.js} +2 -2
- package/dist/{IpynbViewer-DfL4EauH.js.map → IpynbViewer-CJ-6pU_Z.js.map} +1 -1
- package/dist/{MermaidViewer-QNl_o7V-.js → MermaidViewer-CyQQDyRb.js} +2 -2
- package/dist/{MermaidViewer-QNl_o7V-.js.map → MermaidViewer-CyQQDyRb.js.map} +1 -1
- package/dist/{PsdViewer-Btmpr0Fz.js → PsdViewer-ChfaXVgN.js} +2 -2
- package/dist/{PsdViewer-Btmpr0Fz.js.map → PsdViewer-ChfaXVgN.js.map} +1 -1
- package/dist/{TiffViewer-CMLpwds5.js → TiffViewer-CsfD-NG6.js} +2 -2
- package/dist/{TiffViewer-CMLpwds5.js.map → TiffViewer-CsfD-NG6.js.map} +1 -1
- package/dist/{Viewer3D-gf3cb2gv.js → Viewer3D-BQ2XYmkN.js} +2 -2
- package/dist/{Viewer3D-gf3cb2gv.js.map → Viewer3D-BQ2XYmkN.js.map} +1 -1
- package/dist/filex-core.js +1 -1
- package/dist/filex-core.umd.cjs +42 -42
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-PnhGunSA.js +7972 -0
- package/dist/index-PnhGunSA.js.map +1 -0
- package/dist/index.d.ts +69 -2
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +146 -0
- package/src/components/ContextMenu.vue +101 -3
- package/src/components/InspectorPanel.vue +455 -0
- package/src/components/Toolbar.vue +250 -5
- package/src/composables/useFileApi.ts +59 -0
- package/src/composables/useKeyboardShortcuts.ts +11 -0
- package/src/locales/en.ts +45 -0
- package/src/locales/tr.ts +45 -0
- package/src/styles/base.css +458 -0
- package/dist/index-D49iw00E.js +0 -7119
- package/dist/index-D49iw00E.js.map +0 -1
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* InspectorPanel — koru:k1 details ("Ayrıntılar") side panel.
|
|
4
|
+
*
|
|
5
|
+
* Rendered as a flex sibling of `.fe__body` (right side, ~300px). In
|
|
6
|
+
* `fe--narrow` embeds it becomes a full-size overlay with its own close
|
|
7
|
+
* button (Esc handled by FileExplorer's shortcut chain).
|
|
8
|
+
*
|
|
9
|
+
* Sections:
|
|
10
|
+
* - Genel : icon/thumb, name, path (copy), size, modified, mime, etag.
|
|
11
|
+
* Multi-select → "N items, total X" summary; no selection →
|
|
12
|
+
* current-folder summary.
|
|
13
|
+
* - Sürümler : version history via GET /api/files/versions?node_id=…
|
|
14
|
+
* + restore (with optional snapshot-current) + snapshot-now.
|
|
15
|
+
* The section hides silently when the backend gates the
|
|
16
|
+
* endpoint (401/403) or doesn't ship it (404).
|
|
17
|
+
* - İzinler : effective RBAC level badge + "manage" (opens the existing
|
|
18
|
+
* PermissionsModal through the host). Only when ACL is
|
|
19
|
+
* enforced on the storage (perm is a non-empty string).
|
|
20
|
+
* - Paylaşımlar: existing share links (GET /api/files/share?path=…) with
|
|
21
|
+
* copy buttons. Hidden silently when the list call fails
|
|
22
|
+
* (viewer-level users get 403 by design).
|
|
23
|
+
*
|
|
24
|
+
* The panel is mounted with v-if by the host — closed state leaves zero DOM.
|
|
25
|
+
*/
|
|
26
|
+
import { computed, ref, watch } from 'vue';
|
|
27
|
+
import type { FileApi, NodeVersion } from '../composables/useFileApi';
|
|
28
|
+
import type { FileNode, ShareInfo } from '../types/FileNode';
|
|
29
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
30
|
+
import { useLocale } from '../composables/useLocale';
|
|
31
|
+
import { fileIconSvg } from '../lib/fileIcons';
|
|
32
|
+
|
|
33
|
+
const props = defineProps<{
|
|
34
|
+
api: FileApi;
|
|
35
|
+
/** Current selection (empty array → current-folder summary). */
|
|
36
|
+
nodes: FileNode[];
|
|
37
|
+
/** Display label of the folder being viewed (for the no-selection state). */
|
|
38
|
+
dirLabel: string;
|
|
39
|
+
/** Number of entries in the folder being viewed. */
|
|
40
|
+
dirCount: number;
|
|
41
|
+
/** RBAC effective level of the current dir ('' = ACL not enforced). */
|
|
42
|
+
dirPerm?: string;
|
|
43
|
+
locale: LocaleCode;
|
|
44
|
+
/** Narrow/embed mode → full-size overlay presentation. */
|
|
45
|
+
narrow?: boolean;
|
|
46
|
+
/** Authenticated thumbnail resolver (useThumbs.src). Optional. */
|
|
47
|
+
thumbSrc?: (n: FileNode) => string | null;
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
const emit = defineEmits<{
|
|
51
|
+
(e: 'close'): void;
|
|
52
|
+
(e: 'manage-permissions', node: FileNode): void;
|
|
53
|
+
(e: 'toast', message: string): void;
|
|
54
|
+
/** Fired after a successful restore/snapshot so the host can reload. */
|
|
55
|
+
(e: 'changed'): void;
|
|
56
|
+
}>();
|
|
57
|
+
|
|
58
|
+
const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
|
|
59
|
+
|
|
60
|
+
// ── selection shape ──────────────────────────────────────────────────
|
|
61
|
+
const single = computed<FileNode | null>(() =>
|
|
62
|
+
props.nodes.length === 1 ? props.nodes[0] : null,
|
|
63
|
+
);
|
|
64
|
+
const isMulti = computed(() => props.nodes.length > 1);
|
|
65
|
+
const isFile = computed(() => single.value?.type === 'file');
|
|
66
|
+
const nodeId = computed<number | null>(() =>
|
|
67
|
+
typeof single.value?.id === 'number' ? (single.value.id as number) : null,
|
|
68
|
+
);
|
|
69
|
+
const multiTotal = computed(() =>
|
|
70
|
+
props.nodes.reduce((acc, n) => acc + (typeof n.size === 'number' ? n.size : 0), 0),
|
|
71
|
+
);
|
|
72
|
+
const etag = computed<string | null>(() => {
|
|
73
|
+
const v = single.value?.etag;
|
|
74
|
+
return typeof v === 'string' && v !== '' ? v : null;
|
|
75
|
+
});
|
|
76
|
+
const thumb = computed<string | null>(() =>
|
|
77
|
+
single.value && isFile.value && props.thumbSrc ? props.thumbSrc(single.value) : null,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
function shortHash(h: string): string {
|
|
81
|
+
return h.length > 12 ? `${h.slice(0, 12)}…` : h;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatDate(ms: number | undefined): string {
|
|
85
|
+
if (!ms) return '—';
|
|
86
|
+
try {
|
|
87
|
+
return new Date(ms).toLocaleString(props.locale === 'en' ? 'en-GB' : 'tr-TR', {
|
|
88
|
+
dateStyle: 'medium',
|
|
89
|
+
timeStyle: 'short',
|
|
90
|
+
});
|
|
91
|
+
} catch {
|
|
92
|
+
return new Date(ms).toISOString();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatDateStr(s: string | undefined | null): string {
|
|
97
|
+
if (!s) return '—';
|
|
98
|
+
const ms = Date.parse(s);
|
|
99
|
+
return Number.isNaN(ms) ? s : formatDate(ms);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function copyText(text: string): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
await navigator.clipboard.writeText(text);
|
|
105
|
+
emit('toast', t('inspector.copied'));
|
|
106
|
+
} catch {
|
|
107
|
+
emit('toast', text);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── RBAC (İzinler) ───────────────────────────────────────────────────
|
|
112
|
+
// Effective level for the selected item: its own perm, else the dir's.
|
|
113
|
+
// A non-empty string means ACL is enforced → section shows.
|
|
114
|
+
const effectivePerm = computed<string>(() => {
|
|
115
|
+
if (!single.value) return '';
|
|
116
|
+
// Backends may send '' when ACL is off — widen past the declared union.
|
|
117
|
+
const own = single.value.perm as string | undefined;
|
|
118
|
+
if (typeof own === 'string' && own !== '') return own;
|
|
119
|
+
return props.dirPerm || '';
|
|
120
|
+
});
|
|
121
|
+
const canManagePerms = computed(
|
|
122
|
+
() => effectivePerm.value === 'editor' || effectivePerm.value === 'owner',
|
|
123
|
+
);
|
|
124
|
+
function permLabel(level: string): string {
|
|
125
|
+
return t(`inspector.perm.${level}`) === `inspector.perm.${level}`
|
|
126
|
+
? level
|
|
127
|
+
: t(`inspector.perm.${level}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── versions state ───────────────────────────────────────────────────
|
|
131
|
+
type SectionState = 'idle' | 'loading' | 'ok' | 'hidden' | 'error';
|
|
132
|
+
const versionsState = ref<SectionState>('idle');
|
|
133
|
+
const versions = ref<NodeVersion[]>([]);
|
|
134
|
+
const confirmVersionId = ref<number | null>(null);
|
|
135
|
+
const snapshotFirst = ref(true);
|
|
136
|
+
const versionBusy = ref(false);
|
|
137
|
+
|
|
138
|
+
// ── shares state ─────────────────────────────────────────────────────
|
|
139
|
+
const sharesState = ref<SectionState>('idle');
|
|
140
|
+
const shares = ref<ShareInfo[]>([]);
|
|
141
|
+
|
|
142
|
+
// Race guard: only the latest refresh may write state.
|
|
143
|
+
let refreshSeq = 0;
|
|
144
|
+
|
|
145
|
+
async function refresh(): Promise<void> {
|
|
146
|
+
const seq = ++refreshSeq;
|
|
147
|
+
confirmVersionId.value = null;
|
|
148
|
+
versions.value = [];
|
|
149
|
+
shares.value = [];
|
|
150
|
+
|
|
151
|
+
// Versions — single file with a backend node id only.
|
|
152
|
+
if (single.value && isFile.value && nodeId.value != null) {
|
|
153
|
+
versionsState.value = 'loading';
|
|
154
|
+
void loadVersions(seq);
|
|
155
|
+
} else {
|
|
156
|
+
versionsState.value = 'hidden';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Shares — any single selection (files and folders both shareable).
|
|
160
|
+
if (single.value) {
|
|
161
|
+
sharesState.value = 'loading';
|
|
162
|
+
const path = single.value.path;
|
|
163
|
+
try {
|
|
164
|
+
const { shares: list } = await props.api.listShares(path);
|
|
165
|
+
if (seq !== refreshSeq) return;
|
|
166
|
+
shares.value = Array.isArray(list) ? list : [];
|
|
167
|
+
sharesState.value = 'ok';
|
|
168
|
+
} catch {
|
|
169
|
+
if (seq !== refreshSeq) return;
|
|
170
|
+
// 403 = viewer-level user (by design), anything else — auxiliary
|
|
171
|
+
// info, hide silently rather than alarm.
|
|
172
|
+
sharesState.value = 'hidden';
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
sharesState.value = 'hidden';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function loadVersions(seq: number): Promise<void> {
|
|
180
|
+
const id = nodeId.value;
|
|
181
|
+
if (id == null) return;
|
|
182
|
+
try {
|
|
183
|
+
const list = await props.api.listVersions(id);
|
|
184
|
+
if (seq !== refreshSeq) return;
|
|
185
|
+
versions.value = list;
|
|
186
|
+
versionsState.value = 'ok';
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (seq !== refreshSeq) return;
|
|
189
|
+
const status = (err as { status?: number }).status;
|
|
190
|
+
// Gated (401/403) or absent (404) endpoint → section silently hidden.
|
|
191
|
+
if (status === 401 || status === 403 || status === 404) {
|
|
192
|
+
versionsState.value = 'hidden';
|
|
193
|
+
} else {
|
|
194
|
+
versionsState.value = 'error';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function askRestore(v: NodeVersion): void {
|
|
200
|
+
confirmVersionId.value = v.id;
|
|
201
|
+
snapshotFirst.value = true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function confirmRestore(v: NodeVersion): Promise<void> {
|
|
205
|
+
const id = nodeId.value;
|
|
206
|
+
if (id == null || versionBusy.value) return;
|
|
207
|
+
versionBusy.value = true;
|
|
208
|
+
try {
|
|
209
|
+
await props.api.restoreVersion(id, v.id, snapshotFirst.value);
|
|
210
|
+
emit('toast', t('inspector.versions.restored'));
|
|
211
|
+
confirmVersionId.value = null;
|
|
212
|
+
emit('changed');
|
|
213
|
+
await loadVersions(++refreshSeq);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
emit('toast', (err as Error).message);
|
|
216
|
+
} finally {
|
|
217
|
+
versionBusy.value = false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function takeSnapshot(): Promise<void> {
|
|
222
|
+
const id = nodeId.value;
|
|
223
|
+
if (id == null || versionBusy.value) return;
|
|
224
|
+
versionBusy.value = true;
|
|
225
|
+
try {
|
|
226
|
+
await props.api.snapshotVersion(id);
|
|
227
|
+
emit('toast', t('inspector.versions.snapshotted'));
|
|
228
|
+
await loadVersions(++refreshSeq);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
const status = (err as { status?: number }).status;
|
|
231
|
+
// Older backends don't ship POST /versions/snapshot yet.
|
|
232
|
+
if (status === 404 || status === 405 || status === 501) {
|
|
233
|
+
emit('toast', t('inspector.versions.unsupported'));
|
|
234
|
+
} else {
|
|
235
|
+
emit('toast', (err as Error).message);
|
|
236
|
+
}
|
|
237
|
+
} finally {
|
|
238
|
+
versionBusy.value = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
watch(
|
|
243
|
+
() => props.nodes.map((n) => n.path).join(''),
|
|
244
|
+
() => void refresh(),
|
|
245
|
+
{ immediate: true },
|
|
246
|
+
);
|
|
247
|
+
</script>
|
|
248
|
+
|
|
249
|
+
<template>
|
|
250
|
+
<aside
|
|
251
|
+
class="fe-inspector"
|
|
252
|
+
:class="{ 'fe-inspector--overlay': narrow }"
|
|
253
|
+
role="complementary"
|
|
254
|
+
:aria-label="t('inspector.title')"
|
|
255
|
+
>
|
|
256
|
+
<header class="fe-inspector__head">
|
|
257
|
+
<h2 class="fe-inspector__title">{{ t('inspector.title') }}</h2>
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
class="fe-inspector__close"
|
|
261
|
+
:title="t('inspector.close')"
|
|
262
|
+
:aria-label="t('inspector.close')"
|
|
263
|
+
@click="emit('close')"
|
|
264
|
+
>×</button>
|
|
265
|
+
</header>
|
|
266
|
+
|
|
267
|
+
<div class="fe-inspector__scroll">
|
|
268
|
+
<!-- ══ Genel ══ -->
|
|
269
|
+
<section class="fe-inspector__section">
|
|
270
|
+
<h3 class="fe-inspector__heading">{{ t('inspector.section.general') }}</h3>
|
|
271
|
+
|
|
272
|
+
<!-- Multi selection → summary -->
|
|
273
|
+
<div v-if="isMulti" class="fe-inspector__hero">
|
|
274
|
+
<span class="fe-inspector__bigicon" v-html="fileIconSvg({ type: 'dir' })"></span>
|
|
275
|
+
<p class="fe-inspector__name">
|
|
276
|
+
{{ t('inspector.items_summary', { n: nodes.length, size: formatSize(multiTotal) }) }}
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<!-- Single selection → full meta -->
|
|
281
|
+
<template v-else-if="single">
|
|
282
|
+
<div class="fe-inspector__hero">
|
|
283
|
+
<img
|
|
284
|
+
v-if="thumb"
|
|
285
|
+
class="fe-inspector__thumb"
|
|
286
|
+
:src="thumb"
|
|
287
|
+
alt=""
|
|
288
|
+
aria-hidden="true"
|
|
289
|
+
/>
|
|
290
|
+
<span
|
|
291
|
+
v-else
|
|
292
|
+
class="fe-inspector__bigicon"
|
|
293
|
+
v-html="fileIconSvg(single)"
|
|
294
|
+
></span>
|
|
295
|
+
<p class="fe-inspector__name" :title="single.basename">
|
|
296
|
+
{{ nodeDisplayName(single) }}
|
|
297
|
+
</p>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<dl class="fe-inspector__meta">
|
|
301
|
+
<div class="fe-inspector__row">
|
|
302
|
+
<dt>{{ t('inspector.path') }}</dt>
|
|
303
|
+
<dd class="fe-inspector__pathcell">
|
|
304
|
+
<span class="fe-inspector__path" :title="single.path">{{ single.path }}</span>
|
|
305
|
+
<button
|
|
306
|
+
type="button"
|
|
307
|
+
class="fe-inspector__copy"
|
|
308
|
+
:title="t('inspector.copy')"
|
|
309
|
+
:aria-label="t('inspector.copy')"
|
|
310
|
+
@click="copyText(single.path)"
|
|
311
|
+
>⧉</button>
|
|
312
|
+
</dd>
|
|
313
|
+
</div>
|
|
314
|
+
<div v-if="isFile" class="fe-inspector__row">
|
|
315
|
+
<dt>{{ t('inspector.size') }}</dt>
|
|
316
|
+
<dd>{{ formatSize(typeof single.size === 'number' ? single.size : null) }}</dd>
|
|
317
|
+
</div>
|
|
318
|
+
<div class="fe-inspector__row">
|
|
319
|
+
<dt>{{ t('inspector.modified') }}</dt>
|
|
320
|
+
<dd>{{ formatDate(single.last_modified) }}</dd>
|
|
321
|
+
</div>
|
|
322
|
+
<div v-if="isFile && single.mime_type" class="fe-inspector__row">
|
|
323
|
+
<dt>{{ t('inspector.mime') }}</dt>
|
|
324
|
+
<dd class="fe-inspector__mime">{{ single.mime_type }}</dd>
|
|
325
|
+
</div>
|
|
326
|
+
<div v-if="etag" class="fe-inspector__row">
|
|
327
|
+
<dt>{{ t('inspector.etag') }}</dt>
|
|
328
|
+
<dd class="fe-inspector__pathcell">
|
|
329
|
+
<span class="fe-inspector__path" :title="etag">{{ shortHash(etag) }}</span>
|
|
330
|
+
<button
|
|
331
|
+
type="button"
|
|
332
|
+
class="fe-inspector__copy"
|
|
333
|
+
:title="t('inspector.copy')"
|
|
334
|
+
:aria-label="t('inspector.copy')"
|
|
335
|
+
@click="copyText(etag)"
|
|
336
|
+
>⧉</button>
|
|
337
|
+
</dd>
|
|
338
|
+
</div>
|
|
339
|
+
</dl>
|
|
340
|
+
</template>
|
|
341
|
+
|
|
342
|
+
<!-- No selection → current folder summary -->
|
|
343
|
+
<div v-else class="fe-inspector__hero">
|
|
344
|
+
<span class="fe-inspector__bigicon" v-html="fileIconSvg({ type: 'dir' })"></span>
|
|
345
|
+
<p class="fe-inspector__name" :title="dirLabel">{{ dirLabel }}</p>
|
|
346
|
+
<p class="fe-inspector__sub">{{ t('inspector.folder_items', { n: dirCount }) }}</p>
|
|
347
|
+
</div>
|
|
348
|
+
</section>
|
|
349
|
+
|
|
350
|
+
<!-- ══ Sürümler ══ -->
|
|
351
|
+
<section
|
|
352
|
+
v-if="versionsState === 'ok' || versionsState === 'error'"
|
|
353
|
+
class="fe-inspector__section"
|
|
354
|
+
>
|
|
355
|
+
<h3 class="fe-inspector__heading">{{ t('inspector.section.versions') }}</h3>
|
|
356
|
+
|
|
357
|
+
<p v-if="versionsState === 'error'" class="fe-inspector__empty">
|
|
358
|
+
{{ t('inspector.error') }}
|
|
359
|
+
</p>
|
|
360
|
+
<template v-else>
|
|
361
|
+
<p v-if="versions.length === 0" class="fe-inspector__empty">
|
|
362
|
+
{{ t('inspector.versions.empty') }}
|
|
363
|
+
</p>
|
|
364
|
+
<ul v-else class="fe-inspector__versions">
|
|
365
|
+
<li v-for="v in versions" :key="v.id" class="fe-inspector__version">
|
|
366
|
+
<div class="fe-inspector__version-main">
|
|
367
|
+
<span class="fe-inspector__version-n">
|
|
368
|
+
{{ t('inspector.versions.v', { n: v.version_n }) }}
|
|
369
|
+
</span>
|
|
370
|
+
<span class="fe-inspector__version-meta">
|
|
371
|
+
{{ formatDateStr(v.created_at) }} · {{ formatSize(v.size) }}
|
|
372
|
+
</span>
|
|
373
|
+
</div>
|
|
374
|
+
<div v-if="confirmVersionId === v.id" class="fe-inspector__confirm">
|
|
375
|
+
<p class="fe-inspector__confirm-q">{{ t('inspector.versions.restore_confirm') }}</p>
|
|
376
|
+
<label class="fe-inspector__check">
|
|
377
|
+
<input v-model="snapshotFirst" type="checkbox" />
|
|
378
|
+
{{ t('inspector.versions.snapshot_current') }}
|
|
379
|
+
</label>
|
|
380
|
+
<div class="fe-inspector__confirm-actions">
|
|
381
|
+
<button
|
|
382
|
+
type="button"
|
|
383
|
+
class="fe-btn fe-btn--primary fe-btn--sm"
|
|
384
|
+
:disabled="versionBusy"
|
|
385
|
+
@click="confirmRestore(v)"
|
|
386
|
+
>{{ t('inspector.versions.confirm') }}</button>
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
class="fe-btn fe-btn--sm"
|
|
390
|
+
:disabled="versionBusy"
|
|
391
|
+
@click="confirmVersionId = null"
|
|
392
|
+
>{{ t('inspector.versions.cancel') }}</button>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
<button
|
|
396
|
+
v-else
|
|
397
|
+
type="button"
|
|
398
|
+
class="fe-btn fe-btn--sm"
|
|
399
|
+
:disabled="versionBusy"
|
|
400
|
+
@click="askRestore(v)"
|
|
401
|
+
>{{ t('inspector.versions.restore') }}</button>
|
|
402
|
+
</li>
|
|
403
|
+
</ul>
|
|
404
|
+
<button
|
|
405
|
+
type="button"
|
|
406
|
+
class="fe-btn fe-btn--sm fe-inspector__snapshot"
|
|
407
|
+
:disabled="versionBusy"
|
|
408
|
+
@click="takeSnapshot"
|
|
409
|
+
>{{ t('inspector.versions.take_snapshot') }}</button>
|
|
410
|
+
</template>
|
|
411
|
+
</section>
|
|
412
|
+
|
|
413
|
+
<!-- ══ İzinler ══ -->
|
|
414
|
+
<section v-if="single && effectivePerm" class="fe-inspector__section">
|
|
415
|
+
<h3 class="fe-inspector__heading">{{ t('inspector.section.permissions') }}</h3>
|
|
416
|
+
<div class="fe-inspector__permrow">
|
|
417
|
+
<span
|
|
418
|
+
class="fe-inspector__badge"
|
|
419
|
+
:class="`fe-inspector__badge--${effectivePerm}`"
|
|
420
|
+
>{{ permLabel(effectivePerm) }}</span>
|
|
421
|
+
<button
|
|
422
|
+
v-if="canManagePerms"
|
|
423
|
+
type="button"
|
|
424
|
+
class="fe-btn fe-btn--sm"
|
|
425
|
+
@click="emit('manage-permissions', single)"
|
|
426
|
+
>{{ t('inspector.perm.manage') }}</button>
|
|
427
|
+
</div>
|
|
428
|
+
</section>
|
|
429
|
+
|
|
430
|
+
<!-- ══ Paylaşımlar ══ -->
|
|
431
|
+
<section v-if="sharesState === 'ok'" class="fe-inspector__section">
|
|
432
|
+
<h3 class="fe-inspector__heading">{{ t('inspector.section.shares') }}</h3>
|
|
433
|
+
<p v-if="shares.length === 0" class="fe-inspector__empty">
|
|
434
|
+
{{ t('inspector.shares.empty') }}
|
|
435
|
+
</p>
|
|
436
|
+
<ul v-else class="fe-inspector__shares">
|
|
437
|
+
<li v-for="s in shares" :key="s.uuid" class="fe-inspector__share">
|
|
438
|
+
<span class="fe-inspector__share-url" :title="s.url">{{ s.url }}</span>
|
|
439
|
+
<span v-if="s.password_pin" class="fe-inspector__badge fe-inspector__badge--pin">PIN</span>
|
|
440
|
+
<span v-if="s.expires_at" class="fe-inspector__share-exp">
|
|
441
|
+
{{ formatDateStr(s.expires_at) }}
|
|
442
|
+
</span>
|
|
443
|
+
<button
|
|
444
|
+
type="button"
|
|
445
|
+
class="fe-inspector__copy"
|
|
446
|
+
:title="t('inspector.shares.copy')"
|
|
447
|
+
:aria-label="t('inspector.shares.copy')"
|
|
448
|
+
@click="copyText(s.url)"
|
|
449
|
+
>⧉</button>
|
|
450
|
+
</li>
|
|
451
|
+
</ul>
|
|
452
|
+
</section>
|
|
453
|
+
</div>
|
|
454
|
+
</aside>
|
|
455
|
+
</template>
|