@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
|
@@ -41,16 +41,19 @@ import {
|
|
|
41
41
|
} from '../lib/shareTtl';
|
|
42
42
|
import { EXPLORER_CLOCK } from '../lib/timezone';
|
|
43
43
|
import { resolveLocale } from '../locales/resolve';
|
|
44
|
-
import {
|
|
44
|
+
import { localeTag, useLocale } from '../composables/useLocale';
|
|
45
|
+
import { personInitial, personName } from '../lib/personName';
|
|
46
|
+
import { splitList } from '../lib/listInput';
|
|
45
47
|
import { actionIconSvg } from '../lib/actionIcons';
|
|
46
48
|
import { fileIconTile } from '../lib/fileIcons';
|
|
49
|
+
import { gateOnService } from '../lib/serviceGate';
|
|
47
50
|
|
|
48
51
|
const props = defineProps<{
|
|
49
52
|
api: FileApi;
|
|
50
53
|
path: string; // adapter://rel of the target item
|
|
51
54
|
isDir?: boolean; // folder → grants cascade; file → no `/…` inheritance hint
|
|
52
55
|
size?: number; // bytes, for the share-mail body (files only)
|
|
53
|
-
locale?:
|
|
56
|
+
locale?: string;
|
|
54
57
|
/** Server ceiling on a new link's life in days (capabilities.share_max_ttl_days).
|
|
55
58
|
* undefined/0 = no ceiling. The expiry choices are derived from it. */
|
|
56
59
|
shareMaxTtlDays?: number;
|
|
@@ -67,15 +70,36 @@ const props = defineProps<{
|
|
|
67
70
|
* theirs to see.
|
|
68
71
|
*/
|
|
69
72
|
initialTab?: 'perms' | 'share' | 'drop';
|
|
73
|
+
/**
|
|
74
|
+
* Can outgoing mail be sent right now (`capabilities.mail.ready`)? `false`
|
|
75
|
+
* only when the server SAID so — absent (an older server) keeps the "Send
|
|
76
|
+
* by email" rows exactly as they were.
|
|
77
|
+
*/
|
|
78
|
+
mailReady?: boolean;
|
|
79
|
+
/** Could this person set mail up (`capabilities.caller_admin`)? */
|
|
80
|
+
canConfigure?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The caller's level on this item, when the host already knows it (the
|
|
83
|
+
* listing row's `perm`). Anything below `owner` cannot read the grant list,
|
|
84
|
+
* so the dialog does not ask: it used to send the request anyway, swallow
|
|
85
|
+
* the 403 and quietly draw no people section — a question the host had
|
|
86
|
+
* already answered, asked again on every open. Absent = ask.
|
|
87
|
+
*/
|
|
88
|
+
perm?: string;
|
|
70
89
|
}>();
|
|
71
90
|
const emit = defineEmits<{ (e: 'close'): void }>();
|
|
72
91
|
|
|
92
|
+
/**
|
|
93
|
+
* "Send by email", when mail cannot be sent — the owner's rule for a missing
|
|
94
|
+
* service (2026-09-21, lib/serviceGate): an administrator sees the row greyed
|
|
95
|
+
* with where to set mail up; everybody else is not offered it. It used to be
|
|
96
|
+
* offered to everyone and answer only after the click.
|
|
97
|
+
*/
|
|
98
|
+
const mailMissing = computed(() => props.mailReady === false);
|
|
99
|
+
const mailRowShown = computed(() => !mailMissing.value || props.canConfigure === true);
|
|
100
|
+
|
|
73
101
|
const localeCode = computed(() => resolveLocale(props.locale));
|
|
74
|
-
const { t } = useLocale(() => localeCode.value);
|
|
75
|
-
const tr = computed(() => localeCode.value !== 'en');
|
|
76
|
-
function L(t: string, e: string): string {
|
|
77
|
-
return tr.value ? t : e;
|
|
78
|
-
}
|
|
102
|
+
const { t, formatSize } = useLocale(() => localeCode.value);
|
|
79
103
|
|
|
80
104
|
// Split adapter://rel for a friendlier path chip.
|
|
81
105
|
const pathParts = computed(() => {
|
|
@@ -120,15 +144,33 @@ const suggestions = ref<UserSuggestion[]>([]);
|
|
|
120
144
|
const showSuggest = ref(false);
|
|
121
145
|
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
|
122
146
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{ v: '
|
|
127
|
-
|
|
147
|
+
// ⚠ computed, not a plain array: labels built once at setup kept the language
|
|
148
|
+
// the dialog was opened in.
|
|
149
|
+
const levels = computed<Array<{ v: 'viewer' | 'editor' | 'owner'; l: string; d: string }>>(() => [
|
|
150
|
+
{ v: 'viewer', l: t('access.ui.level_viewer'), d: t('access.ui.level_viewer_desc') },
|
|
151
|
+
{ v: 'editor', l: t('access.ui.level_editor'), d: t('access.ui.level_editor_desc') },
|
|
152
|
+
{ v: 'owner', l: t('access.ui.level_owner'), d: t('access.ui.level_owner_desc') },
|
|
153
|
+
]);
|
|
128
154
|
function levelLabel(v: string): string {
|
|
129
|
-
return levels.find((o) => o.v === v)?.l ?? v;
|
|
155
|
+
return levels.value.find((o) => o.v === v)?.l ?? v;
|
|
130
156
|
}
|
|
131
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Per-item access with RBAC OFF on the storage.
|
|
160
|
+
*
|
|
161
|
+
* ⚠ The add-people form used to work there while a box above it said grants
|
|
162
|
+
* do not apply (QA, 2026-09-21): a form that succeeds at something that has no
|
|
163
|
+
* effect. It is the same case as a missing service (lib/serviceGate): an
|
|
164
|
+
* administrator — who can switch RBAC on — sees the section with the form
|
|
165
|
+
* greyed and where to switch it on; anybody else is not offered the section.
|
|
166
|
+
* Grants that already exist stay listed, so they can still be removed.
|
|
167
|
+
*/
|
|
168
|
+
const rbacGate = computed(() =>
|
|
169
|
+
storageRbac.value ? {} : gateOnService(false, props.canConfigure === true, t('access.ui.rbac_off_here')),
|
|
170
|
+
);
|
|
171
|
+
const peopleShown = computed(() => canManage.value && rbacGate.value.hidden !== true);
|
|
172
|
+
const addBlocked = computed(() => rbacGate.value.disabled === true);
|
|
173
|
+
|
|
132
174
|
// ── share state ──
|
|
133
175
|
const shares = ref<ShareInfo[]>([]);
|
|
134
176
|
const shareBusy = ref(false);
|
|
@@ -163,11 +205,10 @@ const dropShares = computed(() => shares.value.filter((s) => shareKind(s) === 'd
|
|
|
163
205
|
// on a server that keeps links for 7 would show a choice that is not one.
|
|
164
206
|
// The same helper drives every surface (see lib/shareTtl.ts).
|
|
165
207
|
function expiryLabel(days: number): string {
|
|
166
|
-
|
|
167
|
-
return tr.value ? `${days} gün` : `${days} day${days === 1 ? '' : 's'}`;
|
|
208
|
+
return days === 0 ? t('access.ui.never') : t('access.ui.days', { days });
|
|
168
209
|
}
|
|
169
210
|
const expiryOptions = computed(() => clampExpiryOptions(STOCK_EXPIRY_DAYS, props.shareMaxTtlDays, expiryLabel));
|
|
170
|
-
const ttlHint = computed(() => ttlCeilingHint(props.shareMaxTtlDays,
|
|
211
|
+
const ttlHint = computed(() => ttlCeilingHint(props.shareMaxTtlDays, localeCode.value));
|
|
171
212
|
watch(
|
|
172
213
|
() => props.shareMaxTtlDays,
|
|
173
214
|
() => {
|
|
@@ -185,14 +226,10 @@ watch(
|
|
|
185
226
|
// rather than a number box: "let this be opened three times" is the whole use
|
|
186
227
|
// case, and typing a number to say it is friction.
|
|
187
228
|
const shareMaxDl = ref(0); // 0 = unlimited
|
|
188
|
-
const maxDlOptions = [
|
|
189
|
-
{ v: 0, l:
|
|
190
|
-
{ v:
|
|
191
|
-
|
|
192
|
-
{ v: 5, l: L('5 indirme', '5 downloads') },
|
|
193
|
-
{ v: 10, l: L('10 indirme', '10 downloads') },
|
|
194
|
-
{ v: 25, l: L('25 indirme', '25 downloads') },
|
|
195
|
-
];
|
|
229
|
+
const maxDlOptions = computed(() => [
|
|
230
|
+
{ v: 0, l: t('access.ui.unlimited') },
|
|
231
|
+
...[1, 3, 5, 10, 25].map((count) => ({ v: count, l: t('access.ui.downloads', { count }) })),
|
|
232
|
+
]);
|
|
196
233
|
|
|
197
234
|
// ⚠ The one-line curl went missing the same way the download cap did: it was
|
|
198
235
|
// part of the old standalone share dialog, and link creation moved here
|
|
@@ -229,10 +266,11 @@ const dropMailNotice = ref('');
|
|
|
229
266
|
|
|
230
267
|
// splitEmails turns a free-text recipient field into a deduped address list —
|
|
231
268
|
// comma / semicolon / whitespace separated, so one input handles many people.
|
|
269
|
+
// ⚠ lib/listInput: an Arabic keyboard's "،" and "؛" separate addresses too.
|
|
232
270
|
function splitEmails(raw: string): string[] {
|
|
233
271
|
const seen = new Set<string>();
|
|
234
272
|
const out: string[] = [];
|
|
235
|
-
for (const part of raw
|
|
273
|
+
for (const part of splitList(raw, { spaces: true, semicolons: true })) {
|
|
236
274
|
const e = part.trim().toLowerCase();
|
|
237
275
|
if (e && e.includes('@') && !seen.has(e)) {
|
|
238
276
|
seen.add(e);
|
|
@@ -246,13 +284,19 @@ function splitEmails(raw: string): string[] {
|
|
|
246
284
|
function mailResultNotice(res: { sent?: string[]; failed?: string[] }): string {
|
|
247
285
|
const sent = res.sent?.length ?? 0;
|
|
248
286
|
const failed = res.failed?.length ?? 0;
|
|
249
|
-
if (failed === 0) return
|
|
250
|
-
return
|
|
287
|
+
if (failed === 0) return t('access.ui.mail_sent', { sent });
|
|
288
|
+
return t('access.ui.mail_partial', { sent, failed });
|
|
251
289
|
}
|
|
252
290
|
|
|
253
291
|
async function reload() {
|
|
254
292
|
loading.value = true;
|
|
255
293
|
err.value = '';
|
|
294
|
+
if (props.perm && props.perm !== 'owner') {
|
|
295
|
+
canManage.value = false;
|
|
296
|
+
open.value = { ...open.value, people: false };
|
|
297
|
+
loading.value = false;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
256
300
|
try {
|
|
257
301
|
const r = await props.api.listPermissions(props.path);
|
|
258
302
|
direct.value = r.direct ?? [];
|
|
@@ -331,6 +375,7 @@ function onEmailInput() {
|
|
|
331
375
|
}, 180);
|
|
332
376
|
}
|
|
333
377
|
async function pickUser(u: UserSuggestion) {
|
|
378
|
+
if (addBlocked.value) return;
|
|
334
379
|
showSuggest.value = false;
|
|
335
380
|
email.value = u.email;
|
|
336
381
|
let lvl = level.value;
|
|
@@ -342,7 +387,7 @@ async function pickUser(u: UserSuggestion) {
|
|
|
342
387
|
email.value = '';
|
|
343
388
|
suggestions.value = [];
|
|
344
389
|
await reload();
|
|
345
|
-
notice.value =
|
|
390
|
+
notice.value = t('access.ui.access_granted');
|
|
346
391
|
} catch (e) {
|
|
347
392
|
notice.value = e instanceof Error ? e.message : String(e);
|
|
348
393
|
} finally {
|
|
@@ -350,9 +395,10 @@ async function pickUser(u: UserSuggestion) {
|
|
|
350
395
|
}
|
|
351
396
|
}
|
|
352
397
|
async function submitEmail() {
|
|
398
|
+
if (addBlocked.value) return;
|
|
353
399
|
const addr = email.value.trim().toLowerCase();
|
|
354
400
|
if (!addr || !addr.includes('@')) {
|
|
355
|
-
notice.value =
|
|
401
|
+
notice.value = t('access.ui.enter_a_valid_email');
|
|
356
402
|
return;
|
|
357
403
|
}
|
|
358
404
|
showSuggest.value = false;
|
|
@@ -367,7 +413,7 @@ async function submitEmail() {
|
|
|
367
413
|
await props.api.addPermission({ path: props.path, user_id: res.user.id, level: lvl, is_dir: !!props.isDir });
|
|
368
414
|
email.value = '';
|
|
369
415
|
await reload();
|
|
370
|
-
notice.value =
|
|
416
|
+
notice.value = t('access.ui.access_granted');
|
|
371
417
|
} else {
|
|
372
418
|
noAccount.value = true;
|
|
373
419
|
}
|
|
@@ -388,8 +434,8 @@ async function inviteCreateUser() {
|
|
|
388
434
|
});
|
|
389
435
|
inviteResult.value = { tempPassword: r.temp_password };
|
|
390
436
|
notice.value = r.emailed
|
|
391
|
-
?
|
|
392
|
-
:
|
|
437
|
+
? t('access.ui.user_created_invite_emailed')
|
|
438
|
+
: t('access.ui.user_created_share_the_temp_password');
|
|
393
439
|
noAccount.value = false; email.value = '';
|
|
394
440
|
await reload();
|
|
395
441
|
} catch (e) {
|
|
@@ -420,11 +466,13 @@ async function removeGrant(g: Grant) {
|
|
|
420
466
|
catch (e) { notice.value = e instanceof Error ? e.message : String(e); }
|
|
421
467
|
finally { busy.value = false; }
|
|
422
468
|
}
|
|
469
|
+
/* One rule for naming a person (lib/personName): the server's name, the
|
|
470
|
+
display name, the username, the address. */
|
|
423
471
|
function glabel(g: Grant): string {
|
|
424
|
-
return g.user_display_name
|
|
472
|
+
return personName({ name: g.user_name, display_name: g.user_display_name, email: g.user_email }) || `#${g.user_id}`;
|
|
425
473
|
}
|
|
426
474
|
function ginitial(g: Grant): string {
|
|
427
|
-
return (g.user_display_name
|
|
475
|
+
return personInitial({ name: g.user_name, display_name: g.user_display_name, email: g.user_email }, localeTag(localeCode.value)) || '?';
|
|
428
476
|
}
|
|
429
477
|
|
|
430
478
|
// ── share actions ──
|
|
@@ -432,12 +480,23 @@ function expiresAtISO(): string | null {
|
|
|
432
480
|
if (!shareExpiry.value) return null;
|
|
433
481
|
return new Date(Date.now() + shareExpiry.value * 86400000).toISOString();
|
|
434
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Make THE link with the options as they stand.
|
|
485
|
+
*
|
|
486
|
+
* ⚠⚠ ONE download link per item, whichever control made it. The switch at the
|
|
487
|
+
* top mints one; this button used to mint ANOTHER beside it, so a person who
|
|
488
|
+
* turned the link on and then picked a PIN walked away with two live links —
|
|
489
|
+
* the old one without the PIN (QA, 2026-09-21). When a link is on, the button
|
|
490
|
+
* REPLACES it (its label and the note under it say so): the old address stops
|
|
491
|
+
* working and the new one carries the new options.
|
|
492
|
+
*/
|
|
435
493
|
async function createLink() {
|
|
436
494
|
shareBusy.value = true;
|
|
437
495
|
shareErr.value = '';
|
|
438
|
-
shareResult.value = null;
|
|
439
496
|
shareMailNotice.value = '';
|
|
440
497
|
try {
|
|
498
|
+
for (const s of downloadShares.value) await props.api.revokeShare(s.uuid);
|
|
499
|
+
shareResult.value = null;
|
|
441
500
|
const r = await props.api.createShare({
|
|
442
501
|
path: props.path,
|
|
443
502
|
password: sharePwd.value,
|
|
@@ -495,7 +554,7 @@ async function toggleLink() {
|
|
|
495
554
|
async function sendShareMail() {
|
|
496
555
|
const list = splitEmails(shareMailTo.value);
|
|
497
556
|
if (!list.length) {
|
|
498
|
-
shareMailNotice.value =
|
|
557
|
+
shareMailNotice.value = t('access.ui.enter_a_valid_email');
|
|
499
558
|
return;
|
|
500
559
|
}
|
|
501
560
|
if (!shareResult.value?.url) return;
|
|
@@ -514,9 +573,9 @@ async function sendShareMail() {
|
|
|
514
573
|
} catch (e) {
|
|
515
574
|
const detail = (e as { detail?: string }).detail ?? '';
|
|
516
575
|
if (detail.includes('not_configured')) {
|
|
517
|
-
shareMailNotice.value =
|
|
576
|
+
shareMailNotice.value = t('access.ui.smtp_not_set_up_verified_share_the_link');
|
|
518
577
|
} else if (detail.includes('send_failed')) {
|
|
519
|
-
shareMailNotice.value =
|
|
578
|
+
shareMailNotice.value = t('access.ui.send_failed_temporary_please_retry');
|
|
520
579
|
} else {
|
|
521
580
|
shareMailNotice.value = e instanceof Error ? e.message : String(e);
|
|
522
581
|
}
|
|
@@ -539,7 +598,7 @@ async function createDropLink() {
|
|
|
539
598
|
const drop_settings: Record<string, unknown> = { ask_name: dropAskName.value };
|
|
540
599
|
if (dropMaxFiles.value) drop_settings.max_files = Number(dropMaxFiles.value);
|
|
541
600
|
if (dropMaxSizeMB.value) drop_settings.max_file_size_mb = Number(dropMaxSizeMB.value);
|
|
542
|
-
const exts = dropAllowedExt.value
|
|
601
|
+
const exts = splitList(dropAllowedExt.value, { spaces: true }).map((s) => s.replace(/^\./, '')).filter(Boolean);
|
|
543
602
|
if (exts.length) drop_settings.allowed_ext = exts;
|
|
544
603
|
const r = await props.api.createShare({
|
|
545
604
|
path: props.path,
|
|
@@ -564,7 +623,7 @@ async function createDropLink() {
|
|
|
564
623
|
async function sendDropMail() {
|
|
565
624
|
const list = splitEmails(dropMailTo.value);
|
|
566
625
|
if (!list.length) {
|
|
567
|
-
dropMailNotice.value =
|
|
626
|
+
dropMailNotice.value = t('access.ui.enter_a_valid_email');
|
|
568
627
|
return;
|
|
569
628
|
}
|
|
570
629
|
if (!dropResult.value?.url) return;
|
|
@@ -583,7 +642,7 @@ async function sendDropMail() {
|
|
|
583
642
|
} catch (e) {
|
|
584
643
|
const detail = (e as { detail?: string }).detail ?? '';
|
|
585
644
|
if (detail.includes('not_configured')) {
|
|
586
|
-
dropMailNotice.value =
|
|
645
|
+
dropMailNotice.value = t('access.ui.smtp_not_set_up_verified_share_the_link');
|
|
587
646
|
} else {
|
|
588
647
|
dropMailNotice.value = e instanceof Error ? e.message : String(e);
|
|
589
648
|
}
|
|
@@ -619,28 +678,24 @@ function copy(text: string, tag = 'url') {
|
|
|
619
678
|
const canShare = computed(() => typeof navigator !== 'undefined' && typeof navigator.share === 'function');
|
|
620
679
|
|
|
621
680
|
/**
|
|
622
|
-
* The size line in the share message
|
|
681
|
+
* The size line in the share message — the product's one byte format, in the
|
|
682
|
+
* viewer's language.
|
|
623
683
|
*
|
|
624
|
-
* ⚠
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
* ⚠ It is therefore the one place in the UI that is NOT decimal, so a share
|
|
632
|
-
* message reads "1.4 MB" where the listing row reads "1.5 MB". The register
|
|
633
|
-
* entry for this was right about the real fix: the e-mail should render from
|
|
634
|
-
* one source rather than the UI copying its rounding.
|
|
684
|
+
* ⚠ It used to mirror the server's `humanSize()` (base 1024, always one
|
|
685
|
+
* decimal, a dot and English letters) so that the message forwarded by the OS
|
|
686
|
+
* share sheet and the one e-mailed would agree — and both disagreed with the
|
|
687
|
+
* listing: "1.4 MB" in the message, "1.5 MB" on the row. The server writes
|
|
688
|
+
* sizes with the interface's rules now (srvtext.Bytes), so this is simply the
|
|
689
|
+
* listing's formatter.
|
|
635
690
|
*/
|
|
636
691
|
function humanSize(b?: number): string {
|
|
637
692
|
if (!b || b <= 0) return '';
|
|
638
|
-
return
|
|
693
|
+
return formatSize(b);
|
|
639
694
|
}
|
|
640
695
|
|
|
641
696
|
function expiryLine(days: number): string {
|
|
642
|
-
if (days > 0) return
|
|
643
|
-
return
|
|
697
|
+
if (days > 0) return t('access.ui.valid_for', { days });
|
|
698
|
+
return t('share.ttl.never');
|
|
644
699
|
}
|
|
645
700
|
// What the server actually stored — shown under a fresh link so the real
|
|
646
701
|
// expiry is visible even when the server shortened the request.
|
|
@@ -648,7 +703,7 @@ function expiryLine(days: number): string {
|
|
|
648
703
|
const clock = inject(EXPLORER_CLOCK, undefined);
|
|
649
704
|
|
|
650
705
|
function validUntil(r: { expiresAt?: string | null } | null): string {
|
|
651
|
-
return validUntilLine(r?.expiresAt ?? null,
|
|
706
|
+
return validUntilLine(r?.expiresAt ?? null, localeCode.value, clock);
|
|
652
707
|
}
|
|
653
708
|
|
|
654
709
|
/* ── the top tier: one switch, one sentence, one link ──────────────────── */
|
|
@@ -657,6 +712,15 @@ function validUntil(r: { expiresAt?: string | null } | null): string {
|
|
|
657
712
|
const primaryLink = computed(() => shareResult.value?.url ?? downloadShares.value[0]?.url ?? '');
|
|
658
713
|
const linkOn = computed(() => !!primaryLink.value);
|
|
659
714
|
|
|
715
|
+
/**
|
|
716
|
+
* The links the header does NOT already show. ⚠ The header's link used to be
|
|
717
|
+
* listed again right under it, with a second Copy button — one link, two
|
|
718
|
+
* places (QA, 2026-09-21). What is left is an older extra link, kept listed
|
|
719
|
+
* so it can still be copied or revoked.
|
|
720
|
+
*/
|
|
721
|
+
const otherDownloadShares = computed(() => downloadShares.value.filter((s) => s.url !== primaryLink.value));
|
|
722
|
+
const otherDropShares = computed(() => dropShares.value.filter((s) => s.url !== dropResult.value?.url));
|
|
723
|
+
|
|
660
724
|
/**
|
|
661
725
|
* The plain sentence. It says who can open the item RIGHT NOW, which is the
|
|
662
726
|
* question the dialog exists to answer.
|
|
@@ -678,12 +742,12 @@ const linkDetail = computed(() => {
|
|
|
678
742
|
const bits: string[] = [];
|
|
679
743
|
if (shareResult.value) {
|
|
680
744
|
bits.push(validUntil(shareResult.value));
|
|
681
|
-
if (shareResult.value.clamped) bits.push(
|
|
682
|
-
if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
|
|
745
|
+
if (shareResult.value.clamped) bits.push(t('access.ui.server_limit_applied'));
|
|
746
|
+
if (shareMaxDl.value) bits.push(maxDlOptions.value.find((o) => o.v === shareMaxDl.value)?.l ?? '');
|
|
683
747
|
} else if (downloadShares.value[0]) {
|
|
684
748
|
const s = downloadShares.value[0];
|
|
685
749
|
bits.push(validUntil({ expiresAt: s.expires_at ?? null }));
|
|
686
|
-
if (s.max_downloads) bits.push(maxDlOptions.find((o) => o.v === s.max_downloads)?.l ?? String(s.max_downloads));
|
|
750
|
+
if (s.max_downloads) bits.push(maxDlOptions.value.find((o) => o.v === s.max_downloads)?.l ?? String(s.max_downloads));
|
|
687
751
|
}
|
|
688
752
|
return shareDetailLine(bits);
|
|
689
753
|
});
|
|
@@ -694,7 +758,7 @@ const linkSummary = computed(() => {
|
|
|
694
758
|
const bits: string[] = [];
|
|
695
759
|
bits.push(sharePwd.value ? t('access.sum.pin_on') : t('access.sum.pin_off'));
|
|
696
760
|
bits.push(expiryLabel(shareExpiry.value));
|
|
697
|
-
if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
|
|
761
|
+
if (shareMaxDl.value) bits.push(maxDlOptions.value.find((o) => o.v === shareMaxDl.value)?.l ?? '');
|
|
698
762
|
return bits.filter(Boolean).join(' · ');
|
|
699
763
|
});
|
|
700
764
|
const peopleSummary = computed(() => {
|
|
@@ -710,22 +774,21 @@ function shareBody(): { title: string; text: string } {
|
|
|
710
774
|
const name = pathParts.value.name;
|
|
711
775
|
const url = shareResult.value?.url ?? '';
|
|
712
776
|
const pin = shareResult.value?.pin ?? '';
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
: `${name} has been shared with you`;
|
|
777
|
+
// ⚠ A sentence per kind, not "a {kind}": a word dropped into another
|
|
778
|
+
// sentence cannot agree with it in a language with gender or case.
|
|
779
|
+
const title = t(props.isDir ? 'access.ui.mail_title_folder' : 'access.ui.mail_title_file', { name });
|
|
717
780
|
const lines: string[] = [];
|
|
718
|
-
lines.push(
|
|
719
|
-
lines.push(
|
|
781
|
+
lines.push(t('access.ui.mail_hello'), '');
|
|
782
|
+
lines.push(t(props.isDir ? 'access.ui.mail_shared_folder' : 'access.ui.mail_shared_file'), '');
|
|
720
783
|
if (props.isDir) {
|
|
721
|
-
lines.push(
|
|
784
|
+
lines.push(t('access.ui.mail_folder', { name }));
|
|
722
785
|
} else {
|
|
723
|
-
lines.push(
|
|
786
|
+
lines.push(t('access.ui.mail_file', { name }));
|
|
724
787
|
const sz = humanSize(props.size);
|
|
725
|
-
if (sz) lines.push(
|
|
788
|
+
if (sz) lines.push(t('access.ui.mail_size', { size: sz }));
|
|
726
789
|
}
|
|
727
|
-
lines.push('',
|
|
728
|
-
if (pin) lines.push('',
|
|
790
|
+
lines.push('', t('access.ui.mail_download_here'), url);
|
|
791
|
+
if (pin) lines.push('', t('access.ui.mail_pin', { pin }));
|
|
729
792
|
lines.push('', expiryLine(shareExpiry.value));
|
|
730
793
|
return { title, text: lines.join('\n') };
|
|
731
794
|
}
|
|
@@ -737,18 +800,18 @@ function dropBody(): { title: string; text: string } {
|
|
|
737
800
|
const pin = dropResult.value?.pin ?? '';
|
|
738
801
|
const maxFiles = Number(dropMaxFiles.value) || 20;
|
|
739
802
|
const maxMB = Number(dropMaxSizeMB.value) || 500;
|
|
740
|
-
const exts = dropAllowedExt.value
|
|
741
|
-
const title =
|
|
803
|
+
const exts = splitList(dropAllowedExt.value, { spaces: true }).map((s) => s.replace(/^\./, '')).filter(Boolean);
|
|
804
|
+
const title = t('access.ui.drop_title', { folder });
|
|
742
805
|
const lines: string[] = [];
|
|
743
|
-
lines.push(
|
|
744
|
-
lines.push(
|
|
745
|
-
lines.push(
|
|
746
|
-
lines.push(
|
|
806
|
+
lines.push(t('access.ui.mail_hello'), '');
|
|
807
|
+
lines.push(t('access.ui.drop_invited'), '');
|
|
808
|
+
lines.push(t('access.ui.mail_folder', { name: folder }));
|
|
809
|
+
lines.push(t('access.ui.drop_limit', { files: maxFiles, mb: maxMB }));
|
|
747
810
|
lines.push(exts.length
|
|
748
|
-
?
|
|
749
|
-
:
|
|
750
|
-
lines.push('',
|
|
751
|
-
if (pin) lines.push('',
|
|
811
|
+
? t('access.ui.drop_types', { types: exts.join(', ') })
|
|
812
|
+
: t('access.ui.drop_types_all'));
|
|
813
|
+
lines.push('', t('access.ui.drop_upload_here'), url);
|
|
814
|
+
if (pin) lines.push('', t('access.ui.mail_pin', { pin }));
|
|
752
815
|
lines.push('', expiryLine(dropExpiry.value));
|
|
753
816
|
return { title, text: lines.join('\n') };
|
|
754
817
|
}
|
|
@@ -772,7 +835,7 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
772
835
|
<span class="fe-share__tile" aria-hidden="true" v-html="titleTile"></span>
|
|
773
836
|
<div class="fe-share__headtext">
|
|
774
837
|
<h3 class="fe-share__title">{{ t('access.title', { name: pathParts.name }) }}</h3>
|
|
775
|
-
<span class="fe-share__path" :title="path">{{ pathParts.adapter }}</span>
|
|
838
|
+
<span class="fe-share__path" :title="path"><bdi>{{ pathParts.adapter }}</bdi></span>
|
|
776
839
|
</div>
|
|
777
840
|
<button type="button" class="fe-share__close" :aria-label="t('access.close')" :title="t('access.close')" @click="emit('close')">
|
|
778
841
|
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
@@ -810,8 +873,9 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
810
873
|
<div v-if="shareResult?.pin" class="fe-share__pinrow">
|
|
811
874
|
<span class="fe-share__pinlabel">PIN</span>
|
|
812
875
|
<code class="fe-share__pin">{{ shareResult?.pin }}</code>
|
|
813
|
-
<button type="button" class="fe-
|
|
814
|
-
|
|
876
|
+
<button type="button" class="fe-share__copy" @click="copy(shareResult?.pin ?? '', 'sharepin')">
|
|
877
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
878
|
+
<span>{{ copied === 'sharepin' ? t('access.copied') : t('access.copy') }}</span>
|
|
815
879
|
</button>
|
|
816
880
|
</div>
|
|
817
881
|
</template>
|
|
@@ -837,72 +901,97 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
837
901
|
</button>
|
|
838
902
|
<div v-if="open.link" class="fe-share__panel">
|
|
839
903
|
<div class="fe-share__opts">
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
904
|
+
<!-- ⚠ ONE on/off control in this dialog: the switch. The PIN
|
|
905
|
+
used to be a native checkbox under a switch — two shapes
|
|
906
|
+
for one kind of answer (QA, 2026-09-21). -->
|
|
907
|
+
<label class="fe-share__togglerow">
|
|
908
|
+
<span class="fe-share__togglelabel">{{ t('access.ui.protect_with_a_pin') }}</span>
|
|
909
|
+
<button
|
|
910
|
+
type="button"
|
|
911
|
+
class="fe-share__switch"
|
|
912
|
+
role="switch"
|
|
913
|
+
:aria-checked="sharePwd ? 'true' : 'false'"
|
|
914
|
+
data-testid="share-pin-switch"
|
|
915
|
+
@click="sharePwd = !sharePwd"
|
|
916
|
+
><span class="fe-share__knob"></span></button>
|
|
843
917
|
</label>
|
|
844
918
|
<label class="fe-share__field">
|
|
845
|
-
<span class="fe-share__fieldlabel">{{
|
|
919
|
+
<span class="fe-share__fieldlabel">{{ t('access.ui.expiry') }}</span>
|
|
846
920
|
<select v-model.number="shareExpiry" class="fe-share__select" data-testid="share-expiry">
|
|
847
921
|
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
848
922
|
</select>
|
|
849
923
|
</label>
|
|
850
924
|
<label class="fe-share__field">
|
|
851
|
-
<span class="fe-share__fieldlabel">{{
|
|
925
|
+
<span class="fe-share__fieldlabel">{{ t('access.ui.download_limit') }}</span>
|
|
852
926
|
<select v-model.number="shareMaxDl" class="fe-share__select">
|
|
853
927
|
<option v-for="o in maxDlOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
854
928
|
</select>
|
|
855
929
|
</label>
|
|
856
930
|
</div>
|
|
857
931
|
<p v-if="ttlHint" class="fe-share__hint" data-testid="share-ttl-hint">{{ ttlHint }}</p>
|
|
858
|
-
<button
|
|
859
|
-
|
|
932
|
+
<button
|
|
933
|
+
type="button"
|
|
934
|
+
class="fx-perm-create fe-share__btn fe-share__btn--primary fe-share__btn--wide"
|
|
935
|
+
data-testid="share-create"
|
|
936
|
+
:disabled="shareBusy"
|
|
937
|
+
@click="createLink"
|
|
938
|
+
>
|
|
939
|
+
{{ linkOn ? t('access.ui.replace_link') : t('access.ui.create_link') }}
|
|
860
940
|
</button>
|
|
941
|
+
<p v-if="linkOn" class="fe-share__hint" data-testid="share-replace-hint">{{ t('access.ui.replace_link_hint') }}</p>
|
|
861
942
|
|
|
943
|
+
<!-- ⚠ One message with the address IN it: the sentence used to
|
|
944
|
+
be two halves around a <strong>, which put a space before
|
|
945
|
+
the English full stop and could not be ordered by a
|
|
946
|
+
language that puts the address elsewhere. -->
|
|
862
947
|
<p v-if="!shareResult && shareMailTo" class="fe-share__hint">
|
|
863
|
-
{{
|
|
864
|
-
<strong>{{ shareMailTo }}</strong> {{ L('adresine gönderin.', '.') }}
|
|
948
|
+
{{ t('access.ui.create_then_send', { email: shareMailTo }) }}
|
|
865
949
|
</p>
|
|
866
950
|
|
|
867
951
|
<template v-if="shareResult">
|
|
868
952
|
<!-- one-line download command, for pulling the file onto a server -->
|
|
869
953
|
<div class="fx-perm-cli fe-share__cli">
|
|
870
|
-
<span class="fe-share__fieldlabel">{{
|
|
954
|
+
<span class="fe-share__fieldlabel">{{ t('access.ui.command_line') }}</span>
|
|
871
955
|
<div class="fe-share__clirow">
|
|
872
956
|
<code class="fe-share__clicmd" :title="shareCli">{{ shareCli }}</code>
|
|
873
|
-
<button type="button" class="fe-
|
|
874
|
-
|
|
957
|
+
<button type="button" class="fe-share__copy" @click="copy(shareCli, 'sharecli')">
|
|
958
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
959
|
+
<span>{{ copied === 'sharecli' ? t('access.copied') : t('access.copy') }}</span>
|
|
875
960
|
</button>
|
|
876
961
|
</div>
|
|
877
962
|
</div>
|
|
878
963
|
|
|
879
964
|
<!-- send by email (one or more, comma/space separated) -->
|
|
880
|
-
<div class="fe-share__mailrow">
|
|
965
|
+
<div v-if="mailRowShown" class="fe-share__mailrow" data-testid="share-mail-row">
|
|
881
966
|
<input v-model="shareMailTo" type="text" class="fe-share__input" autocomplete="off"
|
|
882
|
-
:
|
|
883
|
-
|
|
884
|
-
|
|
967
|
+
:disabled="mailMissing"
|
|
968
|
+
:placeholder="t('access.ui.emails_comma_separated')" @keyup.enter="sendShareMail" />
|
|
969
|
+
<button type="button" class="fe-share__btn" :disabled="shareMailBusy || mailMissing" @click="sendShareMail">
|
|
970
|
+
{{ t('access.ui.send') }}
|
|
885
971
|
</button>
|
|
886
972
|
</div>
|
|
973
|
+
<div v-if="mailRowShown && mailMissing" class="fe-share__notice" data-testid="share-mail-unavailable">
|
|
974
|
+
{{ t('access.ui.mail_not_set_up') }}
|
|
975
|
+
</div>
|
|
887
976
|
<div v-if="shareMailNotice" class="fe-share__notice">{{ shareMailNotice }}</div>
|
|
888
977
|
|
|
889
978
|
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
890
979
|
<button v-if="canShare" type="button" class="fe-share__btn fe-share__btn--wide" @click="nativeShare(shareBody())">
|
|
891
980
|
<span aria-hidden="true" v-html="actionIconSvg('access')"></span>
|
|
892
|
-
<span>{{
|
|
981
|
+
<span>{{ t('access.ui.share') }}</span>
|
|
893
982
|
</button>
|
|
894
983
|
</template>
|
|
895
984
|
|
|
896
|
-
<div class="fe-share__list">
|
|
897
|
-
<h4 class="fe-share__listhead">{{
|
|
898
|
-
<
|
|
899
|
-
<div v-for="s in downloadShares" :key="s.uuid" class="fe-share__row">
|
|
985
|
+
<div v-if="otherDownloadShares.length" class="fe-share__list" data-testid="share-other-links">
|
|
986
|
+
<h4 class="fe-share__listhead">{{ t('access.ui.other_links') }}</h4>
|
|
987
|
+
<div v-for="s in otherDownloadShares" :key="s.uuid" class="fe-share__row">
|
|
900
988
|
<span class="fe-share__url" :title="s.url">{{ s.url }}</span>
|
|
901
|
-
<button type="button" class="fe-
|
|
902
|
-
|
|
989
|
+
<button type="button" class="fe-share__copy" @click="copy(s.url, s.uuid)">
|
|
990
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
991
|
+
<span>{{ copied === s.uuid ? t('access.copied') : t('access.copy') }}</span>
|
|
903
992
|
</button>
|
|
904
|
-
<button type="button" class="fe-share__del" :disabled="shareBusy" :title="
|
|
905
|
-
:aria-label="
|
|
993
|
+
<button type="button" class="fe-share__del" :disabled="shareBusy" :title="t('access.ui.revoke')"
|
|
994
|
+
:aria-label="t('access.ui.revoke')" @click="revoke(s)">
|
|
906
995
|
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
907
996
|
</button>
|
|
908
997
|
</div>
|
|
@@ -911,7 +1000,7 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
911
1000
|
</section>
|
|
912
1001
|
|
|
913
1002
|
<!-- People with access: the per-user / inherited grants -->
|
|
914
|
-
<section v-if="
|
|
1003
|
+
<section v-if="peopleShown" class="fe-share__section" data-testid="share-people">
|
|
915
1004
|
<button
|
|
916
1005
|
type="button"
|
|
917
1006
|
class="fe-share__sechead"
|
|
@@ -926,59 +1015,60 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
926
1015
|
<span class="fe-share__secmeta">{{ peopleSummary }}</span>
|
|
927
1016
|
</button>
|
|
928
1017
|
<div v-if="open.people" class="fe-share__panel">
|
|
929
|
-
<div v-if="
|
|
930
|
-
{{
|
|
1018
|
+
<div v-if="addBlocked" class="fe-share__warn" data-testid="share-rbac-off">
|
|
1019
|
+
{{ t('access.ui.rbac_off_here') }}
|
|
931
1020
|
</div>
|
|
932
1021
|
<div v-if="err" class="fe-share__warn">{{ err }}</div>
|
|
933
|
-
<p v-if="loading" class="fe-share__hint">{{
|
|
1022
|
+
<p v-if="loading" class="fe-share__hint">{{ t('access.ui.loading') }}</p>
|
|
934
1023
|
<template v-else>
|
|
935
|
-
<div class="fe-share__add">
|
|
1024
|
+
<div class="fe-share__add" data-testid="share-add-person">
|
|
936
1025
|
<div class="fe-share__emailwrap">
|
|
937
1026
|
<input v-model="email" type="email" class="fe-share__input" autocomplete="off"
|
|
938
|
-
:
|
|
1027
|
+
:disabled="addBlocked"
|
|
1028
|
+
:placeholder="t('access.ui.name_or_email')"
|
|
939
1029
|
@input="onEmailInput" @keyup.enter="submitEmail" @focus="onEmailInput" />
|
|
940
1030
|
<ul v-if="showSuggest" class="fe-share__suggest">
|
|
941
1031
|
<li v-for="u in suggestions" :key="u.id" @mousedown.prevent="pickUser(u)">
|
|
942
|
-
<span class="fe-share__av fe-share__av--sm">{{ (u
|
|
1032
|
+
<span class="fe-share__av fe-share__av--sm">{{ personInitial(u, localeTag(localeCode)) }}</span>
|
|
943
1033
|
<span class="fe-share__suggesttxt">
|
|
944
|
-
<span class="fe-share__suggestname">{{ u
|
|
945
|
-
<span class="fe-share__suggestmeta">{{ u.email }} · {{ u.role }}</span>
|
|
1034
|
+
<span class="fe-share__suggestname">{{ personName(u) }}</span>
|
|
1035
|
+
<span class="fe-share__suggestmeta"><bdi>{{ u.email }}</bdi> · {{ u.role }}</span>
|
|
946
1036
|
</span>
|
|
947
1037
|
</li>
|
|
948
1038
|
</ul>
|
|
949
1039
|
</div>
|
|
950
|
-
<select v-model="level" class="fe-share__select" :title="levels.find(o => o.v === level)?.d">
|
|
1040
|
+
<select v-model="level" class="fe-share__select" :disabled="addBlocked" :title="levels.find(o => o.v === level)?.d">
|
|
951
1041
|
<option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
952
1042
|
</select>
|
|
953
|
-
<button type="button" class="fe-share__btn fe-share__btn--primary" :disabled="busy" @click="submitEmail">
|
|
954
|
-
{{
|
|
1043
|
+
<button type="button" class="fe-share__btn fe-share__btn--primary" :disabled="busy || addBlocked" @click="submitEmail">
|
|
1044
|
+
{{ t('access.ui.add') }}
|
|
955
1045
|
</button>
|
|
956
1046
|
</div>
|
|
957
1047
|
|
|
958
1048
|
<div v-if="noAccount" class="fe-share__invite">
|
|
959
|
-
<p class="fe-share__hint">{{
|
|
1049
|
+
<p class="fe-share__hint">{{ t('access.ui.no_account_for_this_email_what_next') }}</p>
|
|
960
1050
|
<div class="fe-share__inviterow">
|
|
961
1051
|
<select v-model="createRole" class="fe-share__select">
|
|
962
|
-
<option value="user">{{
|
|
963
|
-
<option value="viewer">{{
|
|
1052
|
+
<option value="user">{{ t('access.ui.user') }}</option>
|
|
1053
|
+
<option value="viewer">{{ t('access.ui.viewer') }}</option>
|
|
964
1054
|
</select>
|
|
965
1055
|
<button type="button" class="fe-share__btn fe-share__btn--primary" :disabled="busy" @click="inviteCreateUser">
|
|
966
|
-
{{
|
|
1056
|
+
{{ t('access.ui.create_user_grant') }}
|
|
967
1057
|
</button>
|
|
968
1058
|
</div>
|
|
969
1059
|
<button type="button" class="fe-share__linkbtn" :disabled="busy" @click="gotoShareWithMail">
|
|
970
|
-
{{
|
|
1060
|
+
{{ t('access.ui.just_send_a_share_link') }}
|
|
971
1061
|
</button>
|
|
972
1062
|
</div>
|
|
973
1063
|
<div v-if="notice" class="fe-share__notice">{{ notice }}</div>
|
|
974
1064
|
<div v-if="inviteResult?.tempPassword" class="fe-share__notice">
|
|
975
|
-
{{
|
|
1065
|
+
{{ t('access.ui.temp_password') }} <code class="fe-share__pin">{{ inviteResult.tempPassword }}</code>
|
|
976
1066
|
</div>
|
|
977
1067
|
|
|
978
1068
|
<div class="fe-share__list">
|
|
979
|
-
<h4 class="fe-share__listhead">{{
|
|
1069
|
+
<h4 class="fe-share__listhead">{{ t('access.ui.people_with_access') }}</h4>
|
|
980
1070
|
<p v-if="!direct.length && !inherited.length" class="fe-share__empty">
|
|
981
|
-
{{
|
|
1071
|
+
{{ t('access.ui.not_shared_with_anyone_yet') }}
|
|
982
1072
|
</p>
|
|
983
1073
|
<div v-for="g in direct" :key="'d' + g.id" class="fe-share__row">
|
|
984
1074
|
<span class="fe-share__av">{{ ginitial(g) }}</span>
|
|
@@ -987,8 +1077,8 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
987
1077
|
@change="changeLevel(g, ($event.target as HTMLSelectElement).value)">
|
|
988
1078
|
<option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
989
1079
|
</select>
|
|
990
|
-
<button type="button" class="fe-share__del" :disabled="busy" :title="
|
|
991
|
-
:aria-label="
|
|
1080
|
+
<button type="button" class="fe-share__del" :disabled="busy" :title="t('access.ui.remove')"
|
|
1081
|
+
:aria-label="t('access.ui.remove')" @click="removeGrant(g)">
|
|
992
1082
|
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
993
1083
|
</button>
|
|
994
1084
|
</div>
|
|
@@ -996,7 +1086,7 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
996
1086
|
<span class="fe-share__av fe-share__av--dim">{{ ginitial(g) }}</span>
|
|
997
1087
|
<span class="fe-share__person" :title="g.user_email">{{ glabel(g) }}</span>
|
|
998
1088
|
<span class="fe-share__badge">{{ levelLabel(g.level) }}</span>
|
|
999
|
-
<span class="fe-share__from" :title="
|
|
1089
|
+
<span class="fe-share__from" :title="t('access.ui.inherited_from') + ': ' + (g.path_prefix || '/')">
|
|
1000
1090
|
{{ g.path_prefix || '/' }}
|
|
1001
1091
|
</span>
|
|
1002
1092
|
</div>
|
|
@@ -1022,15 +1112,22 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
1022
1112
|
</button>
|
|
1023
1113
|
<div v-if="open.drop" class="fe-share__panel">
|
|
1024
1114
|
<p class="fe-share__hint">
|
|
1025
|
-
{{
|
|
1115
|
+
{{ t('access.ui.a_public_link_that_lets_anyone_upload_fi') }}
|
|
1026
1116
|
</p>
|
|
1027
1117
|
<div class="fe-share__opts">
|
|
1028
|
-
<label class="fe-
|
|
1029
|
-
<
|
|
1030
|
-
<
|
|
1118
|
+
<label class="fe-share__togglerow">
|
|
1119
|
+
<span class="fe-share__togglelabel">{{ t('access.ui.protect_with_a_pin') }}</span>
|
|
1120
|
+
<button
|
|
1121
|
+
type="button"
|
|
1122
|
+
class="fe-share__switch"
|
|
1123
|
+
role="switch"
|
|
1124
|
+
:aria-checked="dropPwd ? 'true' : 'false'"
|
|
1125
|
+
data-testid="drop-pin-switch"
|
|
1126
|
+
@click="dropPwd = !dropPwd"
|
|
1127
|
+
><span class="fe-share__knob"></span></button>
|
|
1031
1128
|
</label>
|
|
1032
1129
|
<label class="fe-share__field">
|
|
1033
|
-
<span class="fe-share__fieldlabel">{{
|
|
1130
|
+
<span class="fe-share__fieldlabel">{{ t('access.ui.expiry') }}</span>
|
|
1034
1131
|
<select v-model.number="dropExpiry" class="fe-share__select" data-testid="drop-expiry">
|
|
1035
1132
|
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
1036
1133
|
</select>
|
|
@@ -1038,28 +1135,35 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
1038
1135
|
</div>
|
|
1039
1136
|
<p v-if="ttlHint" class="fe-share__hint">{{ ttlHint }}</p>
|
|
1040
1137
|
<button type="button" class="fe-share__btn fe-share__btn--primary fe-share__btn--wide" data-testid="drop-create" :disabled="dropBusy" @click="createDropLink">
|
|
1041
|
-
{{
|
|
1138
|
+
{{ t('access.ui.create_link') }}
|
|
1042
1139
|
</button>
|
|
1043
1140
|
|
|
1044
1141
|
<button type="button" class="fe-share__linkbtn" :aria-expanded="dropShowAdv ? 'true' : 'false'" @click="dropShowAdv = !dropShowAdv">
|
|
1045
|
-
{{ dropShowAdv ?
|
|
1142
|
+
{{ dropShowAdv ? t('access.ui.hide_upload_limits') : t('access.ui.upload_limits') }}
|
|
1046
1143
|
</button>
|
|
1047
1144
|
<div v-if="dropShowAdv" class="fe-share__adv">
|
|
1048
1145
|
<label class="fe-share__advrow">
|
|
1049
|
-
<span class="fe-share__fieldlabel">{{
|
|
1146
|
+
<span class="fe-share__fieldlabel">{{ t('access.ui.max_files') }}</span>
|
|
1050
1147
|
<input v-model="dropMaxFiles" type="number" min="1" class="fe-share__input fe-share__input--sm" placeholder="20" />
|
|
1051
1148
|
</label>
|
|
1052
1149
|
<label class="fe-share__advrow">
|
|
1053
|
-
<span class="fe-share__fieldlabel">{{
|
|
1150
|
+
<span class="fe-share__fieldlabel">{{ t('access.ui.mb_file') }}</span>
|
|
1054
1151
|
<input v-model="dropMaxSizeMB" type="number" min="1" class="fe-share__input fe-share__input--sm" placeholder="500" />
|
|
1055
1152
|
</label>
|
|
1056
1153
|
<label class="fe-share__advrow">
|
|
1057
|
-
<span class="fe-share__fieldlabel">{{
|
|
1058
|
-
<input v-model="dropAllowedExt" type="text" class="fe-share__input fe-share__input--sm" :placeholder="
|
|
1154
|
+
<span class="fe-share__fieldlabel">{{ t('access.ui.allowed_types') }}</span>
|
|
1155
|
+
<input v-model="dropAllowedExt" type="text" class="fe-share__input fe-share__input--sm" :placeholder="t('access.ui.all_e_g_pdf_jpg')" />
|
|
1059
1156
|
</label>
|
|
1060
|
-
<label class="fe-
|
|
1061
|
-
<
|
|
1062
|
-
<
|
|
1157
|
+
<label class="fe-share__togglerow">
|
|
1158
|
+
<span class="fe-share__togglelabel">{{ t('access.ui.ask_uploader_name') }}</span>
|
|
1159
|
+
<button
|
|
1160
|
+
type="button"
|
|
1161
|
+
class="fe-share__switch"
|
|
1162
|
+
role="switch"
|
|
1163
|
+
:aria-checked="dropAskName ? 'true' : 'false'"
|
|
1164
|
+
data-testid="drop-askname-switch"
|
|
1165
|
+
@click="dropAskName = !dropAskName"
|
|
1166
|
+
><span class="fe-share__knob"></span></button>
|
|
1063
1167
|
</label>
|
|
1064
1168
|
</div>
|
|
1065
1169
|
|
|
@@ -1075,43 +1179,48 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
1075
1179
|
</div>
|
|
1076
1180
|
<p class="fe-share__detail">
|
|
1077
1181
|
{{ validUntil(dropResult) }}
|
|
1078
|
-
<span v-if="dropResult.clamped">{{
|
|
1182
|
+
<span v-if="dropResult.clamped">{{ t('access.ui.server_limit_applied') }}</span>
|
|
1079
1183
|
</p>
|
|
1080
1184
|
<div v-if="dropResult.pin" class="fe-share__pinrow">
|
|
1081
1185
|
<span class="fe-share__pinlabel">PIN</span>
|
|
1082
1186
|
<code class="fe-share__pin">{{ dropResult.pin }}</code>
|
|
1083
|
-
<button type="button" class="fe-
|
|
1084
|
-
|
|
1187
|
+
<button type="button" class="fe-share__copy" @click="copy(dropResult.pin, 'droppin')">
|
|
1188
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
1189
|
+
<span>{{ copied === 'droppin' ? t('access.copied') : t('access.copy') }}</span>
|
|
1085
1190
|
</button>
|
|
1086
1191
|
</div>
|
|
1087
1192
|
|
|
1088
1193
|
<!-- email the upload link to one or more people -->
|
|
1089
|
-
<div class="fe-share__mailrow">
|
|
1194
|
+
<div v-if="mailRowShown" class="fe-share__mailrow">
|
|
1090
1195
|
<input v-model="dropMailTo" type="text" class="fe-share__input" autocomplete="off"
|
|
1091
|
-
:
|
|
1092
|
-
|
|
1093
|
-
|
|
1196
|
+
:disabled="mailMissing"
|
|
1197
|
+
:placeholder="t('access.ui.emails_comma_separated')" @keyup.enter="sendDropMail" />
|
|
1198
|
+
<button type="button" class="fe-share__btn" :disabled="dropMailBusy || mailMissing" @click="sendDropMail">
|
|
1199
|
+
{{ t('access.ui.send') }}
|
|
1094
1200
|
</button>
|
|
1095
1201
|
</div>
|
|
1202
|
+
<div v-if="mailRowShown && mailMissing" class="fe-share__notice">
|
|
1203
|
+
{{ t('access.ui.mail_not_set_up') }}
|
|
1204
|
+
</div>
|
|
1096
1205
|
<div v-if="dropMailNotice" class="fe-share__notice">{{ dropMailNotice }}</div>
|
|
1097
1206
|
|
|
1098
1207
|
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
1099
1208
|
<button v-if="canShare" type="button" class="fe-share__btn fe-share__btn--wide" @click="nativeShare(dropBody())">
|
|
1100
1209
|
<span aria-hidden="true" v-html="actionIconSvg('access')"></span>
|
|
1101
|
-
<span>{{
|
|
1210
|
+
<span>{{ t('access.ui.share') }}</span>
|
|
1102
1211
|
</button>
|
|
1103
1212
|
</template>
|
|
1104
1213
|
|
|
1105
|
-
<div class="fe-share__list">
|
|
1106
|
-
<h4 class="fe-share__listhead">{{
|
|
1107
|
-
<
|
|
1108
|
-
<div v-for="s in dropShares" :key="s.uuid" class="fe-share__row">
|
|
1214
|
+
<div v-if="otherDropShares.length" class="fe-share__list" data-testid="drop-other-links">
|
|
1215
|
+
<h4 class="fe-share__listhead">{{ dropResult ? t('access.ui.other_links') : t('access.ui.existing_links') }}</h4>
|
|
1216
|
+
<div v-for="s in otherDropShares" :key="s.uuid" class="fe-share__row">
|
|
1109
1217
|
<span class="fe-share__url" :title="s.url">{{ s.url }}</span>
|
|
1110
|
-
<button type="button" class="fe-
|
|
1111
|
-
|
|
1218
|
+
<button type="button" class="fe-share__copy" @click="copy(s.url, s.uuid)">
|
|
1219
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
1220
|
+
<span>{{ copied === s.uuid ? t('access.copied') : t('access.copy') }}</span>
|
|
1112
1221
|
</button>
|
|
1113
|
-
<button type="button" class="fe-share__del" :disabled="shareBusy" :title="
|
|
1114
|
-
:aria-label="
|
|
1222
|
+
<button type="button" class="fe-share__del" :disabled="shareBusy" :title="t('access.ui.revoke')"
|
|
1223
|
+
:aria-label="t('access.ui.revoke')" @click="revoke(s)">
|
|
1115
1224
|
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
1116
1225
|
</button>
|
|
1117
1226
|
</div>
|