@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
|
@@ -17,10 +17,23 @@ import { computed, ref, onBeforeUnmount } from 'vue';
|
|
|
17
17
|
import type { ExplorerConfig } from '../types/ExplorerConfig';
|
|
18
18
|
import type { FileApi } from './useFileApi';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The queue kinds this package knows by name. A server may emit others (an
|
|
22
|
+
* app-plugin job is `plugin-action` on the wire and `plugin` here); anything
|
|
23
|
+
* unknown is carried through verbatim rather than being read as a delete.
|
|
24
|
+
*/
|
|
25
|
+
export type PendingOpType = 'copy' | 'move' | 'delete' | 'plugin' | 'trash-empty' | (string & {});
|
|
26
|
+
|
|
27
|
+
/** One produced file of a finished plugin job, as an adapter-qualified path. */
|
|
28
|
+
export interface PendingOpOutput {
|
|
29
|
+
path: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
export interface PendingOp {
|
|
21
33
|
id: number;
|
|
22
|
-
op_type:
|
|
23
|
-
|
|
34
|
+
op_type: PendingOpType;
|
|
35
|
+
/** `cancelled` — somebody stopped it (POST /ops/{id}/cancel). */
|
|
36
|
+
status: 'pending' | 'running' | 'done' | 'error' | 'cancelled';
|
|
24
37
|
progress_total: number;
|
|
25
38
|
progress_done: number;
|
|
26
39
|
/** Running cross-storage transfer's bytes (issue #27); absent otherwise. */
|
|
@@ -30,9 +43,24 @@ export interface PendingOp {
|
|
|
30
43
|
source_dir: string | null;
|
|
31
44
|
source_count: number;
|
|
32
45
|
error_message: string | null;
|
|
46
|
+
/** A failed APP job, classified by the server (lib/errorWords `jobFailure`
|
|
47
|
+
* says it); absent on copy/move/delete rows and on an older server. */
|
|
48
|
+
error_code?: string;
|
|
49
|
+
/** The engine a job needed and the server lacks (`error_code: engine_missing`). */
|
|
50
|
+
error_engine?: string;
|
|
33
51
|
started_at: string | null;
|
|
34
52
|
finished_at: string | null;
|
|
35
53
|
created_at: string | null;
|
|
54
|
+
/* App-plugin jobs (`op_type: "plugin-action"` on the wire) — absent on
|
|
55
|
+
* copy/move/delete rows. See docs/APP-PLUGINS-API.md → Ops rows. */
|
|
56
|
+
plugin?: string;
|
|
57
|
+
action?: string;
|
|
58
|
+
/** The action's label in the caller's locale — the tray's row title. */
|
|
59
|
+
label?: string;
|
|
60
|
+
/** The last `job_progress` message. */
|
|
61
|
+
message?: string;
|
|
62
|
+
/** Files the job committed, once finished. */
|
|
63
|
+
outputs?: PendingOpOutput[];
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
const POLL_MS = 2000;
|
|
@@ -46,7 +74,7 @@ const RETAIN_MS = 8000;
|
|
|
46
74
|
* running op (progress_* were undefined) and never treated an 'ok'/'failed'
|
|
47
75
|
* row as terminal. Tolerant of an already-normalized shape (idempotent).
|
|
48
76
|
*/
|
|
49
|
-
function normalizeOp(raw: Record<string, unknown>): PendingOp {
|
|
77
|
+
export function normalizeOp(raw: Record<string, unknown>): PendingOp {
|
|
50
78
|
const num = (...vals: unknown[]): number => {
|
|
51
79
|
for (const v of vals) if (typeof v === 'number') return v;
|
|
52
80
|
return 0;
|
|
@@ -56,19 +84,35 @@ function normalizeOp(raw: Record<string, unknown>): PendingOp {
|
|
|
56
84
|
return null;
|
|
57
85
|
};
|
|
58
86
|
const rawStatus = String(raw.status ?? 'pending');
|
|
87
|
+
// ⚠ `cancelled` is an ending of its own. It used to fall through to
|
|
88
|
+
// 'pending', so a job somebody cancelled sat in the operations centre as
|
|
89
|
+
// "Queued" for as long as the list still carried the row.
|
|
59
90
|
const status: PendingOp['status'] =
|
|
60
91
|
rawStatus === 'ok' || rawStatus === 'done'
|
|
61
92
|
? 'done'
|
|
62
93
|
: rawStatus === 'failed' || rawStatus === 'partial' || rawStatus === 'error'
|
|
63
94
|
? 'error'
|
|
64
|
-
: rawStatus === '
|
|
65
|
-
? '
|
|
66
|
-
: '
|
|
95
|
+
: rawStatus === 'cancelled'
|
|
96
|
+
? 'cancelled'
|
|
97
|
+
: rawStatus === 'running'
|
|
98
|
+
? 'running'
|
|
99
|
+
: 'pending';
|
|
67
100
|
const sources = Array.isArray(raw.sources) ? raw.sources : [];
|
|
101
|
+
// ⚠ Unknown kinds pass through. This used to fold everything that was not
|
|
102
|
+
// copy/move into `delete`, which drew a bin beside a plugin job and read
|
|
103
|
+
// "Deleted (1)" when it finished. `plugin-action` (the wire name) becomes
|
|
104
|
+
// `plugin`; a row with no kind at all is the one case still read as delete
|
|
105
|
+
// (the legacy delete endpoint's rows carried none).
|
|
68
106
|
const opType = str(raw.op_type, raw.kind) ?? 'delete';
|
|
107
|
+
const outputs = Array.isArray(raw.outputs)
|
|
108
|
+
? (raw.outputs as unknown[])
|
|
109
|
+
.map((o) => (o && typeof o === 'object' ? str((o as { path?: unknown }).path) : null))
|
|
110
|
+
.filter((p): p is string => p !== null)
|
|
111
|
+
.map((path) => ({ path }))
|
|
112
|
+
: undefined;
|
|
69
113
|
return {
|
|
70
114
|
id: num(raw.id),
|
|
71
|
-
op_type:
|
|
115
|
+
op_type: opType === 'plugin-action' ? 'plugin' : opType,
|
|
72
116
|
status,
|
|
73
117
|
progress_total: num(raw.progress_total, raw.total, sources.length),
|
|
74
118
|
progress_done: num(raw.progress_done, raw.done),
|
|
@@ -78,9 +122,16 @@ function normalizeOp(raw: Record<string, unknown>): PendingOp {
|
|
|
78
122
|
source_dir: str(raw.source_dir),
|
|
79
123
|
source_count: num(raw.source_count, sources.length),
|
|
80
124
|
error_message: str(raw.error_message, raw.error),
|
|
125
|
+
error_code: str(raw.error_code) ?? undefined,
|
|
126
|
+
error_engine: str(raw.error_engine) ?? undefined,
|
|
81
127
|
started_at: str(raw.started_at),
|
|
82
128
|
finished_at: str(raw.finished_at),
|
|
83
129
|
created_at: str(raw.created_at),
|
|
130
|
+
plugin: str(raw.plugin) ?? undefined,
|
|
131
|
+
action: str(raw.action) ?? undefined,
|
|
132
|
+
label: str(raw.label) ?? undefined,
|
|
133
|
+
message: str(raw.message) ?? undefined,
|
|
134
|
+
outputs,
|
|
84
135
|
};
|
|
85
136
|
}
|
|
86
137
|
|
|
@@ -133,7 +184,7 @@ export function usePendingOps(
|
|
|
133
184
|
const incoming = (res.ops || []).map(normalizeOp);
|
|
134
185
|
|
|
135
186
|
for (const op of incoming) {
|
|
136
|
-
if ((op.status === 'done' || op.status === 'error') && !announced.has(op.id)) {
|
|
187
|
+
if ((op.status === 'done' || op.status === 'error' || op.status === 'cancelled') && !announced.has(op.id)) {
|
|
137
188
|
announced.add(op.id);
|
|
138
189
|
settledAt.set(op.id, Date.now());
|
|
139
190
|
if (firstPollDone) {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePluginActions — the file-menu rows app plugins add, and how to run one.
|
|
3
|
+
*
|
|
4
|
+
* `GET /api/files/plugins/actions` is fetched once per endpoint URL and
|
|
5
|
+
* shared by every explorer instance on the page (the admin explorer, a split
|
|
6
|
+
* pane, an embedded web component all ask the same server), and re-asked
|
|
7
|
+
* after five minutes — the contract's client cache. Nothing is requested
|
|
8
|
+
* until `enabled()` says so: with `capabilities.app_plugins.enabled` false the
|
|
9
|
+
* explorer makes zero plugin calls.
|
|
10
|
+
*
|
|
11
|
+
* The composable holds no menu logic of its own: which rows a selection gets
|
|
12
|
+
* is `lib/pluginMenu`, the rule is `lib/pluginApplies`, both pure.
|
|
13
|
+
*/
|
|
14
|
+
import { computed, ref, watch, type Ref } from 'vue';
|
|
15
|
+
import type { FileApi } from './useFileApi';
|
|
16
|
+
import type { FileNode } from '../types/FileNode';
|
|
17
|
+
import type { PluginActionRow, PluginActionsResponse, PluginRunResult, PluginViewRow } from '../types/Plugins';
|
|
18
|
+
import { pluginActionsFor, pluginActionKey } from '../lib/pluginMenu';
|
|
19
|
+
|
|
20
|
+
export const PLUGIN_ACTIONS_TTL_MS = 5 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
interface CacheEntry {
|
|
23
|
+
at: number;
|
|
24
|
+
data: Ref<PluginActionsResponse>;
|
|
25
|
+
inflight: Promise<PluginActionsResponse> | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One entry per actions URL, shared across every instance on the page. */
|
|
29
|
+
const cache = new Map<string, CacheEntry>();
|
|
30
|
+
|
|
31
|
+
/** Forget every cached answer — tests, and a host that just installed a plugin. */
|
|
32
|
+
export function invalidatePluginActions(): void {
|
|
33
|
+
cache.clear();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function entryFor(url: string): CacheEntry {
|
|
37
|
+
let e = cache.get(url);
|
|
38
|
+
if (!e) {
|
|
39
|
+
e = { at: 0, data: ref<PluginActionsResponse>({ actions: [], views: [] }), inflight: null };
|
|
40
|
+
cache.set(url, e);
|
|
41
|
+
}
|
|
42
|
+
return e;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function usePluginActions(api: FileApi, enabled: () => boolean) {
|
|
46
|
+
const url = api.endpoints.pluginActions ?? '';
|
|
47
|
+
const entry = entryFor(url || '(none)');
|
|
48
|
+
const loaded = ref(false);
|
|
49
|
+
const error = ref<string | null>(null);
|
|
50
|
+
|
|
51
|
+
const isEnabled = computed(() => enabled() && url !== '');
|
|
52
|
+
|
|
53
|
+
const actions = computed<PluginActionRow[]>(() => (isEnabled.value ? entry.data.value.actions : []));
|
|
54
|
+
const views = computed<PluginViewRow[]>(() => (isEnabled.value ? entry.data.value.views : []));
|
|
55
|
+
|
|
56
|
+
/** Fetch (or reuse the shared answer) — a no-op while the feature is off. */
|
|
57
|
+
async function refresh(force = false): Promise<PluginActionsResponse> {
|
|
58
|
+
if (!isEnabled.value) return { actions: [], views: [] };
|
|
59
|
+
const fresh = entry.at > 0 && Date.now() - entry.at < PLUGIN_ACTIONS_TTL_MS;
|
|
60
|
+
if (fresh && !force) {
|
|
61
|
+
loaded.value = true;
|
|
62
|
+
return entry.data.value;
|
|
63
|
+
}
|
|
64
|
+
if (entry.inflight) return entry.inflight;
|
|
65
|
+
entry.inflight = api
|
|
66
|
+
.pluginActions()
|
|
67
|
+
.then((res) => {
|
|
68
|
+
entry.data.value = { actions: res.actions ?? [], views: res.views ?? [] };
|
|
69
|
+
entry.at = Date.now();
|
|
70
|
+
error.value = null;
|
|
71
|
+
return entry.data.value;
|
|
72
|
+
})
|
|
73
|
+
.catch((e: unknown) => {
|
|
74
|
+
error.value = String((e as Error)?.message ?? e);
|
|
75
|
+
return entry.data.value;
|
|
76
|
+
})
|
|
77
|
+
.finally(() => {
|
|
78
|
+
entry.inflight = null;
|
|
79
|
+
loaded.value = true;
|
|
80
|
+
});
|
|
81
|
+
return entry.inflight;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The first time the feature turns on (capabilities answered), ask once.
|
|
85
|
+
watch(
|
|
86
|
+
isEnabled,
|
|
87
|
+
(on) => {
|
|
88
|
+
if (on) void refresh();
|
|
89
|
+
},
|
|
90
|
+
{ immediate: true },
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
/** Action rows whose rule accepts this selection. */
|
|
94
|
+
function actionsFor(selection: FileNode[]): PluginActionRow[] {
|
|
95
|
+
return pluginActionsFor(actions.value, selection);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The action row behind a menu key, if any. */
|
|
99
|
+
function byKey(key: string): PluginActionRow | undefined {
|
|
100
|
+
return actions.value.find((a) => pluginActionKey(a) === key);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Run an action on wire paths. `{op}` when the server queued a job, or
|
|
105
|
+
* `{surface}` when the action opens a view first.
|
|
106
|
+
*/
|
|
107
|
+
function run(
|
|
108
|
+
action: PluginActionRow,
|
|
109
|
+
targets: FileNode[],
|
|
110
|
+
params?: Record<string, unknown>,
|
|
111
|
+
): Promise<PluginRunResult> {
|
|
112
|
+
return api.pluginActionRun(action.plugin, action.id, {
|
|
113
|
+
paths: targets.map((n) => n.path),
|
|
114
|
+
params,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { enabled: isEnabled, actions, views, loaded, error, refresh, actionsFor, byKey, run };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type PluginActionsStore = ReturnType<typeof usePluginActions>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePluginSurface — one plugin view's conversation with the server.
|
|
3
|
+
*
|
|
4
|
+
* A `modal` view (PluginViewModal) and an `inspector` section
|
|
5
|
+
* (PluginInspectorSection) draw the same kind of screen and post the same
|
|
6
|
+
* events; what differs is only the frame around it. So the conversation
|
|
7
|
+
* lives here, once: the current surface, the values its nodes hold, the
|
|
8
|
+
* debounced `change`, the footer press, the list row action, and what an
|
|
9
|
+
* answer does — a new screen, a toast, `done`, or `{op}` (the server queued
|
|
10
|
+
* a job; the caller registers the row and closes).
|
|
11
|
+
*
|
|
12
|
+
* Values: seeded from every fresh surface (`lib/surfaceValues.initialValues`),
|
|
13
|
+
* replaced as the components edit them, and sent whole on every event as
|
|
14
|
+
* `data.values`. After a `change` answer the person's current entries WIN
|
|
15
|
+
* over the echo — they may have typed more while the request was out.
|
|
16
|
+
*/
|
|
17
|
+
import { computed, getCurrentInstance, onBeforeUnmount, ref } from 'vue';
|
|
18
|
+
import type { FileApi } from './useFileApi';
|
|
19
|
+
import type {
|
|
20
|
+
PluginSurface,
|
|
21
|
+
PluginViewEventBody,
|
|
22
|
+
PluginViewEventData,
|
|
23
|
+
SurfaceAction,
|
|
24
|
+
SurfaceOpenRequest,
|
|
25
|
+
} from '../types/Plugins';
|
|
26
|
+
import { foreignText } from '../lib/direction';
|
|
27
|
+
import { isOpenRequest } from '../lib/surfaceOpen';
|
|
28
|
+
import { labelOf } from '../lib/pluginLabel';
|
|
29
|
+
import { initialValues } from '../lib/surfaceValues';
|
|
30
|
+
import { missingRequired, stripHiddenValues } from '../lib/surfaceConditions';
|
|
31
|
+
|
|
32
|
+
/** The contract's debounce for `form` edits → `event: "change"`. */
|
|
33
|
+
export const SURFACE_CHANGE_DEBOUNCE_MS = 300;
|
|
34
|
+
|
|
35
|
+
export interface PluginSurfaceHost {
|
|
36
|
+
api: Pick<FileApi, 'pluginViewEvent'>;
|
|
37
|
+
plugin: string;
|
|
38
|
+
view: string;
|
|
39
|
+
/** Adapter-qualified path the view was opened on — echoed on every event. */
|
|
40
|
+
path?: () => string | undefined;
|
|
41
|
+
locale: () => string;
|
|
42
|
+
/** The words for an answer without a surface. */
|
|
43
|
+
errorText: () => string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PluginSurfaceEvents {
|
|
47
|
+
/** The server enqueued a job: the raw ops row. */
|
|
48
|
+
onOp: (op: Record<string, unknown>) => void;
|
|
49
|
+
/** The conversation is over (`done`, or after `{op}`). */
|
|
50
|
+
onDone: () => void;
|
|
51
|
+
onToast: (message: string) => void;
|
|
52
|
+
/**
|
|
53
|
+
* v3 §3.0 — the answer says "go to this file" (`lib/surfaceOpen`).
|
|
54
|
+
*
|
|
55
|
+
* ⚠ Raised AFTER `onDone` when the answer also carries `done: true`: the
|
|
56
|
+
* screen that asked for the navigation is closed first, or the person
|
|
57
|
+
* arrives at the new one with the old one still over it.
|
|
58
|
+
*
|
|
59
|
+
* ⚠ A frame with nowhere to navigate (a public page has no explorer
|
|
60
|
+
* behind it) simply does not pass this in, and the request is dropped.
|
|
61
|
+
*/
|
|
62
|
+
onOpen?: (req: SurfaceOpenRequest) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function usePluginSurface(host: PluginSurfaceHost, events: PluginSurfaceEvents) {
|
|
66
|
+
const current = ref<PluginSurface | null>(null);
|
|
67
|
+
const values = ref<Record<string, unknown>>({});
|
|
68
|
+
const busy = ref(false);
|
|
69
|
+
const failure = ref('');
|
|
70
|
+
/**
|
|
71
|
+
* v3 §2.1 — the visible, required fields a blocked submit is waiting for.
|
|
72
|
+
*
|
|
73
|
+
* ⚠ Pointed at, not merely refused. A submit that does nothing and says
|
|
74
|
+
* nothing is the worst of the three possible behaviours; this list is what
|
|
75
|
+
* puts the star and the "required" line on the fields themselves.
|
|
76
|
+
*/
|
|
77
|
+
const invalidKeys = ref<string[]>([]);
|
|
78
|
+
|
|
79
|
+
const footer = computed<SurfaceAction[]>(() => current.value?.actions ?? []);
|
|
80
|
+
const errors = computed(() => current.value?.errors);
|
|
81
|
+
|
|
82
|
+
/** A new screen. `keepValues` overlays what the person already entered. */
|
|
83
|
+
function setSurface(s: PluginSurface | null, keepValues = false): void {
|
|
84
|
+
current.value = s;
|
|
85
|
+
failure.value = '';
|
|
86
|
+
invalidKeys.value = [];
|
|
87
|
+
const seeded = initialValues(s?.nodes);
|
|
88
|
+
values.value = keepValues ? { ...seeded, ...values.value } : seeded;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function updateValues(v: Record<string, unknown>): void {
|
|
92
|
+
values.value = v;
|
|
93
|
+
// Answering one of them clears its mark as it is typed, rather than on
|
|
94
|
+
// the next press: a field that has been filled must not stay red.
|
|
95
|
+
if (invalidKeys.value.length) {
|
|
96
|
+
const still = missingRequired(current.value?.nodes, v).map((f) => f.key);
|
|
97
|
+
if (still.length !== invalidKeys.value.length) invalidKeys.value = still;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let changeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
102
|
+
function cancelChange(): void {
|
|
103
|
+
if (changeTimer !== null) clearTimeout(changeTimer);
|
|
104
|
+
changeTimer = null;
|
|
105
|
+
}
|
|
106
|
+
/** A form edit: one `change` event 300 ms after the last keystroke. */
|
|
107
|
+
function scheduleChange(): void {
|
|
108
|
+
cancelChange();
|
|
109
|
+
changeTimer = setTimeout(() => {
|
|
110
|
+
changeTimer = null;
|
|
111
|
+
void post('change');
|
|
112
|
+
}, SURFACE_CHANGE_DEBOUNCE_MS);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let seq = 0;
|
|
116
|
+
|
|
117
|
+
async function post(
|
|
118
|
+
event: PluginViewEventBody['event'],
|
|
119
|
+
actionId?: string,
|
|
120
|
+
extra: Omit<PluginViewEventData, 'values'> = {},
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
if (!current.value) return;
|
|
123
|
+
const blocking = event !== 'change';
|
|
124
|
+
if (blocking) {
|
|
125
|
+
cancelChange();
|
|
126
|
+
if (busy.value) return;
|
|
127
|
+
busy.value = true;
|
|
128
|
+
}
|
|
129
|
+
const mine = ++seq;
|
|
130
|
+
failure.value = '';
|
|
131
|
+
try {
|
|
132
|
+
const res = await host.api.pluginViewEvent(host.plugin, host.view, {
|
|
133
|
+
path: host.path?.(),
|
|
134
|
+
state: current.value?.state ?? {},
|
|
135
|
+
event,
|
|
136
|
+
action_id: actionId,
|
|
137
|
+
// ⚠⚠ The values a HIDDEN field holds are dropped here, not blanked
|
|
138
|
+
// (v3 §2.1: "a value belonging to a hidden field is dropped before
|
|
139
|
+
// the job runs — it cannot arrive as a surprise"). The plugin is
|
|
140
|
+
// re-asked the same question by the host at submit; this is the
|
|
141
|
+
// client half of the same promise, and it also keeps a `change`
|
|
142
|
+
// event from echoing back a field the step no longer shows.
|
|
143
|
+
data: { values: stripHiddenValues(current.value?.nodes, values.value), ...extra },
|
|
144
|
+
});
|
|
145
|
+
// A `change` answer that arrives after a later event started is stale:
|
|
146
|
+
// the later event's answer is the screen that counts.
|
|
147
|
+
if (!blocking && mine !== seq) return;
|
|
148
|
+
if (res.op) {
|
|
149
|
+
events.onOp(res.op);
|
|
150
|
+
events.onDone();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const next = res.surface;
|
|
154
|
+
if (!next) {
|
|
155
|
+
failure.value = host.errorText();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const toast = labelOf(next.toast, host.locale());
|
|
159
|
+
if (toast) events.onToast(toast);
|
|
160
|
+
// ⚠ The order is the contract's: `done` closes the screen, and only
|
|
161
|
+
// then does the navigation happen. Reversed, the person lands on the
|
|
162
|
+
// new screen with the finished one still drawn over it.
|
|
163
|
+
const go = isOpenRequest(next.open) ? next.open : null;
|
|
164
|
+
if (next.done) {
|
|
165
|
+
events.onDone();
|
|
166
|
+
if (go) events.onOpen?.(go);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setSurface(next, !blocking);
|
|
170
|
+
if (go) events.onOpen?.(go);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
if (!blocking && mine !== seq) return;
|
|
173
|
+
// ⚠ An app's own error text never met the catalogue — isolate it.
|
|
174
|
+
failure.value = foreignText(host.locale(), String((e as Error)?.message ?? e)) || host.errorText();
|
|
175
|
+
} finally {
|
|
176
|
+
if (blocking) busy.value = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A footer button: `submit` when it is the primary one, `action` otherwise.
|
|
182
|
+
*
|
|
183
|
+
* ⚠ A submit is refused locally while a visible, required field is empty.
|
|
184
|
+
* The server refuses it too — that is the boundary — but a round trip to
|
|
185
|
+
* be told "this is required" about a box that is on the screen is a round
|
|
186
|
+
* trip the person should not have to make.
|
|
187
|
+
*/
|
|
188
|
+
function press(a: SurfaceAction): Promise<void> {
|
|
189
|
+
if (busy.value || a.disabled) return Promise.resolve();
|
|
190
|
+
if (a.primary) {
|
|
191
|
+
const missing = missingRequired(current.value?.nodes, values.value);
|
|
192
|
+
invalidKeys.value = missing.map((f) => f.key);
|
|
193
|
+
if (missing.length) return Promise.resolve();
|
|
194
|
+
}
|
|
195
|
+
return post(a.primary ? 'submit' : 'action', a.id);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A `list` row's button. */
|
|
199
|
+
function rowAction(p: { action_id: string; row_id: string }): Promise<void> {
|
|
200
|
+
return post('action', p.action_id, { row_id: p.row_id });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (getCurrentInstance()) onBeforeUnmount(cancelChange);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
current,
|
|
207
|
+
values,
|
|
208
|
+
busy,
|
|
209
|
+
failure,
|
|
210
|
+
footer,
|
|
211
|
+
errors,
|
|
212
|
+
invalidKeys,
|
|
213
|
+
setSurface,
|
|
214
|
+
updateValues,
|
|
215
|
+
scheduleChange,
|
|
216
|
+
post,
|
|
217
|
+
press,
|
|
218
|
+
rowAction,
|
|
219
|
+
dispose: cancelChange,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type PluginSurfaceStore = ReturnType<typeof usePluginSurface>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePublicBranding — whose product the visitor thinks they are looking at.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ v3 §1.2: whitelabel means whitelabel. A share, a file request and a
|
|
5
|
+
* signature request are the surfaces an OUTSIDER sees, and until now they
|
|
6
|
+
* were the only ones that ignored the instance's own name, logo and colour
|
|
7
|
+
* — so an operator who had renamed filex everywhere still sent out links
|
|
8
|
+
* that said filex. The branding record is the same one the admin
|
|
9
|
+
* "Corporate identity" page writes; this composable is how the public shell
|
|
10
|
+
* wears it.
|
|
11
|
+
*
|
|
12
|
+
* ⚠ It is a PUBLIC fetch, and the page must be usable before it lands: the
|
|
13
|
+
* shell paints with the stock palette and re-paints when the answer arrives.
|
|
14
|
+
* Nothing waits on it and a failure is not an error — an older server has no
|
|
15
|
+
* such route, and the product's own name is a perfectly good answer.
|
|
16
|
+
*
|
|
17
|
+
* ⚠ The accent is written as `--fe-*` custom properties on the SHELL's root
|
|
18
|
+
* element, never on `:root`. The same shell is mounted inside other pages
|
|
19
|
+
* (an embed, a test harness), and a public link must not repaint the host
|
|
20
|
+
* around it.
|
|
21
|
+
*/
|
|
22
|
+
import { computed, ref } from 'vue';
|
|
23
|
+
import type { PublicBranding } from '../types/Public';
|
|
24
|
+
import { setLocalesFromBranding } from '../lib/uiLocales';
|
|
25
|
+
|
|
26
|
+
/** The product's own name, used until (and unless) an operator renames it. */
|
|
27
|
+
export const DEFAULT_BRAND_NAME = 'filex';
|
|
28
|
+
|
|
29
|
+
export interface PublicBrandingOptions {
|
|
30
|
+
/** API origin; empty = same origin. */
|
|
31
|
+
base?: string;
|
|
32
|
+
fetchImpl?: typeof fetch;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
36
|
+
|
|
37
|
+
/** `#abc` → `#aabbcc`; anything that is not a hex colour → ''. */
|
|
38
|
+
export function normalizeAccent(raw: string | undefined | null): string {
|
|
39
|
+
const v = String(raw ?? '').trim();
|
|
40
|
+
if (!HEX.test(v)) return '';
|
|
41
|
+
if (v.length === 4) return `#${v[1]}${v[1]}${v[2]}${v[2]}${v[3]}${v[3]}`.toLowerCase();
|
|
42
|
+
return v.toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The same colour, `k` of the way to black (0.85 = the hover state). */
|
|
46
|
+
export function shade(hex: string, k: number): string {
|
|
47
|
+
const v = normalizeAccent(hex);
|
|
48
|
+
if (!v) return '';
|
|
49
|
+
const n = parseInt(v.slice(1), 16);
|
|
50
|
+
const part = (shift: number) => Math.max(0, Math.min(255, Math.round(((n >> shift) & 0xff) * k)));
|
|
51
|
+
const hx = (x: number) => x.toString(16).padStart(2, '0');
|
|
52
|
+
return `#${hx(part(16))}${hx(part(8))}${hx(part(0))}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Black or white on this colour, by luminance.
|
|
57
|
+
*
|
|
58
|
+
* ⚠ Not a constant. An operator who picks a pale accent gets white text on
|
|
59
|
+
* pale yellow if the ink is hardcoded — which is what "unreadable button"
|
|
60
|
+
* looks like to the person reporting it, and it is only ever seen on
|
|
61
|
+
* somebody else's instance.
|
|
62
|
+
*/
|
|
63
|
+
export function inkOn(hex: string): string {
|
|
64
|
+
const v = normalizeAccent(hex);
|
|
65
|
+
if (!v) return '';
|
|
66
|
+
const n = parseInt(v.slice(1), 16);
|
|
67
|
+
const lin = (c: number) => {
|
|
68
|
+
const x = c / 255;
|
|
69
|
+
return x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
|
|
70
|
+
};
|
|
71
|
+
const L = 0.2126 * lin((n >> 16) & 0xff) + 0.7152 * lin((n >> 8) & 0xff) + 0.0722 * lin(n & 0xff);
|
|
72
|
+
return L > 0.42 ? '#0b0f14' : '#ffffff';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The `--fe-*` properties an accent colour becomes on the public shell — the
|
|
77
|
+
* ONE derivation, used by the real page (usePublicBranding) and by the admin
|
|
78
|
+
* page's live preview (PublicLinkPreview), so the preview cannot paint a
|
|
79
|
+
* button the page itself would not. `{}` for no (or no valid) accent.
|
|
80
|
+
*/
|
|
81
|
+
export function accentStyleOf(raw: string | undefined | null): Record<string, string> {
|
|
82
|
+
const accent = normalizeAccent(raw);
|
|
83
|
+
if (!accent) return {} as Record<string, string>;
|
|
84
|
+
const n = parseInt(accent.slice(1), 16);
|
|
85
|
+
const rgb = `${(n >> 16) & 0xff}, ${(n >> 8) & 0xff}, ${n & 0xff}`;
|
|
86
|
+
return {
|
|
87
|
+
'--fe-primary': accent,
|
|
88
|
+
'--fe-primary-hover': shade(accent, 0.85),
|
|
89
|
+
'--fe-text-on-primary': inkOn(accent),
|
|
90
|
+
// ⚠ The TINT and the ink on it, or the accent only half arrives: the
|
|
91
|
+
// button turned orange while the round badge behind the padlock, the
|
|
92
|
+
// focus ring on the PIN box and the row hover stayed the stock blue
|
|
93
|
+
// (measured on the first after-shot of the restored gate). The old
|
|
94
|
+
// Go-rendered page derived exactly these from the same colour
|
|
95
|
+
// (`--px-accent-soft: rgba(r,g,b,0.14)`, `color: var(--px-accent)`), and
|
|
96
|
+
// a translucent tint is right rather than a solid: it composites onto
|
|
97
|
+
// the card in light and in dark without a second value.
|
|
98
|
+
'--fe-primary-soft': `rgba(${rgb}, 0.14)`,
|
|
99
|
+
'--fe-primary-ink': accent,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function usePublicBranding(opts: PublicBrandingOptions = {}) {
|
|
104
|
+
const branding = ref<PublicBranding | null>(null);
|
|
105
|
+
const loaded = ref(false);
|
|
106
|
+
|
|
107
|
+
const name = computed(() => branding.value?.name?.trim() || DEFAULT_BRAND_NAME);
|
|
108
|
+
/** True when the operator has named the instance something of their own. */
|
|
109
|
+
const whitelabel = computed(() => !!branding.value?.name?.trim() && name.value !== DEFAULT_BRAND_NAME);
|
|
110
|
+
const logo = computed(() => branding.value?.logo_url?.trim() || '');
|
|
111
|
+
const footerText = computed(() => branding.value?.footer_text?.trim() || '');
|
|
112
|
+
const hidePoweredBy = computed(() => branding.value?.hide_powered_by === true);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The instance's light/dark default for a visitor who has chosen nothing.
|
|
116
|
+
*
|
|
117
|
+
* ⚠ The server's word is `system`; this package's is `auto`. They mean
|
|
118
|
+
* the same thing and the translation happens HERE, once, rather than in
|
|
119
|
+
* every component that compares the string.
|
|
120
|
+
*/
|
|
121
|
+
const themeDefault = computed<'light' | 'dark' | 'auto'>(() => {
|
|
122
|
+
const t = branding.value?.theme;
|
|
123
|
+
return t === 'light' || t === 'dark' ? t : 'auto';
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/** The languages this instance offers, as the branding answer lists them. */
|
|
127
|
+
const locales = computed<string[]>(() => branding.value?.locales ?? []);
|
|
128
|
+
|
|
129
|
+
/** The accent, as the `--fe-*` properties the shell's root carries. */
|
|
130
|
+
const accentStyle = computed<Record<string, string>>(() => accentStyleOf(branding.value?.accent));
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Ask the server.
|
|
134
|
+
*
|
|
135
|
+
* ⚠ `/api/public/branding` first, `/api/branding` second. The second is
|
|
136
|
+
* the route this product has served since wiring:e1 and answers a
|
|
137
|
+
* compatible record; asking only for the new address and giving up on a
|
|
138
|
+
* 404 would mean a whitelabelled instance goes back to saying "filex" the
|
|
139
|
+
* moment it is updated in the wrong order.
|
|
140
|
+
*
|
|
141
|
+
* ⚠ The languages ride along: `locales` / `ui_locales` are on this payload,
|
|
142
|
+
* so the picker is filled by the fetch the shell already makes
|
|
143
|
+
* (`lib/uiLocales.setLocalesFromBranding`). Only the language somebody
|
|
144
|
+
* actually reads has its strings fetched, once.
|
|
145
|
+
*/
|
|
146
|
+
async function load(): Promise<PublicBranding | null> {
|
|
147
|
+
const doFetch = opts.fetchImpl ?? (typeof fetch === 'function' ? fetch : null);
|
|
148
|
+
if (!doFetch) return null;
|
|
149
|
+
const origin = (opts.base ?? '').replace(/\/+$/, '');
|
|
150
|
+
for (const path of ['/api/public/branding', '/api/branding']) {
|
|
151
|
+
try {
|
|
152
|
+
const res = await doFetch(`${origin}${path}`, {
|
|
153
|
+
method: 'GET',
|
|
154
|
+
headers: { Accept: 'application/json' },
|
|
155
|
+
credentials: 'same-origin',
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) continue;
|
|
158
|
+
branding.value = (await res.json()) as PublicBranding;
|
|
159
|
+
loaded.value = true;
|
|
160
|
+
// ⚠ With the same origin and fetch: a language's STRINGS are fetched
|
|
161
|
+
// later from `/api/public/ui-locales/{code}` on this same server.
|
|
162
|
+
setLocalesFromBranding(branding.value, { base: opts.base, fetchImpl: opts.fetchImpl });
|
|
163
|
+
return branding.value;
|
|
164
|
+
} catch {
|
|
165
|
+
/* try the next address; the default name is already on screen */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { branding, loaded, name, whitelabel, logo, footerText, hidePoweredBy, themeDefault, locales, accentStyle, load };
|
|
172
|
+
}
|