@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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SurfacePeoplePicker — a plugin surface's `people-picker` node: who a
|
|
4
|
+
* document goes to. Chips for the people already chosen, one box to add
|
|
5
|
+
* the next.
|
|
6
|
+
*
|
|
7
|
+
* Internal users come from `GET /api/files/plugins/users?plugin=&q=`, which
|
|
8
|
+
* answers only for a plugin that holds `users:lookup`. A 403 (or a 404 from
|
|
9
|
+
* a server without the route) turns the lookup off for the life of the
|
|
10
|
+
* node and the box becomes plain e-mail entry — the contract's fallback,
|
|
11
|
+
* and the reason the box never says "no results" for a refusal.
|
|
12
|
+
*
|
|
13
|
+
* Free e-mail entry is offered when the plugin allows external people OR
|
|
14
|
+
* the lookup is off: with neither a way to name a user nor a way to type
|
|
15
|
+
* one, the node would be a wall.
|
|
16
|
+
*/
|
|
17
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
18
|
+
import type { LocaleCode } from '../../../types/ExplorerConfig';
|
|
19
|
+
import type { FileApi } from '../../../composables/useFileApi';
|
|
20
|
+
import type { PluginPerson } from '../../../types/Plugins';
|
|
21
|
+
import { useLocale } from '../../../composables/useLocale';
|
|
22
|
+
import { personName } from '../../../lib/personName';
|
|
23
|
+
import { looksLikeEmail } from '../../../lib/surfaceValues';
|
|
24
|
+
import { isListSeparatorKey } from '../../../lib/listInput';
|
|
25
|
+
|
|
26
|
+
const props = defineProps<{
|
|
27
|
+
id: string;
|
|
28
|
+
modelValue: PluginPerson[];
|
|
29
|
+
multi?: boolean;
|
|
30
|
+
allowExternal?: boolean;
|
|
31
|
+
/** Absent: no lookup at all — free e-mail entry only. */
|
|
32
|
+
api?: Pick<FileApi, 'pluginUsers'>;
|
|
33
|
+
/** The plugin the lookup is asked for. */
|
|
34
|
+
plugin?: string;
|
|
35
|
+
locale: LocaleCode;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
invalid?: boolean;
|
|
38
|
+
}>();
|
|
39
|
+
|
|
40
|
+
const emit = defineEmits<{
|
|
41
|
+
(e: 'update:modelValue', v: PluginPerson[]): void;
|
|
42
|
+
}>();
|
|
43
|
+
|
|
44
|
+
const { t } = useLocale(() => props.locale);
|
|
45
|
+
|
|
46
|
+
const query = ref('');
|
|
47
|
+
const suggestions = ref<PluginPerson[]>([]);
|
|
48
|
+
const showSuggest = ref(false);
|
|
49
|
+
/** The server refused (403) or has no route (404): free e-mail entry only. */
|
|
50
|
+
const lookupOff = ref(!props.api || !props.plugin);
|
|
51
|
+
const notice = ref('');
|
|
52
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
53
|
+
let lastQuery = 0;
|
|
54
|
+
|
|
55
|
+
const people = computed(() => props.modelValue ?? []);
|
|
56
|
+
const freeEntry = computed(() => props.allowExternal === true || lookupOff.value);
|
|
57
|
+
const full = computed(() => props.multi !== true && people.value.length >= 1);
|
|
58
|
+
|
|
59
|
+
/* One rule for naming a person (lib/personName). */
|
|
60
|
+
function personLabel(p: PluginPerson): string {
|
|
61
|
+
return personName({ name: p.name, email: p.email });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function add(p: PluginPerson) {
|
|
65
|
+
const email = String(p.email ?? '').trim().toLowerCase();
|
|
66
|
+
if (!email) return;
|
|
67
|
+
const rest = props.multi === true ? people.value.filter((x) => x.email.toLowerCase() !== email) : [];
|
|
68
|
+
emit('update:modelValue', [...rest, { ...p, email }]);
|
|
69
|
+
query.value = '';
|
|
70
|
+
suggestions.value = [];
|
|
71
|
+
showSuggest.value = false;
|
|
72
|
+
notice.value = '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function remove(email: string) {
|
|
76
|
+
emit('update:modelValue', people.value.filter((x) => x.email !== email));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onInput() {
|
|
80
|
+
notice.value = '';
|
|
81
|
+
const q = query.value.trim();
|
|
82
|
+
if (timer) clearTimeout(timer);
|
|
83
|
+
if (!q || lookupOff.value) {
|
|
84
|
+
suggestions.value = [];
|
|
85
|
+
showSuggest.value = false;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
timer = setTimeout(() => void search(q), 180);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function search(q: string) {
|
|
92
|
+
if (!props.api || !props.plugin || lookupOff.value) return;
|
|
93
|
+
const mine = ++lastQuery;
|
|
94
|
+
try {
|
|
95
|
+
const r = await props.api.pluginUsers(props.plugin, q);
|
|
96
|
+
if (mine !== lastQuery) return;
|
|
97
|
+
suggestions.value = (r.users ?? []).filter((u) => u && typeof u.email === 'string');
|
|
98
|
+
showSuggest.value = suggestions.value.length > 0;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if (mine !== lastQuery) return;
|
|
101
|
+
const status = (e as { status?: number })?.status;
|
|
102
|
+
if (status === 403 || status === 404) lookupOff.value = true;
|
|
103
|
+
suggestions.value = [];
|
|
104
|
+
showSuggest.value = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Enter / comma: the typed address, when free entry is allowed. */
|
|
109
|
+
function commit() {
|
|
110
|
+
const q = query.value.trim();
|
|
111
|
+
if (!q) return;
|
|
112
|
+
if (!freeEntry.value) {
|
|
113
|
+
notice.value = t('plugin.people.external_off');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!looksLikeEmail(q)) {
|
|
117
|
+
notice.value = t('plugin.people.invalid_email');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
add({ email: q });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function onKeydown(ev: KeyboardEvent) {
|
|
124
|
+
// ⚠ "،" / "," commit too (lib/listInput — an Arabic or CJK keyboard).
|
|
125
|
+
if (ev.key === 'Enter' || isListSeparatorKey(ev.key)) {
|
|
126
|
+
ev.preventDefault();
|
|
127
|
+
commit();
|
|
128
|
+
} else if (ev.key === 'Backspace' && query.value === '' && people.value.length) {
|
|
129
|
+
remove(people.value[people.value.length - 1].email);
|
|
130
|
+
} else if (ev.key === 'Escape') {
|
|
131
|
+
showSuggest.value = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function onBlur() {
|
|
136
|
+
// Let a mousedown on a suggestion land first.
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
showSuggest.value = false;
|
|
139
|
+
}, 120);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
watch(
|
|
143
|
+
() => [props.api, props.plugin] as const,
|
|
144
|
+
([api, plugin]) => {
|
|
145
|
+
if (api && plugin) lookupOff.value = false;
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
onBeforeUnmount(() => {
|
|
150
|
+
if (timer) clearTimeout(timer);
|
|
151
|
+
});
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<template>
|
|
155
|
+
<div class="fe-speople" :class="{ 'is-invalid': invalid }" data-testid="surface-people">
|
|
156
|
+
<ul v-if="people.length" class="fe-speople__chips" data-testid="surface-people-chips">
|
|
157
|
+
<li v-for="p in people" :key="p.email" class="fe-speople__chip" :title="p.email">
|
|
158
|
+
<span class="fe-speople__av" aria-hidden="true">{{ personLabel(p).charAt(0).toUpperCase() }}</span>
|
|
159
|
+
<span class="fe-speople__name">{{ personLabel(p) }}</span>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
class="fe-speople__remove"
|
|
163
|
+
:disabled="disabled"
|
|
164
|
+
:title="t('plugin.people.remove', { name: personLabel(p) })"
|
|
165
|
+
:aria-label="t('plugin.people.remove', { name: personLabel(p) })"
|
|
166
|
+
:data-testid="`surface-people-remove-${p.email}`"
|
|
167
|
+
@click="remove(p.email)"
|
|
168
|
+
>
|
|
169
|
+
<svg class="fe-ficon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false">
|
|
170
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
171
|
+
</svg>
|
|
172
|
+
</button>
|
|
173
|
+
</li>
|
|
174
|
+
</ul>
|
|
175
|
+
<div v-if="!full" class="fe-speople__addwrap">
|
|
176
|
+
<input
|
|
177
|
+
v-model="query"
|
|
178
|
+
class="fe-cfield__input fe-speople__input"
|
|
179
|
+
type="email"
|
|
180
|
+
autocomplete="off"
|
|
181
|
+
spellcheck="false"
|
|
182
|
+
:placeholder="lookupOff ? t('plugin.people.placeholder_email') : t('plugin.people.placeholder')"
|
|
183
|
+
:disabled="disabled"
|
|
184
|
+
:aria-invalid="invalid ? 'true' : undefined"
|
|
185
|
+
data-testid="surface-people-input"
|
|
186
|
+
@input="onInput"
|
|
187
|
+
@keydown="onKeydown"
|
|
188
|
+
@focus="onInput"
|
|
189
|
+
@blur="onBlur"
|
|
190
|
+
/>
|
|
191
|
+
<button
|
|
192
|
+
v-if="freeEntry"
|
|
193
|
+
type="button"
|
|
194
|
+
class="fe-btn fe-btn--sm"
|
|
195
|
+
:disabled="disabled || !query.trim()"
|
|
196
|
+
data-testid="surface-people-add"
|
|
197
|
+
@click="commit"
|
|
198
|
+
>
|
|
199
|
+
{{ t('plugin.people.add') }}
|
|
200
|
+
</button>
|
|
201
|
+
<ul v-if="showSuggest" class="fe-share__suggest fe-speople__suggest" data-testid="surface-people-suggest">
|
|
202
|
+
<li v-for="u in suggestions" :key="u.user_id ?? u.email" @mousedown.prevent="add(u)">
|
|
203
|
+
<span class="fe-share__av fe-share__av--sm">{{ personLabel(u).charAt(0).toUpperCase() }}</span>
|
|
204
|
+
<span class="fe-share__suggesttxt">
|
|
205
|
+
<span class="fe-share__suggestname">{{ personLabel(u) }}</span>
|
|
206
|
+
<span class="fe-share__suggestmeta">{{ u.email }}</span>
|
|
207
|
+
</span>
|
|
208
|
+
</li>
|
|
209
|
+
</ul>
|
|
210
|
+
</div>
|
|
211
|
+
<p v-if="notice" class="fe-surface__error" role="alert">{{ notice }}</p>
|
|
212
|
+
</div>
|
|
213
|
+
</template>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SurfacePinInput — a plugin surface's `pin-input` node: a short code, 4..8
|
|
4
|
+
* characters, into `data.values[id]`. One input with the browser's one-time
|
|
5
|
+
* code affordances rather than N boxes: paste works, screen readers read one
|
|
6
|
+
* field, and the length is enforced by the same line that draws it.
|
|
7
|
+
*/
|
|
8
|
+
import { computed } from 'vue';
|
|
9
|
+
import type { LocaleCode } from '../../../types/ExplorerConfig';
|
|
10
|
+
import { useLocale } from '../../../composables/useLocale';
|
|
11
|
+
import { pinLength } from '../../../lib/surfaceValues';
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
id: string;
|
|
15
|
+
length?: number | unknown;
|
|
16
|
+
modelValue: string;
|
|
17
|
+
locale: LocaleCode;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
invalid?: boolean;
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{
|
|
23
|
+
(e: 'update:modelValue', v: string): void;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const { t } = useLocale(() => props.locale);
|
|
27
|
+
|
|
28
|
+
const len = computed(() => pinLength(props.length));
|
|
29
|
+
|
|
30
|
+
function onInput(ev: Event) {
|
|
31
|
+
const raw = (ev.target as HTMLInputElement).value.replace(/\s+/g, '').slice(0, len.value);
|
|
32
|
+
(ev.target as HTMLInputElement).value = raw;
|
|
33
|
+
emit('update:modelValue', raw);
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<div class="fe-spin" data-testid="surface-pin">
|
|
39
|
+
<label class="fe-cfield__label" :for="`fe-spin-${id}`">{{ t('plugin.pin.label') }}</label>
|
|
40
|
+
<input
|
|
41
|
+
:id="`fe-spin-${id}`"
|
|
42
|
+
class="fe-cfield__input fe-cfield__input--mono fe-spin__input"
|
|
43
|
+
:class="{ 'is-invalid': invalid }"
|
|
44
|
+
type="text"
|
|
45
|
+
inputmode="numeric"
|
|
46
|
+
autocomplete="one-time-code"
|
|
47
|
+
spellcheck="false"
|
|
48
|
+
:maxlength="len"
|
|
49
|
+
:size="len"
|
|
50
|
+
:value="modelValue"
|
|
51
|
+
:disabled="disabled"
|
|
52
|
+
:aria-invalid="invalid ? 'true' : undefined"
|
|
53
|
+
:style="{ '--spin-len': len }"
|
|
54
|
+
@input="onInput"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SurfacePreview — a plugin surface's `preview` node: the explorer's own
|
|
4
|
+
* look at a storage file. The bytes come through `api.fetchBlob`, the same
|
|
5
|
+
* authenticated preview fetch every rich viewer uses (bearer / CSRF /
|
|
6
|
+
* credentials mode all honoured), so an embed on another origin sees the
|
|
7
|
+
* picture too. Pictures, video and audio are shown as themselves; anything
|
|
8
|
+
* else gets the listing's own type tile and the name.
|
|
9
|
+
*
|
|
10
|
+
* ⚠ No `<img src="/api/…">` for a PATH: a plain URL cannot carry the auth
|
|
11
|
+
* header and resolves against the HOST page's origin in an embed (see
|
|
12
|
+
* useThumbs). A public page's exposed copy (`ref`, M3) is the exception by
|
|
13
|
+
* design: there is no auth header, the PIN cookie rides with the same-origin
|
|
14
|
+
* URL the host's `fileUrl` resolver hands out, and the browser's own
|
|
15
|
+
* viewer draws it (`<img>` for a picture, `<embed>` for a PDF or anything
|
|
16
|
+
* else) — so the page never buffers a document in memory.
|
|
17
|
+
*/
|
|
18
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
19
|
+
import type { LocaleCode } from '../../../types/ExplorerConfig';
|
|
20
|
+
import type { FileApi } from '../../../composables/useFileApi';
|
|
21
|
+
import type { FileRefResolver } from '../../../types/Plugins';
|
|
22
|
+
import { useLocale } from '../../../composables/useLocale';
|
|
23
|
+
import { labelOfWire } from '../../../lib/destinationTree';
|
|
24
|
+
import { fileIconTile } from '../../../lib/fileIcons';
|
|
25
|
+
|
|
26
|
+
const props = defineProps<{
|
|
27
|
+
/** Adapter-qualified path of a storage file. */
|
|
28
|
+
path: string;
|
|
29
|
+
/** A public page's exposed copy (`pub:N`), shown from `fileUrl(ref)`. */
|
|
30
|
+
fileRef?: string;
|
|
31
|
+
fileUrl?: FileRefResolver;
|
|
32
|
+
locale: LocaleCode;
|
|
33
|
+
/** Absent: the node says the preview is unavailable. */
|
|
34
|
+
api?: Pick<FileApi, 'fetchBlob'>;
|
|
35
|
+
}>();
|
|
36
|
+
|
|
37
|
+
const { t } = useLocale(() => props.locale);
|
|
38
|
+
|
|
39
|
+
const url = ref<string | null>(null);
|
|
40
|
+
const mime = ref('');
|
|
41
|
+
const state = ref<'loading' | 'ok' | 'error'>('loading');
|
|
42
|
+
let seq = 0;
|
|
43
|
+
|
|
44
|
+
/** The exposed copy, when the node names one and the host can resolve it. */
|
|
45
|
+
const exposed = computed(() => (props.fileRef && props.fileUrl ? props.fileUrl(props.fileRef) : null));
|
|
46
|
+
|
|
47
|
+
const name = computed(() => exposed.value?.name || labelOfWire(props.path, '') || props.path || props.fileRef || '');
|
|
48
|
+
const kind = computed<'image' | 'video' | 'audio' | 'other'>(() => {
|
|
49
|
+
const m = mime.value;
|
|
50
|
+
if (m.startsWith('image/')) return 'image';
|
|
51
|
+
if (m.startsWith('video/')) return 'video';
|
|
52
|
+
if (m.startsWith('audio/')) return 'audio';
|
|
53
|
+
return 'other';
|
|
54
|
+
});
|
|
55
|
+
const tile = computed(() => fileIconTile({ type: 'file', basename: name.value }));
|
|
56
|
+
|
|
57
|
+
function release() {
|
|
58
|
+
// Only a blob URL this node minted is ours to revoke; an exposed copy's
|
|
59
|
+
// URL is the server's.
|
|
60
|
+
if (url.value && url.value.startsWith('blob:')) URL.revokeObjectURL(url.value);
|
|
61
|
+
url.value = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function load() {
|
|
65
|
+
const mine = ++seq;
|
|
66
|
+
release();
|
|
67
|
+
mime.value = '';
|
|
68
|
+
if (props.fileRef) {
|
|
69
|
+
if (!exposed.value) {
|
|
70
|
+
state.value = 'error';
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
url.value = exposed.value.url;
|
|
74
|
+
mime.value = exposed.value.mime || '';
|
|
75
|
+
state.value = 'ok';
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!props.api || !props.path) {
|
|
79
|
+
state.value = 'error';
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
state.value = 'loading';
|
|
83
|
+
try {
|
|
84
|
+
const r = await props.api.fetchBlob(props.path);
|
|
85
|
+
if (mine !== seq) {
|
|
86
|
+
URL.revokeObjectURL(r.url);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
url.value = r.url;
|
|
90
|
+
mime.value = r.mime || '';
|
|
91
|
+
state.value = 'ok';
|
|
92
|
+
} catch {
|
|
93
|
+
if (mine !== seq) return;
|
|
94
|
+
state.value = 'error';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
watch([() => props.path, () => props.fileRef], () => void load(), { immediate: true });
|
|
99
|
+
onBeforeUnmount(() => {
|
|
100
|
+
seq++;
|
|
101
|
+
release();
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<template>
|
|
106
|
+
<figure class="fe-spreview" data-testid="surface-preview" :data-state="state">
|
|
107
|
+
<p v-if="state === 'loading'" class="fe-surface__text fe-surface__text--muted">{{ t('plugin.view.loading') }}</p>
|
|
108
|
+
<p v-else-if="state === 'error'" class="fe-surface__text fe-surface__text--muted">{{ t('plugin.view.preview_failed') }}</p>
|
|
109
|
+
<template v-else>
|
|
110
|
+
<img v-if="kind === 'image' && url" class="fe-spreview__media" :src="url" :alt="name" />
|
|
111
|
+
<video v-else-if="kind === 'video' && url" class="fe-spreview__media" :src="url" controls preload="metadata"></video>
|
|
112
|
+
<audio v-else-if="kind === 'audio' && url" class="fe-spreview__audio" :src="url" controls preload="metadata"></audio>
|
|
113
|
+
<embed v-else-if="fileRef && url" class="fe-spreview__media fe-spreview__embed" :src="url" :type="mime || undefined" />
|
|
114
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
|
|
115
|
+
<span v-else class="fe-spreview__tile" aria-hidden="true" v-html="tile"></span>
|
|
116
|
+
</template>
|
|
117
|
+
<figcaption class="fe-spreview__name" :title="path">{{ name }}</figcaption>
|
|
118
|
+
</figure>
|
|
119
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SurfaceProgress — a plugin surface's `progress` node. `value` 0..100, or
|
|
4
|
+
* null for "busy, no idea how long" (drawn as a moving bar).
|
|
5
|
+
*/
|
|
6
|
+
import { computed } from 'vue';
|
|
7
|
+
import type { LocaleCode } from '../../../types/ExplorerConfig';
|
|
8
|
+
import type { PluginText } from '../../../types/Plugins';
|
|
9
|
+
import { useLocale } from '../../../composables/useLocale';
|
|
10
|
+
import { appTextOr } from '../../../lib/pluginLabel';
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
value: number | null | undefined;
|
|
14
|
+
label?: PluginText | string;
|
|
15
|
+
locale: LocaleCode;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const { t } = useLocale(() => props.locale);
|
|
19
|
+
|
|
20
|
+
const pct = computed<number | null>(() => {
|
|
21
|
+
const v = props.value;
|
|
22
|
+
if (v === null || v === undefined || !Number.isFinite(Number(v))) return null;
|
|
23
|
+
return Math.min(100, Math.max(0, Math.round(Number(v))));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/* ⚠ See `appTextOr`: the app's line for this reader, then filex's own. */
|
|
27
|
+
const text = computed(() => appTextOr(props.label, props.locale, () => t('plugin.view.busy')));
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="fe-sprogress" data-testid="surface-progress">
|
|
32
|
+
<div class="fe-sprogress__head">
|
|
33
|
+
<span class="fe-sprogress__label">{{ text }}</span>
|
|
34
|
+
<span v-if="pct !== null" class="fe-sprogress__pct">{{ pct }}%</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div
|
|
37
|
+
class="fe-sprogress__track"
|
|
38
|
+
:class="{ 'is-indeterminate': pct === null }"
|
|
39
|
+
role="progressbar"
|
|
40
|
+
:aria-label="text"
|
|
41
|
+
aria-valuemin="0"
|
|
42
|
+
aria-valuemax="100"
|
|
43
|
+
:aria-valuenow="pct === null ? undefined : pct"
|
|
44
|
+
>
|
|
45
|
+
<div class="fe-sprogress__bar" :style="pct === null ? undefined : { width: `${pct}%` }"></div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|