@brftech/filex-core 0.24.0 → 0.25.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/filex-core.js +6394 -6294
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +49 -49
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +5 -0
- package/src/lib/shareTtl.ts +91 -0
- package/src/locales/en.ts +2 -0
- package/src/locales/tr.ts +2 -0
- package/src/modals/PermissionsModal.vue +59 -15
- package/src/modals/ShareModal.vue +21 -3
- package/src/styles/base.css +2 -0
- package/src/types/FileNode.ts +6 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The server caps how long a new share link may live (`share_max_ttl_days` in
|
|
2
|
+
// /api/capabilities, set by the admin under Protection; default 7 days). The
|
|
3
|
+
// dialogs read it so they only OFFER expiries the server will honour — a
|
|
4
|
+
// "30 days" option that quietly becomes 7 days is a lie on the screen, and a
|
|
5
|
+
// "Never" option that becomes a week is a worse one.
|
|
6
|
+
//
|
|
7
|
+
// One helper for every surface: the Share / Permissions panel, the standalone
|
|
8
|
+
// share dialog, the desktop app and the embeds all render the same choices
|
|
9
|
+
// from the same rule. A surface that clamps differently would mean the same
|
|
10
|
+
// product behaves two ways.
|
|
11
|
+
|
|
12
|
+
export interface ExpiryOption {
|
|
13
|
+
/** Days; 0 = never. */
|
|
14
|
+
v: number;
|
|
15
|
+
l: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The stock expiry choices before the ceiling is applied (days; 0 = never). */
|
|
19
|
+
export const STOCK_EXPIRY_DAYS = [0, 1, 7, 30];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* clampExpiryOptions returns the options a dialog may show under a ceiling
|
|
23
|
+
* of `maxDays` (0/undefined = no ceiling → every option as is). Options past
|
|
24
|
+
* the ceiling and "never" are dropped; the ceiling itself is added as the
|
|
25
|
+
* longest choice when the stock list does not already contain it.
|
|
26
|
+
*/
|
|
27
|
+
export function clampExpiryOptions(
|
|
28
|
+
days: number[],
|
|
29
|
+
maxDays: number | undefined,
|
|
30
|
+
label: (days: number) => string,
|
|
31
|
+
): ExpiryOption[] {
|
|
32
|
+
const max = maxDays && maxDays > 0 ? Math.floor(maxDays) : 0;
|
|
33
|
+
let list = max ? days.filter((d) => d > 0 && d <= max) : days.slice();
|
|
34
|
+
if (max && !list.includes(max)) list.push(max);
|
|
35
|
+
list = Array.from(new Set(list)).sort((a, b) => (a === 0 ? -1 : b === 0 ? 1 : a - b));
|
|
36
|
+
return list.map((d) => ({ v: d, l: label(d) }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* defaultExpiryDays is what a fresh dialog preselects: the ceiling when there
|
|
41
|
+
* is one (the server would apply it anyway — showing it up front is honest),
|
|
42
|
+
* otherwise "never".
|
|
43
|
+
*/
|
|
44
|
+
export function defaultExpiryDays(maxDays: number | undefined): number {
|
|
45
|
+
return maxDays && maxDays > 0 ? Math.floor(maxDays) : 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* clampExpiryDate pulls a free-form expiry (datetime input) under the ceiling.
|
|
50
|
+
* Returns the ISO string to send, or null for "never" when there is no
|
|
51
|
+
* ceiling. `now` is injectable for tests.
|
|
52
|
+
*/
|
|
53
|
+
export function clampExpiryDate(
|
|
54
|
+
chosen: Date | null,
|
|
55
|
+
maxDays: number | undefined,
|
|
56
|
+
now: Date = new Date(),
|
|
57
|
+
): { iso: string | null; clamped: boolean } {
|
|
58
|
+
const max = maxDays && maxDays > 0 ? Math.floor(maxDays) : 0;
|
|
59
|
+
if (!max) return { iso: chosen ? chosen.toISOString() : null, clamped: false };
|
|
60
|
+
const limit = new Date(now.getTime() + max * 86400000);
|
|
61
|
+
if (!chosen || chosen.getTime() > limit.getTime()) return { iso: limit.toISOString(), clamped: true };
|
|
62
|
+
return { iso: chosen.toISOString(), clamped: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Value for a `<input type="datetime-local" max=…>` under the ceiling (local time, minute precision). */
|
|
66
|
+
export function expiryInputMax(maxDays: number | undefined, now: Date = new Date()): string | undefined {
|
|
67
|
+
const max = maxDays && maxDays > 0 ? Math.floor(maxDays) : 0;
|
|
68
|
+
if (!max) return undefined;
|
|
69
|
+
const limit = new Date(now.getTime() + max * 86400000);
|
|
70
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
71
|
+
return `${limit.getFullYear()}-${pad(limit.getMonth() + 1)}-${pad(limit.getDate())}T${pad(limit.getHours())}:${pad(limit.getMinutes())}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** "Valid until 30 Aug 2026, 14:05" / "Does not expire" from the server's `expires_at`. */
|
|
75
|
+
export function validUntilLine(expiresAt: string | null | undefined, locale: 'tr' | 'en'): string {
|
|
76
|
+
if (!expiresAt) return locale === 'tr' ? 'Bu bağlantının süresi yoktur.' : 'This link does not expire.';
|
|
77
|
+
const d = new Date(expiresAt);
|
|
78
|
+
const when = Number.isNaN(d.getTime())
|
|
79
|
+
? expiresAt
|
|
80
|
+
: d.toLocaleString(locale === 'tr' ? 'tr-TR' : 'en-GB', { dateStyle: 'medium', timeStyle: 'short' });
|
|
81
|
+
return locale === 'tr' ? `Bu bağlantı ${when} tarihine kadar geçerli.` : `This link is valid until ${when}.`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The hint under an expiry control: what the server will allow at most. */
|
|
85
|
+
export function ttlCeilingHint(maxDays: number | undefined, locale: 'tr' | 'en'): string {
|
|
86
|
+
const max = maxDays && maxDays > 0 ? Math.floor(maxDays) : 0;
|
|
87
|
+
if (!max) return '';
|
|
88
|
+
return locale === 'tr'
|
|
89
|
+
? `Bağlantılar en fazla ${max} gün geçerli olabilir (sunucu ayarı).`
|
|
90
|
+
: `Links can be valid for at most ${max} day${max === 1 ? '' : 's'} (server setting).`;
|
|
91
|
+
}
|
package/src/locales/en.ts
CHANGED
|
@@ -61,6 +61,8 @@ export const en: Record<string, string> = {
|
|
|
61
61
|
'modal.share.max_downloads': 'Download limit',
|
|
62
62
|
'modal.share.create': 'Share',
|
|
63
63
|
'modal.share.cancel': 'Cancel',
|
|
64
|
+
'modal.share.close': 'Close',
|
|
65
|
+
'modal.share.limit_applied': '(server limit applied)',
|
|
64
66
|
'modal.share.copy': 'Copy',
|
|
65
67
|
'modal.share.url_copied': 'Link copied',
|
|
66
68
|
'modal.share.pin_copied': 'PIN copied',
|
package/src/locales/tr.ts
CHANGED
|
@@ -61,6 +61,8 @@ export const tr: Record<string, string> = {
|
|
|
61
61
|
'modal.share.max_downloads': 'İndirme limiti',
|
|
62
62
|
'modal.share.create': 'Paylaş',
|
|
63
63
|
'modal.share.cancel': 'Vazgeç',
|
|
64
|
+
'modal.share.close': 'Kapat',
|
|
65
|
+
'modal.share.limit_applied': '(sunucu sınırı uygulandı)',
|
|
64
66
|
'modal.share.copy': 'Kopyala',
|
|
65
67
|
'modal.share.url_copied': 'Link kopyalandı',
|
|
66
68
|
'modal.share.pin_copied': 'PIN kopyalandı',
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// from the explorer's unified "Paylaş / İzinler" action. The layout is a fixed
|
|
5
5
|
// header/tabs with a single scrollable body so the popup never grows into one
|
|
6
6
|
// long scroll. Styling uses the SFC's --fe-* theme variables (light/dark).
|
|
7
|
-
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
|
|
7
|
+
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
|
|
8
8
|
import type { FileApi, Grant, UserSuggestion } from '../composables/useFileApi';
|
|
9
9
|
import type { ShareInfo } from '../types/FileNode';
|
|
10
10
|
import { shareCliCommand } from '../lib/shareCli';
|
|
11
|
+
import { STOCK_EXPIRY_DAYS, clampExpiryOptions, defaultExpiryDays, ttlCeilingHint, validUntilLine } from '../lib/shareTtl';
|
|
11
12
|
|
|
12
13
|
const props = defineProps<{
|
|
13
14
|
api: FileApi;
|
|
@@ -15,6 +16,9 @@ const props = defineProps<{
|
|
|
15
16
|
isDir?: boolean; // folder → grants cascade; file → no `/…` inheritance hint
|
|
16
17
|
size?: number; // bytes, for the share-mail body (files only)
|
|
17
18
|
locale?: 'tr' | 'en';
|
|
19
|
+
/** Server ceiling on a new link's life in days (capabilities.share_max_ttl_days).
|
|
20
|
+
* undefined/0 = no ceiling. The expiry choices are derived from it. */
|
|
21
|
+
shareMaxTtlDays?: number;
|
|
18
22
|
}>();
|
|
19
23
|
const emit = defineEmits<{ (e: 'close'): void }>();
|
|
20
24
|
|
|
@@ -66,8 +70,8 @@ function levelLabel(v: string): string {
|
|
|
66
70
|
const shares = ref<ShareInfo[]>([]);
|
|
67
71
|
const shareBusy = ref(false);
|
|
68
72
|
const sharePwd = ref(false);
|
|
69
|
-
const shareExpiry = ref(
|
|
70
|
-
const shareResult = ref<{ url: string; pin?: string | null } | null>(null);
|
|
73
|
+
const shareExpiry = ref(defaultExpiryDays(props.shareMaxTtlDays)); // days; 0 = never
|
|
74
|
+
const shareResult = ref<{ url: string; pin?: string | null; expiresAt?: string | null; clamped?: boolean } | null>(null);
|
|
71
75
|
const shareErr = ref('');
|
|
72
76
|
const copied = ref('');
|
|
73
77
|
// prefilled recipient when the owner chose "share link" for a no-account email
|
|
@@ -75,12 +79,25 @@ const shareMailTo = ref('');
|
|
|
75
79
|
const shareMailBusy = ref(false);
|
|
76
80
|
const shareMailNotice = ref('');
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// ⚠ Derived from the server's ceiling, never a fixed list: offering "30 days"
|
|
83
|
+
// on a server that keeps links for 7 would show a choice that is not one.
|
|
84
|
+
// The same helper drives every surface (see lib/shareTtl.ts).
|
|
85
|
+
function expiryLabel(days: number): string {
|
|
86
|
+
if (days === 0) return L('Süresiz', 'Never');
|
|
87
|
+
return tr.value ? `${days} gün` : `${days} day${days === 1 ? '' : 's'}`;
|
|
88
|
+
}
|
|
89
|
+
const expiryOptions = computed(() => clampExpiryOptions(STOCK_EXPIRY_DAYS, props.shareMaxTtlDays, expiryLabel));
|
|
90
|
+
const ttlHint = computed(() => ttlCeilingHint(props.shareMaxTtlDays, tr.value ? 'tr' : 'en'));
|
|
91
|
+
watch(
|
|
92
|
+
() => props.shareMaxTtlDays,
|
|
93
|
+
() => {
|
|
94
|
+
// The ceiling can arrive after mount (capabilities load async): snap a
|
|
95
|
+
// selection the server would not honour back to what it will.
|
|
96
|
+
const allowed = expiryOptions.value.map((o) => o.v);
|
|
97
|
+
if (!allowed.includes(shareExpiry.value)) shareExpiry.value = defaultExpiryDays(props.shareMaxTtlDays);
|
|
98
|
+
if (!allowed.includes(dropExpiry.value)) dropExpiry.value = defaultExpiryDays(props.shareMaxTtlDays);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
84
101
|
|
|
85
102
|
// ⚠ The download cap lived in the old standalone ShareModal and was left
|
|
86
103
|
// behind when this panel took over link creation — the server has honoured
|
|
@@ -117,7 +134,7 @@ const shareCli = computed(() =>
|
|
|
117
134
|
|
|
118
135
|
// ── file-drop (public upload link) state ──
|
|
119
136
|
const dropPwd = ref(false);
|
|
120
|
-
const dropExpiry = ref(
|
|
137
|
+
const dropExpiry = ref(defaultExpiryDays(props.shareMaxTtlDays)); // days; 0 = never
|
|
121
138
|
const dropShowAdv = ref(false);
|
|
122
139
|
const dropMaxFiles = ref<string>('');
|
|
123
140
|
const dropMaxSizeMB = ref<string>('');
|
|
@@ -125,7 +142,7 @@ const dropAllowedExt = ref<string>('');
|
|
|
125
142
|
const dropAskName = ref(true);
|
|
126
143
|
const dropBusy = ref(false);
|
|
127
144
|
const dropErr = ref('');
|
|
128
|
-
const dropResult = ref<{ url: string; pin?: string | null } | null>(null);
|
|
145
|
+
const dropResult = ref<{ url: string; pin?: string | null; expiresAt?: string | null; clamped?: boolean } | null>(null);
|
|
129
146
|
const dropMailTo = ref('');
|
|
130
147
|
const dropMailBusy = ref(false);
|
|
131
148
|
const dropMailNotice = ref('');
|
|
@@ -328,7 +345,12 @@ async function createLink() {
|
|
|
328
345
|
// null says it explicitly and keeps the payload honest.
|
|
329
346
|
max_downloads: shareMaxDl.value || null,
|
|
330
347
|
});
|
|
331
|
-
shareResult.value = {
|
|
348
|
+
shareResult.value = {
|
|
349
|
+
url: r.share.url,
|
|
350
|
+
pin: r.share.password_pin ?? null,
|
|
351
|
+
expiresAt: r.share.expires_at ?? null,
|
|
352
|
+
clamped: !!r.share.expiry_clamped,
|
|
353
|
+
};
|
|
332
354
|
await reloadShares();
|
|
333
355
|
} catch (e) {
|
|
334
356
|
shareErr.value = e instanceof Error ? e.message : String(e);
|
|
@@ -392,7 +414,12 @@ async function createDropLink() {
|
|
|
392
414
|
expires_at: dropExpiresAtISO(),
|
|
393
415
|
drop_settings,
|
|
394
416
|
});
|
|
395
|
-
dropResult.value = {
|
|
417
|
+
dropResult.value = {
|
|
418
|
+
url: r.share.url,
|
|
419
|
+
pin: r.share.password_pin ?? null,
|
|
420
|
+
expiresAt: r.share.expires_at ?? null,
|
|
421
|
+
clamped: !!r.share.expiry_clamped,
|
|
422
|
+
};
|
|
396
423
|
} catch (e) {
|
|
397
424
|
dropErr.value = e instanceof Error ? e.message : String(e);
|
|
398
425
|
} finally {
|
|
@@ -464,6 +491,11 @@ function expiryLine(days: number): string {
|
|
|
464
491
|
if (days > 0) return L(`Bu bağlantı ${days} gün geçerlidir.`, `This link is valid for ${days} day(s).`);
|
|
465
492
|
return L('Bu bağlantının süresi yoktur.', 'This link does not expire.');
|
|
466
493
|
}
|
|
494
|
+
// What the server actually stored — shown under a fresh link so the real
|
|
495
|
+
// expiry is visible even when the server shortened the request.
|
|
496
|
+
function validUntil(r: { expiresAt?: string | null } | null): string {
|
|
497
|
+
return validUntilLine(r?.expiresAt ?? null, tr.value ? 'tr' : 'en');
|
|
498
|
+
}
|
|
467
499
|
|
|
468
500
|
// Text + title for a download-share link, mirroring shareMailText().
|
|
469
501
|
function shareBody(): { title: string; text: string } {
|
|
@@ -651,7 +683,7 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
651
683
|
</label>
|
|
652
684
|
<label class="fx-perm-field">
|
|
653
685
|
<span class="fx-perm-muted">{{ L('Süre', 'Expiry') }}</span>
|
|
654
|
-
<select v-model.number="shareExpiry" class="fx-perm-sel fx-perm-sel--sm">
|
|
686
|
+
<select v-model.number="shareExpiry" class="fx-perm-sel fx-perm-sel--sm" data-testid="share-expiry">
|
|
655
687
|
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
656
688
|
</select>
|
|
657
689
|
</label>
|
|
@@ -662,6 +694,7 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
662
694
|
</select>
|
|
663
695
|
</label>
|
|
664
696
|
</div>
|
|
697
|
+
<p v-if="ttlHint" class="fx-perm-hint fx-perm-ttlhint" data-testid="share-ttl-hint">{{ ttlHint }}</p>
|
|
665
698
|
<button class="fx-perm-btn fx-perm-btn--primary fx-perm-create" :disabled="shareBusy" @click="createLink">
|
|
666
699
|
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
667
700
|
</button>
|
|
@@ -675,6 +708,10 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
675
708
|
{{ copied === 'new' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
676
709
|
</button>
|
|
677
710
|
</div>
|
|
711
|
+
<div class="fx-perm-muted fx-perm-validuntil" data-testid="share-valid-until">
|
|
712
|
+
{{ validUntil(shareResult) }}
|
|
713
|
+
<span v-if="shareResult.clamped">{{ L('(sunucu sınırı uygulandı)', '(server limit applied)') }}</span>
|
|
714
|
+
</div>
|
|
678
715
|
<div v-if="shareResult.pin" class="fx-perm-pin">
|
|
679
716
|
<span>PIN: <code>{{ shareResult.pin }}</code></span>
|
|
680
717
|
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(shareResult.pin, 'sharepin')">
|
|
@@ -740,11 +777,12 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
740
777
|
</label>
|
|
741
778
|
<label class="fx-perm-field">
|
|
742
779
|
<span class="fx-perm-muted">{{ L('Süre', 'Expiry') }}</span>
|
|
743
|
-
<select v-model.number="dropExpiry" class="fx-perm-sel fx-perm-sel--sm">
|
|
780
|
+
<select v-model.number="dropExpiry" class="fx-perm-sel fx-perm-sel--sm" data-testid="drop-expiry">
|
|
744
781
|
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
745
782
|
</select>
|
|
746
783
|
</label>
|
|
747
784
|
</div>
|
|
785
|
+
<p v-if="ttlHint" class="fx-perm-hint fx-perm-ttlhint">{{ ttlHint }}</p>
|
|
748
786
|
<button class="fx-perm-btn fx-perm-btn--primary fx-perm-create" :disabled="dropBusy" @click="createDropLink">
|
|
749
787
|
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
750
788
|
</button>
|
|
@@ -780,6 +818,10 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
780
818
|
{{ copied === 'drop' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
781
819
|
</button>
|
|
782
820
|
</div>
|
|
821
|
+
<div class="fx-perm-muted fx-perm-validuntil">
|
|
822
|
+
{{ validUntil(dropResult) }}
|
|
823
|
+
<span v-if="dropResult.clamped">{{ L('(sunucu sınırı uygulandı)', '(server limit applied)') }}</span>
|
|
824
|
+
</div>
|
|
783
825
|
<div v-if="dropResult.pin" class="fx-perm-pin">
|
|
784
826
|
<span>PIN: <code>{{ dropResult.pin }}</code></span>
|
|
785
827
|
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(dropResult.pin, 'droppin')">
|
|
@@ -966,6 +1008,8 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
966
1008
|
.fx-perm-mailrow { display: flex; gap: 8px; margin-top: 10px; }
|
|
967
1009
|
.fx-perm-mailrow .fx-perm-input { flex: 1; }
|
|
968
1010
|
.fx-perm-hint { font-size: 13px; color: var(--fe-text-muted); margin: 10px 0 0; }
|
|
1011
|
+
.fx-perm-ttlhint { margin: 4px 0 8px; }
|
|
1012
|
+
.fx-perm-validuntil { margin: 4px 0 6px; }
|
|
969
1013
|
/* native "Paylaş" button — sits under the mail row, full width */
|
|
970
1014
|
.fx-perm-sharebtn { display: flex; width: 100%; align-items: center; justify-content: center; gap: 6px; margin-top: 8px; }
|
|
971
1015
|
|
|
@@ -4,12 +4,15 @@ import type { LocaleCode } from '../types/ExplorerConfig';
|
|
|
4
4
|
import type { ShareInfo } from '../types/FileNode';
|
|
5
5
|
import { useLocale } from '../composables/useLocale';
|
|
6
6
|
import { shareCliCommand } from '../lib/shareCli';
|
|
7
|
+
import { clampExpiryDate, expiryInputMax, ttlCeilingHint, validUntilLine } from '../lib/shareTtl';
|
|
7
8
|
import Modal from './Modal.vue';
|
|
8
9
|
|
|
9
10
|
const props = defineProps<{
|
|
10
11
|
open: boolean;
|
|
11
12
|
locale: LocaleCode;
|
|
12
13
|
share?: (ShareInfo & { url: string; filename?: string }) | null;
|
|
14
|
+
/** Server ceiling on a new link's life in days (0/undefined = none). */
|
|
15
|
+
shareMaxTtlDays?: number;
|
|
13
16
|
}>();
|
|
14
17
|
|
|
15
18
|
const emit = defineEmits<{
|
|
@@ -35,10 +38,21 @@ watch(() => props.open, (v) => {
|
|
|
35
38
|
}
|
|
36
39
|
});
|
|
37
40
|
|
|
41
|
+
// The picker is capped at the server's ceiling and the value is clamped once
|
|
42
|
+
// more on submit — the server clamps too, but asking for a date it will
|
|
43
|
+
// refuse is how the user ends up with a link that says one thing and does
|
|
44
|
+
// another.
|
|
45
|
+
const expiryMax = computed(() => expiryInputMax(props.shareMaxTtlDays));
|
|
46
|
+
const ttlHint = computed(() => ttlCeilingHint(props.shareMaxTtlDays, props.locale === 'tr' ? 'tr' : 'en'));
|
|
47
|
+
const validUntil = computed(() =>
|
|
48
|
+
props.share ? validUntilLine(props.share.expires_at ?? null, props.locale === 'tr' ? 'tr' : 'en') : '',
|
|
49
|
+
);
|
|
50
|
+
|
|
38
51
|
function submit() {
|
|
52
|
+
const chosen = expiresAt.value ? new Date(expiresAt.value) : null;
|
|
39
53
|
emit('submit', {
|
|
40
54
|
password: usePin.value,
|
|
41
|
-
expires_at:
|
|
55
|
+
expires_at: clampExpiryDate(chosen, props.shareMaxTtlDays).iso,
|
|
42
56
|
max_downloads: maxDownloads.value ? Number(maxDownloads.value) : null,
|
|
43
57
|
});
|
|
44
58
|
}
|
|
@@ -70,7 +84,8 @@ const cliCommand = computed(() =>
|
|
|
70
84
|
</label>
|
|
71
85
|
<label class="fe-form__row fe-form__row--stack">
|
|
72
86
|
<span>{{ t('modal.share.expires') }}</span>
|
|
73
|
-
<input v-model="expiresAt" type="datetime-local" class="fe-input" />
|
|
87
|
+
<input v-model="expiresAt" type="datetime-local" class="fe-input" :max="expiryMax" />
|
|
88
|
+
<small v-if="ttlHint" class="fe-form__hint">{{ ttlHint }}</small>
|
|
74
89
|
</label>
|
|
75
90
|
<label class="fe-form__row fe-form__row--stack">
|
|
76
91
|
<span>{{ t('modal.share.max_downloads') }}</span>
|
|
@@ -80,6 +95,9 @@ const cliCommand = computed(() =>
|
|
|
80
95
|
</template>
|
|
81
96
|
<template v-else>
|
|
82
97
|
<div class="fe-share-result">
|
|
98
|
+
<div class="fe-share-result__row fe-share-result__row--note">
|
|
99
|
+
<small>{{ validUntil }}<template v-if="share.expiry_clamped"> {{ t('modal.share.limit_applied') }}</template></small>
|
|
100
|
+
</div>
|
|
83
101
|
<div class="fe-share-result__row">
|
|
84
102
|
<label>Link</label>
|
|
85
103
|
<div class="fe-share-result__copy">
|
|
@@ -111,7 +129,7 @@ const cliCommand = computed(() =>
|
|
|
111
129
|
</template>
|
|
112
130
|
<template #actions>
|
|
113
131
|
<button type="button" class="fe-btn" @click="emit('close')">
|
|
114
|
-
{{ share ? '
|
|
132
|
+
{{ share ? t('modal.share.close') : t('modal.share.cancel') }}
|
|
115
133
|
</button>
|
|
116
134
|
<button v-if="!share" type="button" class="fe-btn fe-btn--primary" @click="submit">
|
|
117
135
|
{{ t('modal.share.create') }}
|
package/src/styles/base.css
CHANGED
|
@@ -1128,6 +1128,8 @@ filex-explorer {
|
|
|
1128
1128
|
flex-direction: column;
|
|
1129
1129
|
align-items: stretch;
|
|
1130
1130
|
}
|
|
1131
|
+
.fe-form__hint { font-size: 12px; color: var(--fe-text-muted); }
|
|
1132
|
+
.fe-share-result__row--note small { color: var(--fe-text-muted); }
|
|
1131
1133
|
.fe-form__error {
|
|
1132
1134
|
color: var(--fe-danger);
|
|
1133
1135
|
font-size: 12px;
|
package/src/types/FileNode.ts
CHANGED
|
@@ -54,6 +54,9 @@ export interface ShareInfo {
|
|
|
54
54
|
url: string;
|
|
55
55
|
password_pin?: string | null;
|
|
56
56
|
expires_at?: string | null;
|
|
57
|
+
/** True when the server shortened (or set) the expiry to honour its
|
|
58
|
+
* max-TTL setting — the UI then shows the real date, not the request. */
|
|
59
|
+
expiry_clamped?: boolean;
|
|
57
60
|
max_downloads?: number | null;
|
|
58
61
|
downloads?: number;
|
|
59
62
|
created_at?: string;
|
|
@@ -82,6 +85,9 @@ export interface Capabilities {
|
|
|
82
85
|
convert_url?: string | null;
|
|
83
86
|
max_chunk_mb?: number;
|
|
84
87
|
upload_limit_mb?: number;
|
|
88
|
+
/** Longest life a new share link may be given, in days (0 = no ceiling).
|
|
89
|
+
* Read by the share dialogs so they offer only expiries the server keeps. */
|
|
90
|
+
share_max_ttl_days?: number;
|
|
85
91
|
external?: {
|
|
86
92
|
onlyoffice?: ExternalServiceStatus;
|
|
87
93
|
drawio?: ExternalServiceStatus;
|