@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
package/src/lib/listing.ts
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* must be identical in both, otherwise split view shows mismatched rows
|
|
7
7
|
* (the trash row missing on one side → visible row-offset).
|
|
8
8
|
*/
|
|
9
|
+
import type { TagKind } from './tags';
|
|
9
10
|
import { ref } from 'vue';
|
|
10
11
|
|
|
11
12
|
import type { FileNode } from '../types/FileNode';
|
|
12
13
|
import { E2E_MARKER_NAME } from './e2ecrypto';
|
|
14
|
+
import { isInternalName, isInternalPath } from './internalPaths';
|
|
13
15
|
|
|
14
16
|
/** Adapter-strip: `s3-test://fileman/x` → `fileman/x`. */
|
|
15
17
|
export function stripAdapter(p: string): string {
|
|
@@ -39,24 +41,21 @@ export function parentDirOf(path: string): string {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
|
-
* Hide system/internal entries the user must never see as files:
|
|
43
|
-
*
|
|
44
|
-
* the
|
|
45
|
-
* both panes' listing filters.
|
|
44
|
+
* Hide system/internal entries the user must never see as files: filex's own
|
|
45
|
+
* directories and the keep marker (lib/internalPaths — the one client list,
|
|
46
|
+
* held equal to the server's by a test), the virtual `.trash` row, and the E2E
|
|
47
|
+
* marker. Shared by both panes' listing filters.
|
|
48
|
+
*
|
|
49
|
+
* ⚠ Absolute, not a "hidden files" preference: a user who has switched hidden
|
|
50
|
+
* files on is asking to see their OWN dotfiles, not ours. The server already
|
|
51
|
+
* leaves these out of every listing; this is the seatbelt for an embed talking
|
|
52
|
+
* to an older server — which is also why it judges the whole PATH, so a search
|
|
53
|
+
* hit such a server returns from inside `.filex-open` is dropped too.
|
|
46
54
|
*/
|
|
47
55
|
export function filterInternalEntries(files: FileNode[]): FileNode[] {
|
|
48
56
|
return (files || []).filter((f) => {
|
|
49
|
-
if (f.
|
|
50
|
-
if (f.path.includes('.versions') || f.basename === '.versions') return false;
|
|
57
|
+
if (isInternalName(f.basename) || isInternalPath(f.path)) return false;
|
|
51
58
|
if (f.basename === '.trash') return false;
|
|
52
|
-
// The desktop app's "open this document with filex" scratch area. Real
|
|
53
|
-
// files live in it for the length of one editing session, but they are the
|
|
54
|
-
// app's plumbing, not the user's documents -- and a user who has switched
|
|
55
|
-
// hidden files on is asking to see their OWN dotfiles, not ours. Hidden
|
|
56
|
-
// absolutely, like .filex-trash and .versions; stale copies are swept by
|
|
57
|
-
// the desktop app itself, not by hand.
|
|
58
|
-
if (f.basename === '.filex-open') return false;
|
|
59
|
-
if (f.basename === '.keepdir') return false;
|
|
60
59
|
if (f.basename === E2E_MARKER_NAME) return false;
|
|
61
60
|
return true;
|
|
62
61
|
});
|
|
@@ -272,16 +271,50 @@ export const VIRTUAL_SEGMENTS: Record<string, string> = {
|
|
|
272
271
|
*/
|
|
273
272
|
export const TAG_SEGMENT_PREFIX = '.tag~';
|
|
274
273
|
|
|
275
|
-
/**
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
/**
|
|
275
|
+
* etiket:k2 (v0.43.0) — the KIND of a tag view. A person can have a personal
|
|
276
|
+
* "rapor" and their team a "rapor"; the navigation panel lists them apart, so
|
|
277
|
+
* each opens its own view: `.mytag~rapor` (personal), `.teamtag~rapor` (team).
|
|
278
|
+
* The plain `.tag~rapor` stays and means BOTH — every link and restored tab
|
|
279
|
+
* from before kinds existed keeps opening what it opened then.
|
|
280
|
+
*
|
|
281
|
+
* ⚠ Listed longest-first is not needed (no prefix is a prefix of another),
|
|
282
|
+
* but every reader goes through `tagViewOfSegment`, never a `startsWith` of
|
|
283
|
+
* its own, so a fourth kind would be one line here.
|
|
284
|
+
*/
|
|
285
|
+
const TAG_KIND_PREFIXES: ReadonlyArray<readonly [string, TagKind | '']> = [
|
|
286
|
+
['.mytag~', 'personal'],
|
|
287
|
+
['.teamtag~', 'team'],
|
|
288
|
+
[TAG_SEGMENT_PREFIX, ''],
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
/** `invoices` → `.tag~invoices`; with a kind, `.mytag~` / `.teamtag~`. */
|
|
292
|
+
export function makeTagSegment(tag: string, kind: TagKind | '' = ''): string {
|
|
293
|
+
const prefix = TAG_KIND_PREFIXES.find(([, k]) => k === kind)?.[0] ?? TAG_SEGMENT_PREFIX;
|
|
294
|
+
return `${prefix}${tag}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** A tag-view segment's name and kind ('' = both), or null for anything else
|
|
298
|
+
* (a bare `.tag~` included). */
|
|
299
|
+
export function tagViewOfSegment(segment: string): { tag: string; kind: TagKind | '' } | null {
|
|
300
|
+
for (const [prefix, kind] of TAG_KIND_PREFIXES) {
|
|
301
|
+
if (segment.startsWith(prefix)) {
|
|
302
|
+
const tag = segment.slice(prefix.length);
|
|
303
|
+
return tag ? { tag, kind } : null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
278
307
|
}
|
|
279
308
|
|
|
280
|
-
/** `.tag~invoices` → `invoices`; ''
|
|
309
|
+
/** `.tag~invoices` (or `.mytag~` / `.teamtag~`) → `invoices`; '' otherwise. */
|
|
281
310
|
export function tagOfSegment(segment: string): string {
|
|
282
|
-
return segment
|
|
283
|
-
|
|
284
|
-
|
|
311
|
+
return tagViewOfSegment(segment)?.tag ?? '';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The kind of a tag-view path: 'personal', 'team', or '' (both / not a tag view). */
|
|
315
|
+
export function tagKindOfPath(path: string): TagKind | '' {
|
|
316
|
+
const clean = (path || '').replace(/^\/+|\/+$/g, '');
|
|
317
|
+
return tagViewOfSegment(clean.split('/').pop() || clean)?.kind ?? '';
|
|
285
318
|
}
|
|
286
319
|
|
|
287
320
|
/** True when `path` (user-facing form) IS a tag listing. */
|
|
@@ -308,8 +341,10 @@ export function virtualSegmentKey(segment: string): string {
|
|
|
308
341
|
export function virtualSegmentLabel(segment: string, t: (key: string) => string): string {
|
|
309
342
|
const key = VIRTUAL_SEGMENTS[segment];
|
|
310
343
|
if (key) return t(key);
|
|
311
|
-
const
|
|
312
|
-
|
|
344
|
+
const view = tagViewOfSegment(segment);
|
|
345
|
+
if (!view) return '';
|
|
346
|
+
// etiket:k2 — the crumb says whose tag this is: "#rapor · Kişisel".
|
|
347
|
+
return view.kind ? `#${view.tag} · ${t(`tags.kind.${view.kind}`)}` : `#${view.tag}`;
|
|
313
348
|
}
|
|
314
349
|
|
|
315
350
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modalStack — which explorer dialog is on top.
|
|
3
|
+
*
|
|
4
|
+
* Every open `modals/Modal.vue` registers here, and only the LAST one
|
|
5
|
+
* answers the keyboard (Escape, the Tab trap). Two dialogs open at once is
|
|
6
|
+
* ordinary — a confirmation over a plugin's screen, the destination picker
|
|
7
|
+
* over a move — and Escape must close the one in front, not both.
|
|
8
|
+
*
|
|
9
|
+
* ⚠ Module state on purpose: `<script setup>` top-level declarations are
|
|
10
|
+
* per INSTANCE, so a stack declared inside the component would hold one
|
|
11
|
+
* entry per dialog and every dialog would think it was on top.
|
|
12
|
+
*/
|
|
13
|
+
const stack: symbol[] = [];
|
|
14
|
+
|
|
15
|
+
export function pushModal(id: symbol): void {
|
|
16
|
+
stack.push(id);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function popModal(id: symbol): void {
|
|
20
|
+
const i = stack.lastIndexOf(id);
|
|
21
|
+
if (i >= 0) stack.splice(i, 1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isTopModal(id: symbol): boolean {
|
|
25
|
+
return stack[stack.length - 1] === id;
|
|
26
|
+
}
|
package/src/lib/nodeRow.ts
CHANGED
|
@@ -52,6 +52,15 @@ export function nodeRowToFileNode(
|
|
|
52
52
|
if (ctx.multiStorageRoot && !storageName) return null;
|
|
53
53
|
const name = String(row.name ?? rel.split('/').pop() ?? '');
|
|
54
54
|
const isDir = row.type === 'dir';
|
|
55
|
+
/* issue #34 — these endpoints answer with the raw node row, so `type` here
|
|
56
|
+
is `model.NodeType`: 'file', 'dir' OR 'symlink'. The listing projector
|
|
57
|
+
collapses that third value to 'file' and sets a `symlink` flag beside it;
|
|
58
|
+
`handlers/meta.go` does not, because it ships the node struct as it is.
|
|
59
|
+
Without this line a link the server will not follow appears on Recent,
|
|
60
|
+
Starred, a tag view or the Home tray as a plain file with no badge and no
|
|
61
|
+
explanation — the same fix reaching the folder listing and stopping at
|
|
62
|
+
the view beside it, which is the split lesson #174 was written about. */
|
|
63
|
+
const isSymlink = row.type === 'symlink';
|
|
55
64
|
const size = typeof row.size === 'number' ? row.size : 0;
|
|
56
65
|
const id = typeof row.id === 'number' ? row.id : undefined;
|
|
57
66
|
/* ⚠⚠ THE CONTEXT MENU MUST BE THE SAME EVERYWHERE (owner, 2026-09-19). The
|
|
@@ -98,9 +107,15 @@ export function nodeRowToFileNode(
|
|
|
98
107
|
mime_type: typeof row.mime === 'string' ? row.mime : '',
|
|
99
108
|
...(perm ? { perm } : {}),
|
|
100
109
|
read_only: readOnly,
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
|
|
110
|
+
...(isSymlink ? { symlink: true } : {}),
|
|
111
|
+
// ⚠⚠ The SERVER's `thumb_url`, never one built here. It used to be
|
|
112
|
+
// `/api/files/thumb/<id>` for every file, and the endpoint answers
|
|
113
|
+
// `404 "not ready"` for everything it has not rendered — a docx, a
|
|
114
|
+
// markdown note, a diagram: opening Recent with ten such files fired ten
|
|
115
|
+
// requests and all ten 404'd (2026-09-21). The rows now carry the URL
|
|
116
|
+
// exactly when there is an image behind it (handlers/meta.go, the folder
|
|
117
|
+
// listing's rule), so a file with none is never asked about.
|
|
118
|
+
thumb_url: !isDir && typeof row.thumb_url === 'string' && row.thumb_url ? row.thumb_url : undefined,
|
|
104
119
|
extra_metadata: {},
|
|
105
120
|
} as unknown as FileNode;
|
|
106
121
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pdfFieldRules — what a `text` field on a PDF will accept, as pure data.
|
|
3
|
+
*
|
|
4
|
+
* A signing app does not only want "a box for some words": it wants a tax
|
|
5
|
+
* number that is digits, a date in the shape the paper form prints, an
|
|
6
|
+
* address that fits the line. So a `text` field may carry a `rule`, and the
|
|
7
|
+
* browser enforces it WHILE the person types rather than after they submit —
|
|
8
|
+
* an invalid signature page that only says so on the way out is a page that
|
|
9
|
+
* gets signed twice.
|
|
10
|
+
*
|
|
11
|
+
* ⚠ The rule travels back to the plugin with the value
|
|
12
|
+
* (`{fields: [{id, value, font?, rule?}]}`, docs/APP-PLUGINS-API.md →
|
|
13
|
+
* `pdf-fields` props) so the stamper formats what it is handed instead of
|
|
14
|
+
* guessing from the string: `01/02/2026` is a date to a rule and ambiguous
|
|
15
|
+
* text to a parser.
|
|
16
|
+
*
|
|
17
|
+
* ⚠ This is a CONVENIENCE, not a security boundary. The browser is the only
|
|
18
|
+
* thing that runs it; a plugin that cares re-checks what it is given. Same
|
|
19
|
+
* relationship `applies` has with the server's re-check.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What a `text` field accepts. `any` is the default and means "anything".
|
|
24
|
+
*
|
|
25
|
+
* ⚠⚠ v3 §3.3 — there is no `date` rule any more. `date` is a FIELD TYPE,
|
|
26
|
+
* and two ways to ask for the same thing is how you get two answers: a date
|
|
27
|
+
* placed as a date field arrived as `YYYY-MM-DD` and a date typed into a text
|
|
28
|
+
* field under the old rule arrived as `DD/MM/YYYY`, so the stamping plugin
|
|
29
|
+
* had to guess which of its own boxes it was looking at. An older manifest's
|
|
30
|
+
* `kind: "date"` is read as `any` rather than refused — the box keeps taking
|
|
31
|
+
* what the person types, it just stops pretending to be a second date
|
|
32
|
+
* control.
|
|
33
|
+
*/
|
|
34
|
+
export type PdfRuleKind = 'any' | 'number' | 'email';
|
|
35
|
+
|
|
36
|
+
export const PDF_RULE_KINDS: readonly PdfRuleKind[] = ['any', 'number', 'email'];
|
|
37
|
+
|
|
38
|
+
export interface PdfFieldRule {
|
|
39
|
+
kind: PdfRuleKind;
|
|
40
|
+
/** Shortest accepted length in characters (0 = no floor). */
|
|
41
|
+
min?: number;
|
|
42
|
+
/** Longest accepted length in characters (0 = no ceiling). */
|
|
43
|
+
max?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isRuleKind(v: unknown): v is PdfRuleKind {
|
|
47
|
+
return typeof v === 'string' && PDF_RULE_KINDS.includes(v as PdfRuleKind);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The wire's `rule`, reduced to something drawable; `null` when there is none. */
|
|
51
|
+
export function normalizeRule(raw: unknown): PdfFieldRule | null {
|
|
52
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
53
|
+
const r = raw as Record<string, unknown>;
|
|
54
|
+
const kind: PdfRuleKind = isRuleKind(r.kind) ? r.kind : 'any';
|
|
55
|
+
const out: PdfFieldRule = { kind };
|
|
56
|
+
const min = Math.trunc(Number(r.min));
|
|
57
|
+
const max = Math.trunc(Number(r.max));
|
|
58
|
+
if (Number.isFinite(min) && min > 0) out.min = min;
|
|
59
|
+
if (Number.isFinite(max) && max > 0) out.max = max;
|
|
60
|
+
// A bare `{kind: "any"}` with no bounds says nothing; treat it as no rule so
|
|
61
|
+
// it does not travel back as noise.
|
|
62
|
+
if (out.kind === 'any' && out.min === undefined && out.max === undefined) return null;
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `inputmode` for the on-screen keyboard a rule wants. */
|
|
67
|
+
export function ruleInputMode(rule: PdfFieldRule | null): 'text' | 'numeric' | 'email' {
|
|
68
|
+
if (!rule) return 'text';
|
|
69
|
+
if (rule.kind === 'number') return 'numeric';
|
|
70
|
+
if (rule.kind === 'email') return 'email';
|
|
71
|
+
return 'text';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The longest string the rule allows to be typed at all. */
|
|
75
|
+
export function ruleMaxLength(rule: PdfFieldRule | null): number | undefined {
|
|
76
|
+
if (!rule) return undefined;
|
|
77
|
+
return rule.max && rule.max > 0 ? rule.max : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Type-as-you-go shaping. Returns what the field should hold after this
|
|
82
|
+
* keystroke — never an error: a rule refuses a CHARACTER here, and says why
|
|
83
|
+
* only in `ruleError` once there is something to judge.
|
|
84
|
+
*/
|
|
85
|
+
export function applyRule(raw: string, rule: PdfFieldRule | null): string {
|
|
86
|
+
let v = String(raw ?? '');
|
|
87
|
+
if (!rule) return v;
|
|
88
|
+
if (rule.kind === 'number') {
|
|
89
|
+
// Digits, one leading minus, one decimal separator — the separator kept
|
|
90
|
+
// as typed so a Turkish keyboard's comma is not silently rewritten.
|
|
91
|
+
const neg = v.startsWith('-');
|
|
92
|
+
let seen = false;
|
|
93
|
+
v = v
|
|
94
|
+
.split('')
|
|
95
|
+
.filter((ch) => {
|
|
96
|
+
if (ch >= '0' && ch <= '9') return true;
|
|
97
|
+
if ((ch === '.' || ch === ',') && !seen) {
|
|
98
|
+
seen = true;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
})
|
|
103
|
+
.join('');
|
|
104
|
+
if (neg) v = `-${v}`;
|
|
105
|
+
} else if (rule.kind === 'email') {
|
|
106
|
+
v = v.replace(/\s+/g, '');
|
|
107
|
+
}
|
|
108
|
+
const max = ruleMaxLength(rule);
|
|
109
|
+
if (max && v.length > max) v = v.slice(0, max);
|
|
110
|
+
return v;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Codes `ruleErrorText` turns into words; `''` when the value is acceptable. */
|
|
114
|
+
export type PdfRuleError = '' | 'number' | 'email' | 'min' | 'max';
|
|
115
|
+
|
|
116
|
+
/** Is `value` acceptable under `rule`? An empty value is never wrong here — `required` is a separate question. */
|
|
117
|
+
export function ruleError(value: unknown, rule: PdfFieldRule | null): PdfRuleError {
|
|
118
|
+
const v = typeof value === 'string' ? value.trim() : '';
|
|
119
|
+
if (!rule || v === '') return '';
|
|
120
|
+
if (rule.min && v.length < rule.min) return 'min';
|
|
121
|
+
if (rule.max && v.length > rule.max) return 'max';
|
|
122
|
+
if (rule.kind === 'number') return /^-?\d+([.,]\d+)?$/.test(v) ? '' : 'number';
|
|
123
|
+
if (rule.kind === 'email') return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v) ? '' : 'email';
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
|