@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,339 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* PluginPageView — an app plugin's `page` view, drawn as a whole page.
|
|
4
|
+
*
|
|
5
|
+
* ⚠⚠ The SAME conversation a `modal` view has. `usePluginSurface` is the
|
|
6
|
+
* shared half (the surface, the values, the debounced `change`, the footer
|
|
7
|
+
* press, the `{op}` answer) and this file is only the frame: a title bar of
|
|
8
|
+
* its own, one column at reading width, the footer buttons at the bottom,
|
|
9
|
+
* and no dialog scroll inside a dialog. Writing a second conversation here
|
|
10
|
+
* is how a `page` wizard and a `modal` wizard start disagreeing about what
|
|
11
|
+
* `submit` sends.
|
|
12
|
+
*
|
|
13
|
+
* Its own two states, which a modal has no use for:
|
|
14
|
+
* • QUEUED — a `page` action never goes through `…/run`, so the job is
|
|
15
|
+
* born from this screen's submit. A tab that just closed itself would
|
|
16
|
+
* leave the person with no idea whether anything happened; instead the
|
|
17
|
+
* page says the job is queued and offers the way back to the operations
|
|
18
|
+
* tray that is now tracking it.
|
|
19
|
+
* • DONE — the plugin ended the conversation (`done`), with nothing queued.
|
|
20
|
+
*
|
|
21
|
+
* ⚠ It is in the package, not in the admin SPA: fm.example.com, the desktop shell
|
|
22
|
+
* and the embeds all reach `page` views through the same explorer, so a copy
|
|
23
|
+
* per host would be a copy per host to keep in step.
|
|
24
|
+
*/
|
|
25
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
26
|
+
import type { LocaleCode, ThemeMode } from '../../types/ExplorerConfig';
|
|
27
|
+
import type { FileApi } from '../../composables/useFileApi';
|
|
28
|
+
import { useLocale } from '../../composables/useLocale';
|
|
29
|
+
import { usePluginSurface } from '../../composables/usePluginSurface';
|
|
30
|
+
import { labelOf } from '../../lib/pluginLabel';
|
|
31
|
+
import SurfaceConversation from './SurfaceConversation.vue';
|
|
32
|
+
import SurfaceFooterButtons from './SurfaceFooterButtons.vue';
|
|
33
|
+
import SurfaceSections from './SurfaceSections.vue';
|
|
34
|
+
import { walkNodes } from '../../lib/surfaceValues';
|
|
35
|
+
import { openTargetFor } from '../../lib/surfaceOpen';
|
|
36
|
+
|
|
37
|
+
const props = defineProps<{
|
|
38
|
+
locale: LocaleCode;
|
|
39
|
+
theme?: ThemeMode;
|
|
40
|
+
api: FileApi;
|
|
41
|
+
plugin: string;
|
|
42
|
+
view: string;
|
|
43
|
+
/** Adapter-qualified path the page was opened on; echoed on every event. */
|
|
44
|
+
path?: string;
|
|
45
|
+
/** Storage names a file-chooser may span; where its picker opens. */
|
|
46
|
+
storages?: string[];
|
|
47
|
+
startAt?: string;
|
|
48
|
+
/** Where "back to the files" goes. Absent: the button is not drawn. */
|
|
49
|
+
backHref?: string;
|
|
50
|
+
/** Where the operations tray lives, for the queued state. */
|
|
51
|
+
opsHref?: string;
|
|
52
|
+
/**
|
|
53
|
+
* The prefix the SPA is served from (`/admin/`, `/drive/`) — what a
|
|
54
|
+
* `Surface.open` address is built against (v3 §3.0). Absent: the request
|
|
55
|
+
* is dropped, because a wrong address is worse than none.
|
|
56
|
+
*/
|
|
57
|
+
mountBase?: string;
|
|
58
|
+
/**
|
|
59
|
+
* The CHROME around the conversation; the conversation itself is the same
|
|
60
|
+
* either way.
|
|
61
|
+
*
|
|
62
|
+
* `page` (the default) is a browser tab of its own: a title bar, one column
|
|
63
|
+
* at reading width, the viewport's height, a footer stuck to its bottom.
|
|
64
|
+
* `embedded` is the same screen drawn INSIDE a host's page — the admin
|
|
65
|
+
* panel's Apps section, an embed that already has a header — and brings
|
|
66
|
+
* none of those three, because the host has already said where the screen
|
|
67
|
+
* begins, how wide it is and what it is called.
|
|
68
|
+
*
|
|
69
|
+
* ⚠ A step carrying a document keeps the INLINE layout when embedded. The
|
|
70
|
+
* `page` rule fits a PDF to the VIEWPORT (v3 §3.2); inside a host's page
|
|
71
|
+
* the viewport is not this frame's box, so the same rule would fit the
|
|
72
|
+
* document to a height the frame does not have.
|
|
73
|
+
*/
|
|
74
|
+
frame?: 'page' | 'embedded';
|
|
75
|
+
/**
|
|
76
|
+
* The section of a home page to open (`surface.sections`), as the host's
|
|
77
|
+
* address says. ⚠ The HOST owns it: choosing a section emits `section`
|
|
78
|
+
* and the host puts it in its URL, which comes back here — so Back walks
|
|
79
|
+
* the sections, and a link names one.
|
|
80
|
+
*/
|
|
81
|
+
section?: string;
|
|
82
|
+
}>();
|
|
83
|
+
|
|
84
|
+
const emit = defineEmits<{
|
|
85
|
+
/** The server enqueued a job for this page; the raw ops row. */
|
|
86
|
+
(e: 'op', op: Record<string, unknown>): void;
|
|
87
|
+
/**
|
|
88
|
+
* The person chose another section of a home page — or, with
|
|
89
|
+
* `{replace: true}`, a page opened with NO section learned which one the
|
|
90
|
+
* plugin shows first: the address should name it without adding a step
|
|
91
|
+
* to history.
|
|
92
|
+
*/
|
|
93
|
+
(e: 'section', id: string, how?: { replace?: boolean }): void;
|
|
94
|
+
}>();
|
|
95
|
+
|
|
96
|
+
const { t } = useLocale(() => props.locale);
|
|
97
|
+
|
|
98
|
+
/** Drawn inside a host's page rather than in a tab of its own. */
|
|
99
|
+
const embedded = computed(() => props.frame === 'embedded');
|
|
100
|
+
|
|
101
|
+
type PageState = 'loading' | 'surface' | 'queued' | 'done' | 'error';
|
|
102
|
+
const state = ref<PageState>('loading');
|
|
103
|
+
const loadError = ref('');
|
|
104
|
+
const toast = ref('');
|
|
105
|
+
|
|
106
|
+
const conv = usePluginSurface(
|
|
107
|
+
{
|
|
108
|
+
api: props.api,
|
|
109
|
+
plugin: props.plugin,
|
|
110
|
+
view: props.view,
|
|
111
|
+
path: () => props.path,
|
|
112
|
+
locale: () => props.locale,
|
|
113
|
+
errorText: () => t('plugin.view.error'),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
// ⚠ `onOp` fires BEFORE `onDone` (usePluginSurface posts them in that
|
|
117
|
+
// order), so the queued state has to be set after both — otherwise
|
|
118
|
+
// `onDone` would overwrite it with "finished" and the person would never
|
|
119
|
+
// see that a job is running.
|
|
120
|
+
/**
|
|
121
|
+
* v3 §3.0 — "go to this file, and start that screen on it".
|
|
122
|
+
*
|
|
123
|
+
* ⚠ A real navigation, not a tab: this IS a whole page, and the person
|
|
124
|
+
* asked to be taken to a document. The address is the explorer deep link
|
|
125
|
+
* a notification already uses, so the placement question is answered
|
|
126
|
+
* where it is answerable — the explorer knows which of the plugin's
|
|
127
|
+
* views is a `page` and opens that one in its own tab.
|
|
128
|
+
*/
|
|
129
|
+
onOpen: (req) => {
|
|
130
|
+
const target = openTargetFor(req, { plugin: props.plugin, base: props.mountBase });
|
|
131
|
+
if (!target.href) return;
|
|
132
|
+
try {
|
|
133
|
+
if (target.newTab) {
|
|
134
|
+
const win = window.open(target.href, '_blank');
|
|
135
|
+
if (win) win.opener = null;
|
|
136
|
+
else window.location.assign(target.href);
|
|
137
|
+
} else {
|
|
138
|
+
window.location.assign(target.href);
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
/* a blocked navigation leaves the screen as it is */
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
onOp: (op) => {
|
|
145
|
+
emit('op', op);
|
|
146
|
+
state.value = 'queued';
|
|
147
|
+
},
|
|
148
|
+
onDone: () => {
|
|
149
|
+
if (state.value !== 'queued') state.value = 'done';
|
|
150
|
+
},
|
|
151
|
+
onToast: (m) => (toast.value = m),
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const current = conv.current;
|
|
156
|
+
const title = computed(() => labelOf(current.value?.title, props.locale) || props.view);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The section being shown. It starts from the host's address and follows
|
|
160
|
+
* it; a choice made here moves it at once (so the menu answers the click
|
|
161
|
+
* even in a host that does not keep an address), then tells the host.
|
|
162
|
+
*/
|
|
163
|
+
const wanted = ref(props.section ?? '');
|
|
164
|
+
const sections = computed(() => current.value?.sections ?? []);
|
|
165
|
+
/** Which entry is lit: what the plugin says it drew, else what was asked. */
|
|
166
|
+
const activeSection = computed(() => current.value?.section || wanted.value);
|
|
167
|
+
|
|
168
|
+
function chooseSection(id: string): void {
|
|
169
|
+
if (!id || id === activeSection.value) return;
|
|
170
|
+
wanted.value = id;
|
|
171
|
+
emit('section', id);
|
|
172
|
+
void load();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
watch(
|
|
176
|
+
() => props.section,
|
|
177
|
+
(s) => {
|
|
178
|
+
const next = s ?? '';
|
|
179
|
+
// The host echoing the choice made above is not a new request.
|
|
180
|
+
if (next === wanted.value) return;
|
|
181
|
+
wanted.value = next;
|
|
182
|
+
void load();
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
async function load(): Promise<void> {
|
|
187
|
+
// ⚠ A section change keeps the page standing (menu and all) while the
|
|
188
|
+
// next table is fetched: a switch that blanked the screen to "Loading…"
|
|
189
|
+
// made the menu itself flash away under the pointer that chose it.
|
|
190
|
+
if (!current.value) state.value = 'loading';
|
|
191
|
+
loadError.value = '';
|
|
192
|
+
try {
|
|
193
|
+
// The section only when there is one: a page that has no menu asks the
|
|
194
|
+
// same question it always asked.
|
|
195
|
+
const res = wanted.value
|
|
196
|
+
? await props.api.pluginView(props.plugin, props.view, props.path, wanted.value)
|
|
197
|
+
: await props.api.pluginView(props.plugin, props.view, props.path);
|
|
198
|
+
if (!res?.surface) {
|
|
199
|
+
loadError.value = t('plugin.view.error');
|
|
200
|
+
state.value = 'error';
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
conv.setSurface(res.surface);
|
|
204
|
+
state.value = 'surface';
|
|
205
|
+
// ⚠ Opened without a section, the page shows the plugin's first choice
|
|
206
|
+
// — and the address has to say which, or choosing that same entry from
|
|
207
|
+
// the menu changes nothing (measured in the e2e round: a click on the
|
|
208
|
+
// lit "requested" left the URL bare), and a copied link opens wherever
|
|
209
|
+
// the plugin's default happens to be that day.
|
|
210
|
+
const shown = res.surface.section ?? '';
|
|
211
|
+
if (!wanted.value && shown) {
|
|
212
|
+
wanted.value = shown;
|
|
213
|
+
emit('section', shown, { replace: true });
|
|
214
|
+
}
|
|
215
|
+
} catch (e) {
|
|
216
|
+
// ⚠ An app's own error text never met the catalogue — isolate it.
|
|
217
|
+
loadError.value = t.foreign?.(String((e as Error)?.message ?? '')) || t('plugin.view.error');
|
|
218
|
+
state.value = 'error';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
onMounted(() => void load());
|
|
223
|
+
defineExpose({ reload: load });
|
|
224
|
+
/**
|
|
225
|
+
* Is this step showing a DOCUMENT?
|
|
226
|
+
*
|
|
227
|
+
* ⚠⚠ v3 §3.2 — "in `page` placement the node takes the whole viewport
|
|
228
|
+
* minus the step header and footer, and fits the page to it". The complaint
|
|
229
|
+
* that started the round was a signature page you had to scroll in two
|
|
230
|
+
* directions: the document was fitted to a 960px text column inside a page
|
|
231
|
+
* that scrolled, so a portrait page ran off the bottom and the boxes were
|
|
232
|
+
* found by hunting.
|
|
233
|
+
*
|
|
234
|
+
* So the FRAME changes shape when the surface carries one: the header and
|
|
235
|
+
* the footer keep their size, the body becomes the remainder, and the node
|
|
236
|
+
* fits the page into it (`SurfacePdfFields.fitWidth`). Only then — an
|
|
237
|
+
* ordinary form step is still a readable column, and locking every step to
|
|
238
|
+
* the viewport would turn a four-field form into a tall empty screen.
|
|
239
|
+
*/
|
|
240
|
+
/**
|
|
241
|
+
* ⚠⚠ ...and only the step that REALLY shows one. A `pdf-fields` node in
|
|
242
|
+
* `define` mode draws no document at all — it is a column of cards, one per
|
|
243
|
+
* box — so shaping the frame to the window for it locked a growing list
|
|
244
|
+
* inside `height: 100vh; overflow: hidden` with no scroller of its own: the
|
|
245
|
+
* fourth box and everything after it sat below the fold, unreachable (the
|
|
246
|
+
* owner, 2026-09-23: "kutucukları oluşturduğumuz sayfada overflow hidden
|
|
247
|
+
* olduğu için aşağıda kalan ekstra kutucuklara kaydırarak gidemiyoruz").
|
|
248
|
+
* `place` and `edit` do carry the document and keep the window shape.
|
|
249
|
+
*/
|
|
250
|
+
const showsDocument = computed(() => {
|
|
251
|
+
let found = false;
|
|
252
|
+
walkNodes(conv.current.value?.nodes, (n) => {
|
|
253
|
+
if (n.type !== 'pdf-fields') return;
|
|
254
|
+
if ((n.props as { mode?: unknown } | undefined)?.mode === 'define') return;
|
|
255
|
+
found = true;
|
|
256
|
+
});
|
|
257
|
+
return found;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
</script>
|
|
261
|
+
|
|
262
|
+
<template>
|
|
263
|
+
<div
|
|
264
|
+
class="fe fe-apppage"
|
|
265
|
+
:class="[
|
|
266
|
+
theme === 'dark' ? 'fe--theme-dark' : theme === 'light' ? 'fe--theme-light' : '',
|
|
267
|
+
{ 'is-doc': showsDocument && !embedded, 'fe-apppage--embedded': embedded },
|
|
268
|
+
]"
|
|
269
|
+
data-testid="plugin-page"
|
|
270
|
+
>
|
|
271
|
+
<header v-if="!embedded" class="fe-apppage__bar">
|
|
272
|
+
<h1 class="fe-apppage__title" data-testid="plugin-page-title">{{ title }}</h1>
|
|
273
|
+
<p v-if="path" class="fe-apppage__path" :title="path">{{ path }}</p>
|
|
274
|
+
<a v-if="backHref" class="fe-btn fe-apppage__back" :href="backHref" data-testid="plugin-page-back">
|
|
275
|
+
{{ t('plugin.page_view.back') }}
|
|
276
|
+
</a>
|
|
277
|
+
</header>
|
|
278
|
+
|
|
279
|
+
<main class="fe-apppage__body">
|
|
280
|
+
<SurfaceSections
|
|
281
|
+
v-if="state === 'surface' && sections.length"
|
|
282
|
+
:sections="sections"
|
|
283
|
+
:active="activeSection"
|
|
284
|
+
:locale="locale"
|
|
285
|
+
:label="title"
|
|
286
|
+
:disabled="conv.busy.value"
|
|
287
|
+
@select="chooseSection"
|
|
288
|
+
/>
|
|
289
|
+
<p v-if="state === 'loading'" class="fe-surface__text fe-surface__text--muted" data-testid="plugin-page-loading">
|
|
290
|
+
{{ t('plugin.view.loading') }}
|
|
291
|
+
</p>
|
|
292
|
+
|
|
293
|
+
<SurfaceConversation
|
|
294
|
+
v-else-if="state === 'surface'"
|
|
295
|
+
:conv="conv"
|
|
296
|
+
:locale="locale"
|
|
297
|
+
:theme="theme"
|
|
298
|
+
:api="api"
|
|
299
|
+
:plugin="plugin"
|
|
300
|
+
:storages="storages"
|
|
301
|
+
:start-at="startAt"
|
|
302
|
+
:toast="toast"
|
|
303
|
+
:layout="embedded ? 'inline' : 'page'"
|
|
304
|
+
/>
|
|
305
|
+
|
|
306
|
+
<div v-else-if="state === 'queued'" class="fe-apppage__state" data-testid="plugin-page-queued">
|
|
307
|
+
<h2 class="fe-apppage__state-title">{{ t('plugin.page_view.queued_title') }}</h2>
|
|
308
|
+
<p class="fe-surface__text">{{ t('plugin.page_view.queued_text') }}</p>
|
|
309
|
+
<div class="fe-apppage__state-actions">
|
|
310
|
+
<a v-if="opsHref" class="fe-btn fe-btn--primary" :href="opsHref" data-testid="plugin-page-ops">
|
|
311
|
+
{{ t('plugin.page_view.open_ops') }}
|
|
312
|
+
</a>
|
|
313
|
+
<a v-if="backHref" class="fe-btn" :href="backHref">{{ t('plugin.page_view.back') }}</a>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div v-else-if="state === 'done'" class="fe-apppage__state" data-testid="plugin-page-done">
|
|
318
|
+
<h2 class="fe-apppage__state-title">{{ t('plugin.page_view.done_title') }}</h2>
|
|
319
|
+
<p class="fe-surface__text">{{ t('plugin.page_view.done_text') }}</p>
|
|
320
|
+
<div class="fe-apppage__state-actions">
|
|
321
|
+
<a v-if="backHref" class="fe-btn fe-btn--primary" :href="backHref">{{ t('plugin.page_view.back') }}</a>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<div v-else class="fe-apppage__state" data-testid="plugin-page-error">
|
|
326
|
+
<h2 class="fe-apppage__state-title">{{ t('plugin.page_view.error_title') }}</h2>
|
|
327
|
+
<p v-if="loadError" class="fe-surface__error">{{ loadError }}</p>
|
|
328
|
+
<div class="fe-apppage__state-actions">
|
|
329
|
+
<button type="button" class="fe-btn" @click="load()">{{ t('plugin.page_view.retry') }}</button>
|
|
330
|
+
<a v-if="backHref" class="fe-btn" :href="backHref">{{ t('plugin.page_view.back') }}</a>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</main>
|
|
334
|
+
|
|
335
|
+
<footer v-if="state === 'surface' && conv.footer.value.length" class="fe-apppage__foot">
|
|
336
|
+
<SurfaceFooterButtons :conv="conv" :locale="locale" testid-prefix="plugin-page" />
|
|
337
|
+
</footer>
|
|
338
|
+
</div>
|
|
339
|
+
</template>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* PluginViewModal — the dialog an app plugin's `modal` view opens in (and
|
|
4
|
+
* the frame a `home` view is drawn in, full-size).
|
|
5
|
+
*
|
|
6
|
+
* The explorer hands it the first surface (the answer to `run`, or the
|
|
7
|
+
* `open` event of a view); from then on the conversation is
|
|
8
|
+
* `usePluginSurface`'s — the same one the inspector section holds — and
|
|
9
|
+
* this file is only the frame: title, size, the footer buttons, Close.
|
|
10
|
+
* When the server answers `{op}` the row goes up (`op`) and the dialog
|
|
11
|
+
* closes.
|
|
12
|
+
*
|
|
13
|
+
* ⚠ No iframe, no `v-html`, no script: SurfaceRenderer draws nodes with our
|
|
14
|
+
* own components and shows an unknown type as unknown.
|
|
15
|
+
*
|
|
16
|
+
* ⚠ The body and the buttons are `SurfaceConversation` / `SurfaceFooterButtons`
|
|
17
|
+
* — shared with the `page` frame and the public card, so a fix to any of the
|
|
18
|
+
* five things a surface body draws reaches all three.
|
|
19
|
+
*/
|
|
20
|
+
import { computed, watch } from 'vue';
|
|
21
|
+
import type { LocaleCode, ThemeMode } from '../../types/ExplorerConfig';
|
|
22
|
+
import type { FileApi } from '../../composables/useFileApi';
|
|
23
|
+
import type { PluginSurface, SurfaceOpenRequest } from '../../types/Plugins';
|
|
24
|
+
import { useLocale } from '../../composables/useLocale';
|
|
25
|
+
import { usePluginSurface } from '../../composables/usePluginSurface';
|
|
26
|
+
import { labelOf } from '../../lib/pluginLabel';
|
|
27
|
+
import Modal from '../../modals/Modal.vue';
|
|
28
|
+
import SurfaceConversation from './SurfaceConversation.vue';
|
|
29
|
+
import SurfaceSections from './SurfaceSections.vue';
|
|
30
|
+
import SurfaceFooterButtons from './SurfaceFooterButtons.vue';
|
|
31
|
+
|
|
32
|
+
const props = defineProps<{
|
|
33
|
+
open: boolean;
|
|
34
|
+
locale: LocaleCode;
|
|
35
|
+
theme?: ThemeMode;
|
|
36
|
+
api: FileApi;
|
|
37
|
+
plugin: string;
|
|
38
|
+
view: string;
|
|
39
|
+
/** The first screen. */
|
|
40
|
+
surface: PluginSurface | null;
|
|
41
|
+
/** Adapter-qualified path of the row the view was opened on (echoed on every event). */
|
|
42
|
+
path?: string;
|
|
43
|
+
/** Storage names a file-chooser may span; where its picker opens. */
|
|
44
|
+
storages?: string[];
|
|
45
|
+
startAt?: string;
|
|
46
|
+
/** Forces the dialog size (a `home` view is drawn `xl`); else the surface's own. */
|
|
47
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
const emit = defineEmits<{
|
|
51
|
+
(e: 'close'): void;
|
|
52
|
+
/** The server enqueued a job for this view; the raw ops row. */
|
|
53
|
+
(e: 'op', op: Record<string, unknown>): void;
|
|
54
|
+
(e: 'toast', message: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* v3 §3.0 — the answer says "go to this file". Reported UP rather than
|
|
57
|
+
* acted on here: the explorer is what can show a file and start a screen
|
|
58
|
+
* on it (`openAppTarget`), and a dialog navigating the window under itself
|
|
59
|
+
* would throw away the folder the person was standing in.
|
|
60
|
+
*/
|
|
61
|
+
(e: 'open', req: SurfaceOpenRequest): void;
|
|
62
|
+
}>();
|
|
63
|
+
|
|
64
|
+
const { t } = useLocale(() => props.locale);
|
|
65
|
+
|
|
66
|
+
const conv = usePluginSurface(
|
|
67
|
+
{
|
|
68
|
+
api: props.api,
|
|
69
|
+
plugin: props.plugin,
|
|
70
|
+
view: props.view,
|
|
71
|
+
path: () => props.path,
|
|
72
|
+
locale: () => props.locale,
|
|
73
|
+
errorText: () => t('plugin.view.error'),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
onOp: (op) => emit('op', op),
|
|
77
|
+
onDone: () => emit('close'),
|
|
78
|
+
onToast: (m) => emit('toast', m),
|
|
79
|
+
onOpen: (req) => emit('open', req),
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
watch(
|
|
84
|
+
() => props.surface,
|
|
85
|
+
(s) => conv.setSurface(s),
|
|
86
|
+
{ immediate: true },
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const current = conv.current;
|
|
90
|
+
const title = computed(() => labelOf(current.value?.title, props.locale) || props.view);
|
|
91
|
+
/**
|
|
92
|
+
* A home page's menu, when a host with no page of its own draws one in this
|
|
93
|
+
* dialog (the degraded frame — an embed without `pluginPageBase`). There is
|
|
94
|
+
* no address to keep here, so a choice simply asks for that section.
|
|
95
|
+
*/
|
|
96
|
+
const sections = computed(() => current.value?.sections ?? []);
|
|
97
|
+
async function chooseSection(id: string): Promise<void> {
|
|
98
|
+
if (!id || id === current.value?.section) return;
|
|
99
|
+
try {
|
|
100
|
+
const res = await props.api.pluginView(props.plugin, props.view, props.path, id);
|
|
101
|
+
if (res?.surface) conv.setSurface(res.surface);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// ⚠ An app's own error text never met the catalogue — isolate it.
|
|
104
|
+
emit('toast', t.foreign?.(String((e as Error)?.message ?? '')) || t('plugin.view.error'));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const size = computed<'sm' | 'md' | 'lg' | 'xl'>(() => {
|
|
109
|
+
if (props.size) return props.size;
|
|
110
|
+
const s = current.value?.size;
|
|
111
|
+
return s === 'sm' || s === 'lg' || s === 'xl' ? s : 'md';
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<template>
|
|
116
|
+
<Modal :open="open" :title="title" :size="size" :theme="theme" @close="emit('close')">
|
|
117
|
+
<div class="fe-plugin-view" data-testid="plugin-view">
|
|
118
|
+
<SurfaceSections
|
|
119
|
+
v-if="sections.length"
|
|
120
|
+
:sections="sections"
|
|
121
|
+
:active="current?.section"
|
|
122
|
+
:locale="locale"
|
|
123
|
+
:label="title"
|
|
124
|
+
:disabled="conv.busy.value"
|
|
125
|
+
@select="chooseSection"
|
|
126
|
+
/>
|
|
127
|
+
<SurfaceConversation
|
|
128
|
+
:conv="conv"
|
|
129
|
+
:locale="locale"
|
|
130
|
+
:theme="theme"
|
|
131
|
+
:api="api"
|
|
132
|
+
:plugin="plugin"
|
|
133
|
+
:storages="storages"
|
|
134
|
+
:start-at="startAt"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
<!-- ⚠ The frame's own Close only when the screen brought no buttons of
|
|
138
|
+
its own. A plugin's footer already carries its way out (Cancel,
|
|
139
|
+
Back), the header has its ×, and Escape and an outside click close
|
|
140
|
+
the dialog — a "Close" beside the plugin's "Cancel" was two buttons
|
|
141
|
+
doing one thing (the converter's first step read "Kapat · Vazgeç ·
|
|
142
|
+
İleri", release-candidate sweep 2026-09-21). A screen with no
|
|
143
|
+
buttons at all — a result, a notice — still gets one, so no screen
|
|
144
|
+
is a dead end for a pointer. -->
|
|
145
|
+
<template #actions>
|
|
146
|
+
<button
|
|
147
|
+
v-if="!conv.footer.value.length"
|
|
148
|
+
type="button"
|
|
149
|
+
class="fe-btn"
|
|
150
|
+
:disabled="conv.busy.value"
|
|
151
|
+
data-testid="plugin-view-close"
|
|
152
|
+
@click="emit('close')"
|
|
153
|
+
>
|
|
154
|
+
{{ t('plugin.view.close') }}
|
|
155
|
+
</button>
|
|
156
|
+
<SurfaceFooterButtons :conv="conv" :locale="locale" testid-prefix="plugin-view" />
|
|
157
|
+
</template>
|
|
158
|
+
</Modal>
|
|
159
|
+
</template>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SurfaceConversation — the body of ANY screen a plugin is talking through.
|
|
4
|
+
*
|
|
5
|
+
* Three frames draw a plugin surface: the modal a `modal` view opens, the
|
|
6
|
+
* whole page a `page` view opens, and the card an outside participant gets
|
|
7
|
+
* at `/p/<token>`. The FRAME is what differs (a dialog, a document, a card);
|
|
8
|
+
* the body is the same five things every time — the nodes, the toast the
|
|
9
|
+
* answer carried, "working…", the failure, and the footer buttons.
|
|
10
|
+
*
|
|
11
|
+
* ⚠⚠ Written once for that reason. Three copies of this block existed by the
|
|
12
|
+
* time the `page` placement arrived, and the drift had already started: the
|
|
13
|
+
* modal showed no toast at all, so a plugin that answered `{toast}` from a
|
|
14
|
+
* dialog was simply not heard. Anything added here — a node type's wiring, a
|
|
15
|
+
* busy affordance, an aria role — now reaches all three frames or none.
|
|
16
|
+
*/
|
|
17
|
+
import type { LocaleCode, ThemeMode } from '../../types/ExplorerConfig';
|
|
18
|
+
import type { PluginSurfaceStore } from '../../composables/usePluginSurface';
|
|
19
|
+
import type { FileRefResolver } from '../../types/Plugins';
|
|
20
|
+
import { useLocale } from '../../composables/useLocale';
|
|
21
|
+
import SurfaceRenderer, { type SurfaceApi } from './SurfaceRenderer.vue';
|
|
22
|
+
|
|
23
|
+
const props = defineProps<{
|
|
24
|
+
/** The conversation this body belongs to (`usePluginSurface`). */
|
|
25
|
+
conv: PluginSurfaceStore;
|
|
26
|
+
locale: LocaleCode;
|
|
27
|
+
theme?: ThemeMode;
|
|
28
|
+
/** Absent in a bare render (the public page has no explorer API). */
|
|
29
|
+
api?: SurfaceApi;
|
|
30
|
+
/** The plugin the surface belongs to — the people-picker's lookup asks for it. */
|
|
31
|
+
plugin?: string;
|
|
32
|
+
storages?: string[];
|
|
33
|
+
startAt?: string;
|
|
34
|
+
/** A public page's exposed copies (`pub:N`) → a URL the browser may load. */
|
|
35
|
+
fileUrl?: FileRefResolver;
|
|
36
|
+
/** The last toast the frame collected, if it shows one inline. */
|
|
37
|
+
toast?: string;
|
|
38
|
+
/**
|
|
39
|
+
* v3 §3.2 — `page` lets a `pdf-fields` node take the viewport instead of
|
|
40
|
+
* sitting in the document flow. The FRAME knows which it is; the nodes do
|
|
41
|
+
* not, and must not guess from the window size (an embed in a narrow
|
|
42
|
+
* column is not a page).
|
|
43
|
+
*/
|
|
44
|
+
layout?: 'inline' | 'page';
|
|
45
|
+
/** Where the document may grow to in `page` layout — the frame's own box. */
|
|
46
|
+
pageHeight?: string;
|
|
47
|
+
}>();
|
|
48
|
+
|
|
49
|
+
const { t } = useLocale(() => props.locale);
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<SurfaceRenderer
|
|
54
|
+
v-if="conv.current.value"
|
|
55
|
+
:nodes="conv.current.value.nodes ?? []"
|
|
56
|
+
:locale="locale"
|
|
57
|
+
:theme="theme"
|
|
58
|
+
:errors="conv.errors.value"
|
|
59
|
+
:values="conv.values.value"
|
|
60
|
+
:api="api"
|
|
61
|
+
:plugin="plugin"
|
|
62
|
+
:storages="storages"
|
|
63
|
+
:start-at="startAt"
|
|
64
|
+
:file-url="fileUrl"
|
|
65
|
+
:layout="layout"
|
|
66
|
+
:page-height="pageHeight"
|
|
67
|
+
:invalid-keys="conv.invalidKeys.value"
|
|
68
|
+
:disabled="conv.busy.value"
|
|
69
|
+
@update:values="conv.updateValues"
|
|
70
|
+
@change="conv.scheduleChange"
|
|
71
|
+
@action="conv.rowAction"
|
|
72
|
+
/>
|
|
73
|
+
<p v-if="toast" class="fe-surface__text fe-surface__text--info" role="status">{{ toast }}</p>
|
|
74
|
+
<p v-if="conv.busy.value" class="fe-surface__text fe-surface__text--muted">{{ t('plugin.view.busy') }}</p>
|
|
75
|
+
<p v-if="conv.failure.value" class="fe-surface__error" role="alert">{{ conv.failure.value }}</p>
|
|
76
|
+
</template>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SurfaceFooterButtons — a surface's footer, wherever the frame puts it.
|
|
4
|
+
*
|
|
5
|
+
* The rule a plugin's buttons follow is not the frame's business: the
|
|
6
|
+
* primary one posts `submit`, every other one posts `action`, a `danger` one
|
|
7
|
+
* is red, and none of them is clickable while the conversation is busy
|
|
8
|
+
* (`usePluginSurface.press`). Three frames (dialog, page, public shell)
|
|
9
|
+
* place the row differently and must not each re-derive that.
|
|
10
|
+
*
|
|
11
|
+
* ⚠⚠ v3 §2 rule 3 — on a STEP (a surface carrying a `steps` node) the row is
|
|
12
|
+
* arranged rather than merely listed: Back on the left, one primary on the
|
|
13
|
+
* right, everything else between them, and a second `primary: true` is drawn
|
|
14
|
+
* as an ordinary button. `lib/surfaceSteps` holds the arrangement so the
|
|
15
|
+
* frames, the tests and the plugin test kit all read one answer.
|
|
16
|
+
*
|
|
17
|
+
* ⚠ `testidPrefix` is the one thing that legitimately differs per frame: the
|
|
18
|
+
* suites address the dialog's buttons and the page's separately, so a shared
|
|
19
|
+
* id would make a passing test ambiguous about which screen it measured.
|
|
20
|
+
*/
|
|
21
|
+
import { computed } from 'vue';
|
|
22
|
+
import type { LocaleCode } from '../../types/ExplorerConfig';
|
|
23
|
+
import type { PluginSurfaceStore } from '../../composables/usePluginSurface';
|
|
24
|
+
import type { SurfaceAction } from '../../types/Plugins';
|
|
25
|
+
import { labelOf } from '../../lib/pluginLabel';
|
|
26
|
+
import { hasSteps, stepFooter } from '../../lib/surfaceSteps';
|
|
27
|
+
|
|
28
|
+
const props = defineProps<{
|
|
29
|
+
conv: PluginSurfaceStore;
|
|
30
|
+
locale: LocaleCode;
|
|
31
|
+
/** `plugin-view` → `data-testid="plugin-view-action-<id>"`. */
|
|
32
|
+
testidPrefix: string;
|
|
33
|
+
/** The inspector's row is compact; every other frame uses the full size. */
|
|
34
|
+
small?: boolean;
|
|
35
|
+
}>();
|
|
36
|
+
|
|
37
|
+
const stepped = computed(() => hasSteps(props.conv.current.value?.nodes));
|
|
38
|
+
|
|
39
|
+
const arranged = computed(() => stepFooter(props.conv.footer.value, stepped.value));
|
|
40
|
+
|
|
41
|
+
/** Back, the ordinary buttons, then the one primary — in that DOM order. */
|
|
42
|
+
const ordered = computed<SurfaceAction[]>(() => {
|
|
43
|
+
const f = arranged.value;
|
|
44
|
+
return [...(f.back ? [f.back] : []), ...f.others, ...(f.primary ? [f.primary] : [])];
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** Only the arranged primary is primary; a plugin's second one is not. */
|
|
48
|
+
function isPrimary(a: SurfaceAction): boolean {
|
|
49
|
+
if (!stepped.value) return a.primary === true;
|
|
50
|
+
return a === arranged.value.primary;
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<button
|
|
56
|
+
v-for="a in ordered"
|
|
57
|
+
:key="a.id"
|
|
58
|
+
type="button"
|
|
59
|
+
class="fe-btn"
|
|
60
|
+
:class="{
|
|
61
|
+
'fe-btn--sm': small,
|
|
62
|
+
'fe-btn--primary': isPrimary(a) && !a.danger,
|
|
63
|
+
'fe-btn--danger': a.danger,
|
|
64
|
+
'fe-surface__back': stepped && a === arranged.back,
|
|
65
|
+
}"
|
|
66
|
+
:disabled="props.conv.busy.value || a.disabled"
|
|
67
|
+
:data-testid="`${testidPrefix}-action-${a.id}`"
|
|
68
|
+
@click="props.conv.press(a)"
|
|
69
|
+
>
|
|
70
|
+
{{ labelOf(a.label, locale) }}
|
|
71
|
+
</button>
|
|
72
|
+
</template>
|
|
73
|
+
|