@brftech/filex-core 0.42.1 → 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-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
- package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
- package/dist/{EpubViewer-DS3mb3k8.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-CIrOn63I.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 +5241 -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 +1207 -343
- 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 +47 -17
- package/src/components/RecoveryKeyModal.vue +1 -1
- package/src/components/RowActions.vue +134 -0
- package/src/components/S3KeysPanel.vue +98 -56
- package/src/components/SSHKeysPanel.vue +79 -26
- 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 -27
- 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 +121 -0
- 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 +522 -119
- package/src/locales/resolve.ts +17 -8
- package/src/locales/tr.ts +539 -155
- 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 +2895 -241
- 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 +58 -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-CLNevfPE.js +0 -124
- package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
- package/dist/CsvViewer-K8XA3z7N.js +0 -141
- package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
- package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
- package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
- package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
- package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
- package/dist/MermaidViewer-DqucH_io.js +0 -137
- package/dist/MermaidViewer-DqucH_io.js.map +0 -1
- package/dist/PsdViewer-CwiVENmq.js +0 -122
- package/dist/PsdViewer-CwiVENmq.js.map +0 -1
- package/dist/TiffViewer-CpewSVmQ.js +0 -142
- package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
- package/dist/Viewer3D-CIrOn63I.js.map +0 -1
- package/dist/index-Dx0IbBcI.js +0 -26831
- package/dist/index-Dx0IbBcI.js.map +0 -1
- package/dist/useViewerFetch-czqbd2Lj.js +0 -25
- package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* PdfFieldEditor — what a box IS, as opposed to where it sits.
|
|
4
|
+
*
|
|
5
|
+
* ⚠⚠ One editor, two screens. The request wizard asks for a box in two
|
|
6
|
+
* steps now — "define the boxes" (this, one card per box, no document in
|
|
7
|
+
* sight) and "place them" (the document, with these boxes handed out one at
|
|
8
|
+
* a time) — and the one-screen `edit` mode still shows it for whichever box
|
|
9
|
+
* is selected. If the controls were written twice they would drift, and the
|
|
10
|
+
* person would meet two different ideas of what naming a box means.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here knows about pages, rectangles or pointers: placing is the
|
|
13
|
+
* parent's job. That split is the whole point of the component.
|
|
14
|
+
*/
|
|
15
|
+
import { computed } from 'vue';
|
|
16
|
+
import type { LocaleCode } from '../../../types/ExplorerConfig';
|
|
17
|
+
import { useLocale } from '../../../composables/useLocale';
|
|
18
|
+
import { appTextOr, labelOf } from '../../../lib/pluginLabel';
|
|
19
|
+
import {
|
|
20
|
+
dateChoices,
|
|
21
|
+
dateExample,
|
|
22
|
+
formatFor,
|
|
23
|
+
isDrawnType,
|
|
24
|
+
linesOf,
|
|
25
|
+
normalizeDateLabels,
|
|
26
|
+
normalizeFormats,
|
|
27
|
+
normalizeStampLines,
|
|
28
|
+
splitFormat,
|
|
29
|
+
stampExample,
|
|
30
|
+
type PdfField,
|
|
31
|
+
type PdfSigner,
|
|
32
|
+
type PdfSignatureStyle,
|
|
33
|
+
} from '../../../lib/pdfFields';
|
|
34
|
+
import { PDF_RULE_KINDS, type PdfFieldRule, type PdfRuleKind } from '../../../lib/pdfFieldRules';
|
|
35
|
+
import { SIGN_FONTS, type SignFontKey } from '../../../lib/signFonts';
|
|
36
|
+
import ChoiceButtons, { type ChoiceOption } from '../../ChoiceButtons.vue';
|
|
37
|
+
|
|
38
|
+
const props = defineProps<{
|
|
39
|
+
field: PdfField;
|
|
40
|
+
signers: PdfSigner[];
|
|
41
|
+
/**
|
|
42
|
+
* The date layouts this surface offers (`[{id, label, example}]`, the
|
|
43
|
+
* node's own `formats` prop). Empty or absent: no layout control is
|
|
44
|
+
* drawn at all, because there is nothing to choose between.
|
|
45
|
+
*/
|
|
46
|
+
formats?: unknown;
|
|
47
|
+
/**
|
|
48
|
+
* The date control's three captions as the PLUGIN words them
|
|
49
|
+
* (`date_labels`): the two questions and the word over the example.
|
|
50
|
+
* Absent: this package's own catalogue stands in.
|
|
51
|
+
*/
|
|
52
|
+
dateLabels?: unknown;
|
|
53
|
+
locale: LocaleCode;
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
/** `surface-pdf` on the one-screen editor; the define cards pass their own. */
|
|
56
|
+
testidPrefix?: string;
|
|
57
|
+
/**
|
|
58
|
+
* The lines a plugin can print under a signature (the node's
|
|
59
|
+
* `stamp_lines`, `lib/pdfFields.normalizeStampLines`). Absent or empty:
|
|
60
|
+
* the plugin offers no choice, and no control is drawn.
|
|
61
|
+
*/
|
|
62
|
+
stampLines?: unknown;
|
|
63
|
+
}>();
|
|
64
|
+
|
|
65
|
+
const emit = defineEmits<{
|
|
66
|
+
/** A changed copy of the field. */
|
|
67
|
+
(e: 'patch', f: PdfField): void;
|
|
68
|
+
(e: 'remove'): void;
|
|
69
|
+
}>();
|
|
70
|
+
|
|
71
|
+
const { t } = useLocale(() => props.locale);
|
|
72
|
+
const prefix = computed(() => props.testidPrefix || 'surface-pdf');
|
|
73
|
+
|
|
74
|
+
function typeLabel(type: PdfField['type']): string {
|
|
75
|
+
return t(`plugin.pdf.type_${type}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Change the field through a copy — the parent owns the list. */
|
|
79
|
+
function patch(fn: (f: PdfField) => void): void {
|
|
80
|
+
const next = { ...props.field };
|
|
81
|
+
fn(next);
|
|
82
|
+
emit('patch', next);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The box's NAME, exactly as it was typed — spaces and all.
|
|
87
|
+
*
|
|
88
|
+
* ⚠⚠ NEVER trim here. `:value` is bound to the field, so a trim on every
|
|
89
|
+
* keystroke writes the trimmed name straight back into the input: the space
|
|
90
|
+
* typed after a word is taken off again before the next letter arrives, and
|
|
91
|
+
* a two-word name becomes impossible to type (the owner, 2026-09-23:
|
|
92
|
+
* "isimde boşluk bırakamıyorum hiç izin vermiyor"). A name is free text in
|
|
93
|
+
* any script; the ASCII IDENTITY the PDF names the field by is derived from
|
|
94
|
+
* it by the plugin (`fields.Slug`) and shown to nobody. The record is
|
|
95
|
+
* trimmed once, where the record is written.
|
|
96
|
+
*/
|
|
97
|
+
function setLabel(v: string): void {
|
|
98
|
+
patch((f) => {
|
|
99
|
+
if (v) f.label = v;
|
|
100
|
+
else delete f.label;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assign(assignee: string): void {
|
|
105
|
+
patch((f) => {
|
|
106
|
+
if (assignee) f.assignee = assignee;
|
|
107
|
+
else delete f.assignee;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function setRequired(on: boolean): void {
|
|
112
|
+
patch((f) => {
|
|
113
|
+
if (on) f.required = true;
|
|
114
|
+
else delete f.required;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function setRuleKind(kind: string): void {
|
|
119
|
+
patch((f) => {
|
|
120
|
+
const k = (PDF_RULE_KINDS as readonly string[]).includes(kind) ? (kind as PdfRuleKind) : 'any';
|
|
121
|
+
const rule: PdfFieldRule = { ...(f.rule ?? { kind: 'any' }), kind: k };
|
|
122
|
+
if (k === 'any' && !rule.min && !rule.max) delete f.rule;
|
|
123
|
+
else f.rule = rule;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function setRuleBound(which: 'min' | 'max', raw: unknown): void {
|
|
128
|
+
const n = Math.trunc(Number(String(raw ?? '').trim()));
|
|
129
|
+
patch((f) => {
|
|
130
|
+
const rule: PdfFieldRule = { ...(f.rule ?? { kind: 'any' }) };
|
|
131
|
+
if (Number.isFinite(n) && n > 0) rule[which] = n;
|
|
132
|
+
else delete rule[which];
|
|
133
|
+
if (rule.kind === 'any' && !rule.min && !rule.max) delete f.rule;
|
|
134
|
+
else f.rule = rule;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function setFormat(id: string): void {
|
|
139
|
+
patch((f) => {
|
|
140
|
+
if (id) f.format = id;
|
|
141
|
+
else delete f.format;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function setFont(key: SignFontKey): void {
|
|
146
|
+
patch((f) => {
|
|
147
|
+
f.font = key;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A signature or initials box: drawn by hand, or a name typed in a face. */
|
|
152
|
+
const drawn = computed(() => isDrawnType(props.field.type));
|
|
153
|
+
|
|
154
|
+
/** How this signature box is given. Absent means drawn — what a signature is. */
|
|
155
|
+
const style = computed<PdfSignatureStyle>(() => props.field.style ?? 'drawn');
|
|
156
|
+
|
|
157
|
+
function setStyle(v: string): void {
|
|
158
|
+
patch((f) => {
|
|
159
|
+
if (v === 'typed') f.style = 'typed';
|
|
160
|
+
else delete f.style;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const styleOptions = computed<ChoiceOption[]>(() => [
|
|
165
|
+
{ value: 'drawn', label: t('plugin.pdf.style_drawn') },
|
|
166
|
+
{ value: 'typed', label: t('plugin.pdf.style_typed') },
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Is the face a question here at all?
|
|
171
|
+
*
|
|
172
|
+
* ⚠ The owner, 2026-09-21: "text seçersem font çıkar, text seçmezsem font
|
|
173
|
+
* seçimi çıkmaz". A drawn signature is somebody's hand; a face offered for it
|
|
174
|
+
* was a control that changed nothing the signer would ever see. Text and
|
|
175
|
+
* date boxes are words, so they always have one; a checkbox never does.
|
|
176
|
+
*/
|
|
177
|
+
const asksFont = computed(() => {
|
|
178
|
+
if (props.field.type === 'checkbox') return false;
|
|
179
|
+
if (drawn.value) return style.value === 'typed';
|
|
180
|
+
return true;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
/* ── what is printed under a signature ────────────────────────────── */
|
|
184
|
+
|
|
185
|
+
const lineCatalogue = computed(() => normalizeStampLines(props.stampLines));
|
|
186
|
+
/** The ids this box prints, in the catalogue's order (its default set when it has none). */
|
|
187
|
+
const pickedLines = computed(() => linesOf(props.field, lineCatalogue.value));
|
|
188
|
+
const lineOptions = computed<ChoiceOption[]>(() =>
|
|
189
|
+
lineCatalogue.value.map((l) => ({ value: l.id, label: labelOf(l.label, props.locale) || l.id })),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
function setLines(v: string | string[]): void {
|
|
193
|
+
const ids = (Array.isArray(v) ? v : [v]).filter(Boolean);
|
|
194
|
+
patch((f) => {
|
|
195
|
+
// ⚠ An EMPTY list is kept, and means "nothing under the signature". It is
|
|
196
|
+
// not the same as absent (the plugin's default set), and folding the two
|
|
197
|
+
// together would put back the very lines the person just took off.
|
|
198
|
+
f.lines = ids;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The words the plugin will print, one per line, for this box's signer —
|
|
204
|
+
* the plugin's own example for each chosen line, so the card previews its
|
|
205
|
+
* wording rather than this package guessing at it.
|
|
206
|
+
*/
|
|
207
|
+
const linePreview = computed(() =>
|
|
208
|
+
pickedLines.value
|
|
209
|
+
.map((id) => lineCatalogue.value.find((l) => l.id === id))
|
|
210
|
+
.filter((l): l is NonNullable<typeof l> => !!l)
|
|
211
|
+
.map((l) => labelOf(stampExample(l, props.field.assignee), props.locale) || labelOf(l.label, props.locale)),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Who fills this box, or who signs it.
|
|
216
|
+
*
|
|
217
|
+
* ⚠ The owner, 2026-09-21: "tarih, metin, onay kutusu gibi şeylerde
|
|
218
|
+
* 'imzacı' yazısı yerine 'dolduran' yazısı koyalım". A date or a tick is
|
|
219
|
+
* FILLED; calling its owner "the signer" asked the requester a question
|
|
220
|
+
* about the wrong act.
|
|
221
|
+
*/
|
|
222
|
+
const assigneeLabel = computed(() => t(drawn.value ? 'plugin.pdf.assignee' : 'plugin.pdf.filled_by'));
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* "Anyone"'s value on the buttons. ⚠ Not `''`: the choice control reads an
|
|
226
|
+
* empty value as "nothing picked", so a box that belonged to anyone showed
|
|
227
|
+
* NO button pressed — measured on the define step, 2026-09-21, three
|
|
228
|
+
* unpressed buttons under every new box of a two-signer request. The
|
|
229
|
+
* sentinel lives only between this control and `assign`; the field still
|
|
230
|
+
* carries no `assignee` at all.
|
|
231
|
+
*/
|
|
232
|
+
const ANYONE = '*';
|
|
233
|
+
|
|
234
|
+
/** Who the box belongs to, as buttons — "Anyone" first. */
|
|
235
|
+
const assigneeOptions = computed<ChoiceOption[]>(() => [
|
|
236
|
+
{ value: ANYONE, label: t('plugin.pdf.unassigned') },
|
|
237
|
+
...props.signers.map((sg) => ({ value: sg.id, label: labelOf(sg.label, props.locale) || sg.id })),
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
/** What a text box accepts, as buttons. */
|
|
241
|
+
const ruleOptions = computed<ChoiceOption[]>(() =>
|
|
242
|
+
PDF_RULE_KINDS.map((k) => ({ value: k, label: t(`plugin.pdf.rule_kind_${k}`) })),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* How a date box is written, as buttons — each one with an example under
|
|
247
|
+
* it, because `MM/DD/YYYY` is a puzzle and `12/31/2000` is an answer.
|
|
248
|
+
*/
|
|
249
|
+
const dateFormats = computed(() => normalizeFormats(props.formats));
|
|
250
|
+
const formatOptions = computed<ChoiceOption[]>(() =>
|
|
251
|
+
dateFormats.value.map((f) => ({ value: f.id, label: labelOf(f.label, props.locale) || f.id, help: f.example })),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Which layout is shown as chosen. A box that has not been given one yet is
|
|
256
|
+
* drawn on the FIRST the surface offers: a catalogue's first entry is what
|
|
257
|
+
* the plugin falls back to, so showing nothing selected would be telling
|
|
258
|
+
* the person their date has no layout when it has one.
|
|
259
|
+
*/
|
|
260
|
+
const pickedFormat = computed(() => props.field.format ?? dateFormats.value[0]?.id ?? '');
|
|
261
|
+
|
|
262
|
+
/* ── a date's layout: TWO questions ───────────────────────────────────
|
|
263
|
+
*
|
|
264
|
+
* ⚠⚠ The owner, 2026-09-23: "tarih biçimi ve ayraçlarını ayrı ayrı
|
|
265
|
+
* seçebilir olalım … Tarih biçimi iki farklı seçimle tamamlandığında örnek
|
|
266
|
+
* halini alta gösterelim". Twenty layouts in one row is a wall to hunt
|
|
267
|
+
* through; the same twenty are five arrangements and four separators, and
|
|
268
|
+
* nobody has to read a pattern to answer either one. The BOX still stores
|
|
269
|
+
* the whole pattern, so a request made before this keeps its layout
|
|
270
|
+
* exactly and no signed document is re-written under anybody.
|
|
271
|
+
*/
|
|
272
|
+
const dateParts = computed(() => dateChoices(dateFormats.value));
|
|
273
|
+
const dateCaptions = computed(() => normalizeDateLabels(props.dateLabels));
|
|
274
|
+
/* An app may caption these three itself; filex has its own words for all of
|
|
275
|
+
them. ⚠ `appTextOr`, not `labelOf(…) || t(…)`: the app's caption for THIS
|
|
276
|
+
reader wins, then filex's own, and the app's other languages last — or a
|
|
277
|
+
reader in Arabic gets the app's English in the middle of an Arabic screen
|
|
278
|
+
while the language pack holds the very same caption (v0.43.0). */
|
|
279
|
+
function caption(k: 'order' | 'separator' | 'example', fallback: string): string {
|
|
280
|
+
return appTextOr(dateCaptions.value[k], props.locale, () => t(fallback));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const pickedPair = computed(() => splitFormat(dateFormats.value, pickedFormat.value));
|
|
284
|
+
const pickedOrder = computed(() => pickedPair.value?.order ?? dateParts.value?.orders[0]?.id ?? '');
|
|
285
|
+
const pickedSeparator = computed(() => pickedPair.value?.separator ?? dateParts.value?.separators[0]?.id ?? '');
|
|
286
|
+
|
|
287
|
+
const orderOptions = computed<ChoiceOption[]>(() =>
|
|
288
|
+
(dateParts.value?.orders ?? []).map((o) => ({ value: o.id, label: labelOf(o.label, props.locale) || o.id })),
|
|
289
|
+
);
|
|
290
|
+
const separatorOptions = computed<ChoiceOption[]>(() =>
|
|
291
|
+
(dateParts.value?.separators ?? []).map((s) => ({ value: s.id, label: labelOf(s.label, props.locale) || s.id })),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
/** Today, written the way the two answers say — the whole point of asking. */
|
|
295
|
+
const dateShown = computed(() => dateExample(pickedOrder.value, pickedSeparator.value, new Date()));
|
|
296
|
+
|
|
297
|
+
function setOrder(v: string): void {
|
|
298
|
+
setFormat(formatFor(dateFormats.value, v, pickedSeparator.value));
|
|
299
|
+
}
|
|
300
|
+
function setSeparator(v: string): void {
|
|
301
|
+
setFormat(formatFor(dateFormats.value, pickedOrder.value, v));
|
|
302
|
+
}
|
|
303
|
+
</script>
|
|
304
|
+
|
|
305
|
+
<template>
|
|
306
|
+
<div class="fe-spdf__props" :data-testid="`${prefix}-editor`">
|
|
307
|
+
<span class="fe-spdf__propname">{{ typeLabel(field.type) }}</span>
|
|
308
|
+
|
|
309
|
+
<!-- v3 §3.1 — the box's own name. Offered with the type's name already
|
|
310
|
+
in it, so a page of boxes is never a page of "Signature". -->
|
|
311
|
+
<label class="fe-spdf__prop">
|
|
312
|
+
<span>{{ t('plugin.pdf.label') }}</span>
|
|
313
|
+
<input
|
|
314
|
+
class="fe-cfield__input fe-spdf__name"
|
|
315
|
+
type="text"
|
|
316
|
+
:value="field.label ?? ''"
|
|
317
|
+
:placeholder="typeLabel(field.type)"
|
|
318
|
+
:disabled="disabled"
|
|
319
|
+
:data-testid="`${prefix}-label`"
|
|
320
|
+
@input="setLabel(($event.target as HTMLInputElement).value)"
|
|
321
|
+
/>
|
|
322
|
+
</label>
|
|
323
|
+
|
|
324
|
+
<div v-if="signers.length" class="fe-spdf__prop">
|
|
325
|
+
<span :id="`${prefix}-${field.id}-assignee-lbl`" :data-testid="`${prefix}-assignee-label`">{{ assigneeLabel }}</span>
|
|
326
|
+
<ChoiceButtons
|
|
327
|
+
:options="assigneeOptions"
|
|
328
|
+
:model-value="field.assignee || ANYONE"
|
|
329
|
+
:disabled="disabled"
|
|
330
|
+
:aria-labelledby="`${prefix}-${field.id}-assignee-lbl`"
|
|
331
|
+
:testid-prefix="`${prefix}-assignee`"
|
|
332
|
+
@update:model-value="(v: string | string[]) => assign(String(v) === ANYONE ? '' : String(v))"
|
|
333
|
+
/>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<!-- A signature is drawn, or it is a name typed in a face — the
|
|
337
|
+
requester decides which, and only the typed one has a face to pick. -->
|
|
338
|
+
<div v-if="drawn" class="fe-spdf__prop" :data-testid="`${prefix}-style`">
|
|
339
|
+
<span :id="`${prefix}-${field.id}-style-lbl`">{{ t('plugin.pdf.style') }}</span>
|
|
340
|
+
<ChoiceButtons
|
|
341
|
+
:options="styleOptions"
|
|
342
|
+
:model-value="style"
|
|
343
|
+
:disabled="disabled"
|
|
344
|
+
:aria-labelledby="`${prefix}-${field.id}-style-lbl`"
|
|
345
|
+
:testid-prefix="`${prefix}-style`"
|
|
346
|
+
@update:model-value="(v: string | string[]) => setStyle(String(v))"
|
|
347
|
+
/>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<label class="fe-spdf__prop fe-spdf__prop--check">
|
|
351
|
+
<input
|
|
352
|
+
type="checkbox"
|
|
353
|
+
:checked="field.required === true"
|
|
354
|
+
:disabled="disabled"
|
|
355
|
+
:data-testid="`${prefix}-required`"
|
|
356
|
+
@change="setRequired(($event.target as HTMLInputElement).checked)"
|
|
357
|
+
/>
|
|
358
|
+
<span>{{ t('plugin.pdf.required') }}</span>
|
|
359
|
+
</label>
|
|
360
|
+
|
|
361
|
+
<!-- A text box says what it will take. ⚠ Only `text`: a date field
|
|
362
|
+
already IS a date, a checkbox is a boolean, and a signature is a
|
|
363
|
+
picture — a rule on any of them would be a control that does
|
|
364
|
+
nothing. -->
|
|
365
|
+
<template v-if="field.type === 'text'">
|
|
366
|
+
<div class="fe-spdf__prop">
|
|
367
|
+
<span :id="`${prefix}-${field.id}-rule-lbl`">{{ t('plugin.pdf.rule') }}</span>
|
|
368
|
+
<ChoiceButtons
|
|
369
|
+
:options="ruleOptions"
|
|
370
|
+
:model-value="field.rule?.kind ?? 'any'"
|
|
371
|
+
:disabled="disabled"
|
|
372
|
+
:aria-labelledby="`${prefix}-${field.id}-rule-lbl`"
|
|
373
|
+
:testid-prefix="`${prefix}-rule`"
|
|
374
|
+
@update:model-value="(v: string | string[]) => setRuleKind(String(v))"
|
|
375
|
+
/>
|
|
376
|
+
</div>
|
|
377
|
+
<label class="fe-spdf__prop">
|
|
378
|
+
<span>{{ t('plugin.pdf.rule_min_label') }}</span>
|
|
379
|
+
<input
|
|
380
|
+
class="fe-cfield__input fe-spdf__num"
|
|
381
|
+
type="number"
|
|
382
|
+
min="0"
|
|
383
|
+
:value="field.rule?.min ?? ''"
|
|
384
|
+
:disabled="disabled"
|
|
385
|
+
:data-testid="`${prefix}-rule-min`"
|
|
386
|
+
@input="setRuleBound('min', ($event.target as HTMLInputElement).value)"
|
|
387
|
+
/>
|
|
388
|
+
</label>
|
|
389
|
+
<label class="fe-spdf__prop">
|
|
390
|
+
<span>{{ t('plugin.pdf.rule_max_label') }}</span>
|
|
391
|
+
<input
|
|
392
|
+
class="fe-cfield__input fe-spdf__num"
|
|
393
|
+
type="number"
|
|
394
|
+
min="0"
|
|
395
|
+
:value="field.rule?.max ?? ''"
|
|
396
|
+
:disabled="disabled"
|
|
397
|
+
:data-testid="`${prefix}-rule-max`"
|
|
398
|
+
@input="setRuleBound('max', ($event.target as HTMLInputElement).value)"
|
|
399
|
+
/>
|
|
400
|
+
</label>
|
|
401
|
+
</template>
|
|
402
|
+
|
|
403
|
+
<!-- ⚠ A date box's layout is its `rule`: it is the one thing about the
|
|
404
|
+
box that is still an open question once it has a name, and leaving
|
|
405
|
+
it unasked meant the plugin's own default was the only answer
|
|
406
|
+
anybody could give. Buttons, never a dropdown (v3 §2). -->
|
|
407
|
+
<template v-if="field.type === 'date' && dateParts">
|
|
408
|
+
<div class="fe-spdf__prop">
|
|
409
|
+
<span :id="`${prefix}-${field.id}-order-lbl`">{{ caption('order', 'plugin.pdf.date_order') }}</span>
|
|
410
|
+
<ChoiceButtons
|
|
411
|
+
:options="orderOptions"
|
|
412
|
+
:model-value="pickedOrder"
|
|
413
|
+
:disabled="disabled"
|
|
414
|
+
:aria-labelledby="`${prefix}-${field.id}-order-lbl`"
|
|
415
|
+
:testid-prefix="`${prefix}-order`"
|
|
416
|
+
@update:model-value="(v: string | string[]) => setOrder(String(v))"
|
|
417
|
+
/>
|
|
418
|
+
</div>
|
|
419
|
+
<div class="fe-spdf__prop">
|
|
420
|
+
<span :id="`${prefix}-${field.id}-sep-lbl`">{{ caption('separator', 'plugin.pdf.date_separator') }}</span>
|
|
421
|
+
<ChoiceButtons
|
|
422
|
+
:options="separatorOptions"
|
|
423
|
+
:model-value="pickedSeparator"
|
|
424
|
+
:disabled="disabled"
|
|
425
|
+
:aria-labelledby="`${prefix}-${field.id}-sep-lbl`"
|
|
426
|
+
:testid-prefix="`${prefix}-sep`"
|
|
427
|
+
@update:model-value="(v: string | string[]) => setSeparator(String(v))"
|
|
428
|
+
/>
|
|
429
|
+
</div>
|
|
430
|
+
<p class="fe-spdf__dateex" :data-testid="`${prefix}-date-example`">
|
|
431
|
+
<span class="fe-spdf__dateex-title">{{ caption('example', 'plugin.pdf.date_example') }}</span>
|
|
432
|
+
<bdi class="fe-spdf__dateex-value">{{ dateShown }}</bdi>
|
|
433
|
+
</p>
|
|
434
|
+
</template>
|
|
435
|
+
<div v-else-if="field.type === 'date' && formatOptions.length" class="fe-spdf__prop">
|
|
436
|
+
<span :id="`${prefix}-${field.id}-format-lbl`">{{ t('plugin.pdf.format') }}</span>
|
|
437
|
+
<ChoiceButtons
|
|
438
|
+
:options="formatOptions"
|
|
439
|
+
:model-value="pickedFormat"
|
|
440
|
+
:disabled="disabled"
|
|
441
|
+
:aria-labelledby="`${prefix}-${field.id}-format-lbl`"
|
|
442
|
+
:testid-prefix="`${prefix}-format`"
|
|
443
|
+
@update:model-value="(v: string | string[]) => setFormat(String(v))"
|
|
444
|
+
/>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<!-- The face the stamper draws this field in. Each button is set in the
|
|
448
|
+
face it offers: a font's NAME tells nobody what it looks like. -->
|
|
449
|
+
<div
|
|
450
|
+
v-if="asksFont"
|
|
451
|
+
class="fe-signfonts fe-spdf__prop"
|
|
452
|
+
role="group"
|
|
453
|
+
:aria-label="t('plugin.pdf.font')"
|
|
454
|
+
:data-testid="`${prefix}-fonts`"
|
|
455
|
+
>
|
|
456
|
+
<button
|
|
457
|
+
v-for="f in SIGN_FONTS"
|
|
458
|
+
:key="f.key"
|
|
459
|
+
type="button"
|
|
460
|
+
class="fe-signfonts__btn"
|
|
461
|
+
:class="[f.className, { 'is-on': (field.font ?? 'caveat') === f.key }]"
|
|
462
|
+
:title="f.name"
|
|
463
|
+
:disabled="disabled"
|
|
464
|
+
:aria-pressed="(field.font ?? 'caveat') === f.key ? 'true' : 'false'"
|
|
465
|
+
:data-testid="`${prefix}-font-${f.key}`"
|
|
466
|
+
@click="setFont(f.key)"
|
|
467
|
+
>
|
|
468
|
+
Aa
|
|
469
|
+
</button>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<!-- What is printed under the signature: chosen per box, from the
|
|
473
|
+
lines the plugin can really fill in (its signing record), with the
|
|
474
|
+
plugin's own wording previewed underneath. ⚠ Nothing typed here
|
|
475
|
+
reaches the paper — the requester picks WHICH facts, never their
|
|
476
|
+
values. -->
|
|
477
|
+
<div v-if="drawn && lineOptions.length" class="fe-spdf__prop fe-spdf__lines" :data-testid="`${prefix}-lines`">
|
|
478
|
+
<span :id="`${prefix}-${field.id}-lines-lbl`">{{ t('plugin.pdf.lines') }}</span>
|
|
479
|
+
<ChoiceButtons
|
|
480
|
+
:options="lineOptions"
|
|
481
|
+
:model-value="pickedLines"
|
|
482
|
+
multi
|
|
483
|
+
:disabled="disabled"
|
|
484
|
+
:aria-labelledby="`${prefix}-${field.id}-lines-lbl`"
|
|
485
|
+
:testid-prefix="`${prefix}-line`"
|
|
486
|
+
@update:model-value="setLines"
|
|
487
|
+
/>
|
|
488
|
+
<div class="fe-spdf__linepreview" :data-testid="`${prefix}-lines-preview`">
|
|
489
|
+
<span class="fe-spdf__linepreview-title">{{ t('plugin.pdf.lines_preview') }}</span>
|
|
490
|
+
<template v-if="linePreview.length">
|
|
491
|
+
<span v-for="(ln, i) in linePreview" :key="i" class="fe-spdf__linepreview-line">{{ ln }}</span>
|
|
492
|
+
</template>
|
|
493
|
+
<span v-else class="fe-spdf__linepreview-line is-none">{{ t('plugin.pdf.lines_none') }}</span>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<slot name="extra"></slot>
|
|
498
|
+
|
|
499
|
+
<button
|
|
500
|
+
type="button"
|
|
501
|
+
class="fe-btn fe-btn--sm fe-btn--danger"
|
|
502
|
+
:disabled="disabled"
|
|
503
|
+
:data-testid="`${prefix}-delete`"
|
|
504
|
+
@click="emit('remove')"
|
|
505
|
+
>
|
|
506
|
+
{{ t('plugin.pdf.delete') }}
|
|
507
|
+
</button>
|
|
508
|
+
</div>
|
|
509
|
+
</template>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SurfaceFileChooser — a plugin surface's `file-chooser` node. "Choose…"
|
|
4
|
+
* opens the explorer's OWN destination picker (`modals/DestinationPickerModal`,
|
|
5
|
+
* the same dialog Move to… / Copy to… use) in `choose` mode; `kind: "file"`
|
|
6
|
+
* asks it to list files and answer with the one ticked. The answer is an
|
|
7
|
+
* adapter-qualified path, which is what goes into `data.values[id]`.
|
|
8
|
+
*/
|
|
9
|
+
import { computed, ref } from 'vue';
|
|
10
|
+
import type { LocaleCode, ThemeMode } from '../../../types/ExplorerConfig';
|
|
11
|
+
import type { FileApi } from '../../../composables/useFileApi';
|
|
12
|
+
import { useLocale } from '../../../composables/useLocale';
|
|
13
|
+
import { labelOfWire, parentOfWire } from '../../../lib/destinationTree';
|
|
14
|
+
import DestinationPickerModal from '../../../modals/DestinationPickerModal.vue';
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
id: string;
|
|
18
|
+
kind?: string;
|
|
19
|
+
modelValue: string;
|
|
20
|
+
locale: LocaleCode;
|
|
21
|
+
theme?: ThemeMode;
|
|
22
|
+
/** Absent: the button is drawn disabled — there is nothing to browse with. */
|
|
23
|
+
api?: Pick<FileApi, 'index'>;
|
|
24
|
+
/** Storage names the picker may span. */
|
|
25
|
+
storages?: string[];
|
|
26
|
+
/** Where the picker opens — the folder the view was opened in. */
|
|
27
|
+
startAt?: string;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
invalid?: boolean;
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
(e: 'update:modelValue', v: string): void;
|
|
34
|
+
}>();
|
|
35
|
+
|
|
36
|
+
const { t } = useLocale(() => props.locale);
|
|
37
|
+
|
|
38
|
+
const open = ref(false);
|
|
39
|
+
const pick = computed<'file' | 'dir'>(() => (props.kind === 'dir' ? 'dir' : 'file'));
|
|
40
|
+
const shown = computed(() => (props.modelValue ? labelOfWire(props.modelValue, '') || props.modelValue : ''));
|
|
41
|
+
/** Where the picker opens: the chosen entry's folder, else where the view was opened. */
|
|
42
|
+
const openAt = computed(() => {
|
|
43
|
+
if (!props.modelValue) return props.startAt;
|
|
44
|
+
if (pick.value === 'dir') return props.modelValue;
|
|
45
|
+
return parentOfWire(props.modelValue, false) ?? props.modelValue;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function onPick(path: string) {
|
|
49
|
+
open.value = false;
|
|
50
|
+
emit('update:modelValue', path);
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<div class="fe-sfile" :class="{ 'is-invalid': invalid }" data-testid="surface-file-chooser">
|
|
56
|
+
<span
|
|
57
|
+
class="fe-sfile__value"
|
|
58
|
+
:class="{ 'fe-sfile__value--empty': !modelValue }"
|
|
59
|
+
:title="modelValue || undefined"
|
|
60
|
+
data-testid="surface-file-chooser-value"
|
|
61
|
+
>{{ shown || t('plugin.file.none') }}</span>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
class="fe-btn fe-btn--sm"
|
|
65
|
+
:disabled="disabled || !api"
|
|
66
|
+
data-testid="surface-file-chooser-open"
|
|
67
|
+
@click="open = true"
|
|
68
|
+
>
|
|
69
|
+
{{ t('plugin.file.choose') }}
|
|
70
|
+
</button>
|
|
71
|
+
<button
|
|
72
|
+
v-if="modelValue"
|
|
73
|
+
type="button"
|
|
74
|
+
class="fe-btn fe-btn--sm"
|
|
75
|
+
:disabled="disabled"
|
|
76
|
+
data-testid="surface-file-chooser-clear"
|
|
77
|
+
@click="emit('update:modelValue', '')"
|
|
78
|
+
>
|
|
79
|
+
{{ t('plugin.file.clear') }}
|
|
80
|
+
</button>
|
|
81
|
+
<DestinationPickerModal
|
|
82
|
+
v-if="api"
|
|
83
|
+
:open="open"
|
|
84
|
+
:api="api"
|
|
85
|
+
:locale="locale"
|
|
86
|
+
mode="choose"
|
|
87
|
+
:pick="pick"
|
|
88
|
+
:storages="storages"
|
|
89
|
+
:start-at="openAt"
|
|
90
|
+
@close="open = false"
|
|
91
|
+
@pick="onPick"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SurfaceForm — a plugin surface's `form` node.
|
|
4
|
+
*
|
|
5
|
+
* The fields are drawn by `StorageFields`, the ONE field renderer this
|
|
6
|
+
* package has (a storage driver's config form, an app's settings form and
|
|
7
|
+
* now a plugin screen all declare `Field[]` in the same shape). This file
|
|
8
|
+
* only translates: a plugin's `label`/`help` may be Text, and the values are
|
|
9
|
+
* flat in the surface's value map, keyed by field key.
|
|
10
|
+
*
|
|
11
|
+
* ⚠⚠ v3 — it also decides WHICH fields exist right now. A field may carry
|
|
12
|
+
* `show_when` / `required_when` (`lib/surfaceConditions`), so "the name of
|
|
13
|
+
* the new file" is simply not on the step while the output is a new version
|
|
14
|
+
* of the old one. A form may not present a contradiction, and the renderer
|
|
15
|
+
* is what makes that true rather than each plugin.
|
|
16
|
+
*
|
|
17
|
+
* ⚠ Hiding is only half of it: the hidden field's VALUE must not travel
|
|
18
|
+
* either, or the plugin receives an answer to a question it stopped asking.
|
|
19
|
+
* That half is `usePluginSurface`'s, because it owns what goes on the wire —
|
|
20
|
+
* this component could only blank the value, and `""` is an answer too.
|
|
21
|
+
*/
|
|
22
|
+
import { computed } from 'vue';
|
|
23
|
+
import type { LocaleCode } from '../../../types/ExplorerConfig';
|
|
24
|
+
import type { PluginField, PluginText } from '../../../types/Plugins';
|
|
25
|
+
import { labelOf } from '../../../lib/pluginLabel';
|
|
26
|
+
import { storageFieldOf } from '../../../lib/surfaceValues';
|
|
27
|
+
import { fieldRequired, visibleFields } from '../../../lib/surfaceConditions';
|
|
28
|
+
import StorageFields from '../../StorageFields.vue';
|
|
29
|
+
|
|
30
|
+
const props = defineProps<{
|
|
31
|
+
fields: PluginField[];
|
|
32
|
+
/** The surface's whole value map — field values are read and written flat. */
|
|
33
|
+
values: Record<string, unknown>;
|
|
34
|
+
locale: LocaleCode;
|
|
35
|
+
/** `surface.errors`: an entry under a field key marks it invalid, with those words. */
|
|
36
|
+
errors?: Record<string, PluginText>;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** Field keys the host is pointing at as unanswered (a blocked submit). */
|
|
39
|
+
invalid?: string[];
|
|
40
|
+
}>();
|
|
41
|
+
|
|
42
|
+
const emit = defineEmits<{
|
|
43
|
+
(e: 'update:values', v: Record<string, unknown>): void;
|
|
44
|
+
}>();
|
|
45
|
+
|
|
46
|
+
/** Only the fields whose `show_when` holds, conditions cascaded. */
|
|
47
|
+
const shown = computed(() => visibleFields(props.fields, props.values));
|
|
48
|
+
|
|
49
|
+
const mapped = computed(() =>
|
|
50
|
+
shown.value.map((f) => {
|
|
51
|
+
const sf = storageFieldOf(f, props.locale);
|
|
52
|
+
// `required_when` is the same promise as `required`, made conditionally:
|
|
53
|
+
// once the condition holds, the star is on the label and the submit is
|
|
54
|
+
// blocked — so nobody is refused by the server for a rule the screen
|
|
55
|
+
// never showed them.
|
|
56
|
+
return { ...sf, required: fieldRequired(f, props.values) };
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const fieldErrors = computed<Record<string, string>>(() => {
|
|
61
|
+
const out: Record<string, string> = {};
|
|
62
|
+
for (const f of mapped.value) {
|
|
63
|
+
const msg = labelOf(props.errors?.[f.key], props.locale);
|
|
64
|
+
if (msg) out[f.key] = msg;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
});
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<div class="fe-surface__form" data-testid="surface-form">
|
|
72
|
+
<StorageFields
|
|
73
|
+
:fields="mapped"
|
|
74
|
+
:model-value="values"
|
|
75
|
+
:locale="locale"
|
|
76
|
+
:errors="fieldErrors"
|
|
77
|
+
:invalid="invalid"
|
|
78
|
+
:disabled="disabled"
|
|
79
|
+
@update:model-value="(v: Record<string, unknown>) => emit('update:values', v)"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|