@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.
Files changed (233) hide show
  1. package/README.md +14 -2
  2. package/dist/ArchiveViewer-BqU761Wk.js +149 -0
  3. package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
  4. package/dist/CsvViewer-ByHVK5ga.js +172 -0
  5. package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
  6. package/dist/{DrawioViewer-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
  7. package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
  8. package/dist/{EpubViewer-DS3mb3k8.js → EpubViewer-DJwUKlHW.js} +50 -50
  9. package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
  10. package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
  11. package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
  12. package/dist/MermaidViewer-CEDTD_pp.js +136 -0
  13. package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
  14. package/dist/PsdViewer-CZcxUpZO.js +121 -0
  15. package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
  16. package/dist/TiffViewer-BwI62CbQ.js +141 -0
  17. package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
  18. package/dist/{Viewer3D-CIrOn63I.js → Viewer3D-DoeZcuMm.js} +16 -16
  19. package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
  20. package/dist/filex-core.js +475 -219
  21. package/dist/filex-core.umd.cjs +165 -108
  22. package/dist/filex-core.umd.cjs.map +1 -1
  23. package/dist/fonts/caveat-latin-ext.woff2 +0 -0
  24. package/dist/fonts/caveat-latin.woff2 +0 -0
  25. package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
  26. package/dist/fonts/dancing-script-latin.woff2 +0 -0
  27. package/dist/fonts/homemade-apple-latin.woff2 +0 -0
  28. package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
  29. package/dist/fonts/source-serif-4-latin.woff2 +0 -0
  30. package/dist/index-DuWnGuv5.js +35901 -0
  31. package/dist/index-DuWnGuv5.js.map +1 -0
  32. package/dist/index.d.ts +5241 -553
  33. package/dist/pdf-CaPYsOUo.js +21302 -0
  34. package/dist/pdf-CaPYsOUo.js.map +1 -0
  35. package/dist/style.css +1 -1
  36. package/package.json +1 -1
  37. package/src/FileExplorer.vue +1207 -343
  38. package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
  39. package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
  40. package/src/assets/fonts/README.md +58 -0
  41. package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
  42. package/src/assets/fonts/caveat-latin.woff2 +0 -0
  43. package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
  44. package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
  45. package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
  46. package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
  47. package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
  48. package/src/components/AdvancedSearch.vue +49 -0
  49. package/src/components/Breadcrumb.vue +5 -5
  50. package/src/components/ChoiceButtons.vue +152 -0
  51. package/src/components/CommandPalette.vue +5 -5
  52. package/src/components/ConnectionGuideView.vue +1 -1
  53. package/src/components/ConnectionNotice.vue +59 -0
  54. package/src/components/ConnectionsPanel.vue +30 -568
  55. package/src/components/ContextMenu.vue +31 -5
  56. package/src/components/DataTable.vue +1331 -0
  57. package/src/components/FilePane.vue +3 -0
  58. package/src/components/FilterBar.vue +12 -5
  59. package/src/components/GalleryView.vue +50 -2
  60. package/src/components/GridView.vue +51 -3
  61. package/src/components/HomeView.vue +18 -6
  62. package/src/components/InspectorPanel.vue +134 -17
  63. package/src/components/ListView.vue +366 -900
  64. package/src/components/LogoMark.vue +57 -0
  65. package/src/components/NFSExportsPanel.vue +76 -28
  66. package/src/components/OnboardingTour.vue +19 -7
  67. package/src/components/OperationsCenter.vue +77 -1
  68. package/src/components/PendingOpsTray.vue +59 -5
  69. package/src/components/PresenceBar.vue +7 -7
  70. package/src/components/ProductVersion.vue +31 -0
  71. package/src/components/QuickLook.vue +10 -14
  72. package/src/components/RecentlyOpened.vue +47 -17
  73. package/src/components/RecoveryKeyModal.vue +1 -1
  74. package/src/components/RowActions.vue +134 -0
  75. package/src/components/S3KeysPanel.vue +98 -56
  76. package/src/components/SSHKeysPanel.vue +79 -26
  77. package/src/components/SideNav.vue +260 -96
  78. package/src/components/StarButton.vue +1 -0
  79. package/src/components/StorageFields.vue +74 -6
  80. package/src/components/StorageTags.vue +62 -0
  81. package/src/components/TabBar.vue +1 -1
  82. package/src/components/TagKindIcon.vue +56 -0
  83. package/src/components/TagPicker.vue +226 -40
  84. package/src/components/ThemePalette.vue +4 -4
  85. package/src/components/TimeZonePicker.vue +29 -10
  86. package/src/components/TokensPanel.vue +82 -27
  87. package/src/components/Toolbar.vue +28 -6
  88. package/src/components/plugin/PluginConfirmModal.vue +54 -0
  89. package/src/components/plugin/PluginInspectorSection.vue +149 -0
  90. package/src/components/plugin/PluginPageView.vue +339 -0
  91. package/src/components/plugin/PluginViewModal.vue +159 -0
  92. package/src/components/plugin/SurfaceConversation.vue +76 -0
  93. package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
  94. package/src/components/plugin/SurfaceRenderer.vue +299 -0
  95. package/src/components/plugin/SurfaceSections.vue +91 -0
  96. package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
  97. package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
  98. package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
  99. package/src/components/plugin/nodes/SurfaceList.vue +127 -0
  100. package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
  101. package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
  102. package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
  103. package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
  104. package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
  105. package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
  106. package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
  107. package/src/components/public/PublicLanguagePicker.vue +58 -0
  108. package/src/components/public/PublicLinkPage.vue +286 -0
  109. package/src/components/public/PublicLinkPreview.vue +85 -0
  110. package/src/components/public/PublicPinGate.vue +108 -0
  111. package/src/components/public/PublicRequestBody.vue +204 -0
  112. package/src/components/public/PublicShareBody.vue +188 -0
  113. package/src/components/public/PublicShell.vue +332 -0
  114. package/src/composables/useConnections.ts +8 -2
  115. package/src/composables/useFileApi.ts +183 -44
  116. package/src/composables/useLocale.ts +277 -41
  117. package/src/composables/useNFSExports.ts +8 -10
  118. package/src/composables/useOperations.ts +44 -3
  119. package/src/composables/usePendingOps.ts +59 -8
  120. package/src/composables/usePluginActions.ts +121 -0
  121. package/src/composables/usePluginSurface.ts +223 -0
  122. package/src/composables/usePublicBranding.ts +172 -0
  123. package/src/composables/usePublicLink.ts +573 -0
  124. package/src/composables/usePublicPage.ts +61 -0
  125. package/src/composables/useS3Keys.ts +17 -12
  126. package/src/composables/useSSHKeys.ts +8 -10
  127. package/src/composables/useTokens.ts +8 -12
  128. package/src/composables/useUploadChunked.ts +18 -8
  129. package/src/composables/useViewerFetch.ts +6 -3
  130. package/src/index.ts +404 -8
  131. package/src/lib/actionIcons.ts +46 -1
  132. package/src/lib/advSearch.ts +14 -7
  133. package/src/lib/appLock.ts +130 -0
  134. package/src/lib/connection.ts +110 -0
  135. package/src/lib/destinationTree.ts +19 -3
  136. package/src/lib/direction.ts +353 -0
  137. package/src/lib/dragGhost.ts +6 -0
  138. package/src/lib/dropLimits.ts +69 -0
  139. package/src/lib/errorWords.ts +394 -0
  140. package/src/lib/externalReach.ts +5 -1
  141. package/src/lib/fileFilters.ts +7 -2
  142. package/src/lib/fileIcons.ts +4 -0
  143. package/src/lib/internalPaths.ts +77 -0
  144. package/src/lib/listInput.ts +64 -0
  145. package/src/lib/listing.ts +58 -23
  146. package/src/lib/modalStack.ts +26 -0
  147. package/src/lib/nodeRow.ts +121 -0
  148. package/src/lib/pdfFieldRules.ts +126 -0
  149. package/src/lib/pdfFields.ts +589 -0
  150. package/src/lib/pdfFieldsGeom.ts +203 -0
  151. package/src/lib/pdfjsLoader.ts +51 -0
  152. package/src/lib/personName.ts +47 -0
  153. package/src/lib/pluginApplies.ts +123 -0
  154. package/src/lib/pluginLabel.ts +72 -0
  155. package/src/lib/pluginMenu.ts +253 -0
  156. package/src/lib/pluginPage.ts +58 -0
  157. package/src/lib/plural.ts +181 -0
  158. package/src/lib/popupLayer.ts +103 -0
  159. package/src/lib/prefs.ts +563 -0
  160. package/src/lib/productVersion.ts +35 -0
  161. package/src/lib/publicLayout.ts +39 -0
  162. package/src/lib/serviceGate.ts +104 -0
  163. package/src/lib/shareTtl.ts +25 -11
  164. package/src/lib/signFonts.ts +85 -0
  165. package/src/lib/signaturePad.ts +111 -0
  166. package/src/lib/sortOrder.ts +100 -38
  167. package/src/lib/star.ts +7 -1
  168. package/src/lib/surfaceCell.ts +41 -0
  169. package/src/lib/surfaceConditions.ts +156 -0
  170. package/src/lib/surfaceOpen.ts +106 -0
  171. package/src/lib/surfaceSteps.ts +68 -0
  172. package/src/lib/surfaceValues.ts +191 -0
  173. package/src/lib/symlink.ts +150 -0
  174. package/src/lib/tableColumns.ts +493 -0
  175. package/src/lib/tableEnv.ts +34 -0
  176. package/src/lib/tablePrefs.ts +84 -0
  177. package/src/lib/tags.ts +121 -12
  178. package/src/lib/themes.ts +286 -18
  179. package/src/lib/tour.ts +152 -0
  180. package/src/lib/trashEmpty.ts +111 -0
  181. package/src/lib/trashTimeLeft.ts +23 -0
  182. package/src/lib/uiLocales.ts +460 -0
  183. package/src/lib/viewPrefs.ts +696 -660
  184. package/src/lib/viewPrefsHttp.ts +78 -0
  185. package/src/locales/en.ts +522 -119
  186. package/src/locales/resolve.ts +17 -8
  187. package/src/locales/tr.ts +539 -155
  188. package/src/modals/ConvertModal.vue +62 -11
  189. package/src/modals/DestinationPickerModal.vue +59 -16
  190. package/src/modals/Modal.vue +113 -23
  191. package/src/modals/NewDocumentModal.vue +16 -6
  192. package/src/modals/NewFolderModal.vue +8 -0
  193. package/src/modals/PermissionsModal.vue +272 -163
  194. package/src/modals/PreviewModal.vue +119 -20
  195. package/src/modals/RenameModal.vue +37 -1
  196. package/src/styles/base.css +2895 -241
  197. package/src/styles/sign-fonts.css +118 -0
  198. package/src/styles/variables.css +60 -0
  199. package/src/types/Connections.ts +17 -0
  200. package/src/types/ExplorerConfig.ts +117 -1
  201. package/src/types/FileNode.ts +58 -0
  202. package/src/types/Plugins.ts +494 -0
  203. package/src/types/Public.ts +212 -0
  204. package/src/viewers/ArchiveViewer.vue +93 -48
  205. package/src/viewers/CsvViewer.vue +99 -53
  206. package/src/viewers/DrawioViewer.vue +35 -9
  207. package/src/viewers/EpubViewer.vue +7 -8
  208. package/src/viewers/IpynbViewer.vue +3 -1
  209. package/src/viewers/MermaidViewer.vue +4 -5
  210. package/src/viewers/PdfViewer.vue +8 -17
  211. package/src/viewers/PsdViewer.vue +2 -4
  212. package/src/viewers/TiffViewer.vue +4 -5
  213. package/src/viewers/Viewer3D.vue +1 -1
  214. package/LICENSE +0 -21
  215. package/dist/ArchiveViewer-CLNevfPE.js +0 -124
  216. package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
  217. package/dist/CsvViewer-K8XA3z7N.js +0 -141
  218. package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
  219. package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
  220. package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
  221. package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
  222. package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
  223. package/dist/MermaidViewer-DqucH_io.js +0 -137
  224. package/dist/MermaidViewer-DqucH_io.js.map +0 -1
  225. package/dist/PsdViewer-CwiVENmq.js +0 -122
  226. package/dist/PsdViewer-CwiVENmq.js.map +0 -1
  227. package/dist/TiffViewer-CpewSVmQ.js +0 -142
  228. package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
  229. package/dist/Viewer3D-CIrOn63I.js.map +0 -1
  230. package/dist/index-Dx0IbBcI.js +0 -26831
  231. package/dist/index-Dx0IbBcI.js.map +0 -1
  232. package/dist/useViewerFetch-czqbd2Lj.js +0 -25
  233. package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
@@ -1,30 +1,31 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * ConnectionsPanel — storage connections, and how to connect to them.
3
+ * ConnectionsPanel — how to connect your computer to this server.
4
4
  *
5
5
  * ⚠⚠ This component is the reason the feature exists ONCE. The desktop
6
6
  * app, the web app and any embed mount this same file; none of them owns a
7
- * hand-written copy of the form, the list or the instructions. A fix here
7
+ * hand-written copy of the credential panels or the instructions. A fix here
8
8
  * lands everywhere on the next release of the package, which is precisely
9
9
  * the standing rule ("never write surface-specific behaviour") applied to a
10
10
  * feature that was asked for on three surfaces at once.
11
11
  *
12
- * Two halves, because they are two different questions:
12
+ * ONE question, answered for the protocol you pick: "connect my computer to
13
+ * filex". Generated from the live deployment — the real host, the storage
14
+ * name, the caller's own username — with the credential each protocol needs
15
+ * minted right above the commands that use it, and a copy button.
13
16
  *
14
- * • INWARD — "connect filex to a bucket / a share / a server". Rendered
15
- * entirely from the backend's driver descriptors, so a driver added on
16
- * the server needs no frontend release and no surface can drift.
17
- * • OUTWARD — "connect my computer to filex". Generated from the live
18
- * deployment: the real host, the storage name, the caller's own
19
- * username, with a copy button.
20
- *
21
- * A user who may not manage storages is told so plainly and still gets the
22
- * outward half — which is the half they actually need. Rendering a form
23
- * whose every submit 403s would be the dishonest alternative.
17
+ * ⚠ There is NO storage form here, and there has not been one since v0.43.0.
18
+ * The owner, testing the release: "nasıl bağlanılır kısmında ve adminde
19
+ * bağlantılar sayfası … ikisinde de depolar gözüküyor bu depolar sekmesine
20
+ * hiç ihtiyaç yok. kaldıralım." Managing storages is a console job and it
21
+ * already had a console: Admin → Storages (create, edit, delete, plus sync
22
+ * mode, RBAC, drift — everything this panel's half never had). Browsing the
23
+ * storages you can see is the explorer's navigation panel. A second, poorer
24
+ * copy of the first in the screen that answers the OTHER question was the
25
+ * whole complaint; do not bring it back, extend Admin → Storages instead.
24
26
  */
25
27
  import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
26
28
  import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
27
- import type { StorageRow } from '../types/Connections';
28
29
  import { useLocale } from '../composables/useLocale';
29
30
  import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
30
31
  import { useConnections, connectionsOrigin } from '../composables/useConnections';
@@ -35,7 +36,6 @@ import {
35
36
  hostOf,
36
37
  type ProtocolGuide,
37
38
  } from '../lib/connectionGuides';
38
- import StorageFields from './StorageFields.vue';
39
39
  import ConnectionGuideView from './ConnectionGuideView.vue';
40
40
  import S3KeysPanel from './S3KeysPanel.vue';
41
41
  import SSHKeysPanel from './SSHKeysPanel.vue';
@@ -45,15 +45,16 @@ import { resolveLocale } from '../locales/resolve';
45
45
 
46
46
  const props = defineProps<{
47
47
  config: ExplorerConfig;
48
- /** Which half to open on. */
49
- initialTab?: 'storages' | 'connect';
50
48
  /** Draw a close control — the desktop app opens this as a full surface
51
49
  * and needs a way out; a page-embedded copy has the page's own chrome. */
52
50
  closable?: boolean;
53
51
  }>();
54
52
 
53
+ /* ⚠ No `changed`. It existed for one reason — a storage added or removed in
54
+ this panel had to reach the host's own list — and this panel no longer
55
+ adds or removes one. An event that can never fire is worse than none: every
56
+ host wires a handler for it and then trusts a refresh that never comes. */
55
57
  const emit = defineEmits<{
56
- (e: 'changed'): void;
57
58
  (e: 'close'): void;
58
59
  (e: 'error', err: { message: string }): void;
59
60
  }>();
@@ -64,27 +65,7 @@ const { t } = useLocale(locale);
64
65
  // Destructured on purpose: Vue only auto-unwraps refs that are top-level in
65
66
  // the setup scope, so `conn.storages` inside a template would render a Ref
66
67
  // object rather than its value.
67
- const {
68
- drivers,
69
- storages,
70
- visible,
71
- me,
72
- publicUrl,
73
- loading,
74
- loaded,
75
- error,
76
- canManage,
77
- denial,
78
- load,
79
- createStorage,
80
- updateStorage,
81
- deleteStorage,
82
- testStorage,
83
- descriptor,
84
- fields,
85
- defaults,
86
- missingRequired,
87
- } = useConnections(props.config);
68
+ const { visible, me, publicUrl, error, load } = useConnections(props.config);
88
69
 
89
70
  // ── theme ────────────────────────────────────────────────────────────
90
71
  // Resolved in JS rather than left to `prefers-color-scheme`, because the
@@ -104,177 +85,15 @@ const themeResolved = computed(() => {
104
85
  return osDark.value ? 'dark' : 'light';
105
86
  });
106
87
 
107
- // ── tabs ─────────────────────────────────────────────────────────────
108
- const tab = ref<'storages' | 'connect'>(props.initialTab ?? 'storages');
109
-
110
- // ── inward: the storage form ─────────────────────────────────────────
111
- type FormMode = { kind: 'none' } | { kind: 'new' } | { kind: 'edit'; row: StorageRow };
112
- const form = ref<FormMode>({ kind: 'none' });
113
- const fName = ref('');
114
- const fDriver = ref('');
115
- const fReadOnly = ref(false);
116
- const fEnabled = ref(true);
117
- const fConfig = ref<Record<string, unknown>>({});
118
- const fInvalid = ref<string[]>([]);
119
- const saving = ref(false);
120
- const testing = ref(false);
121
- const testResult = ref<{ ok: boolean; error?: string; object_count?: number } | null>(null);
88
+ // ── outward: the guides ──────────────────────────────────────────────
89
+ const protocols = guideProtocols();
90
+ const protocol = ref(protocols[0] ?? 'webdav');
91
+ const guideStorage = ref<string>('');
122
92
 
123
93
  /** What the NFS panel published: where to mount, and the path just minted. */
124
94
  const nfs = ref<{ host: string; port: number; enabled: boolean; path?: string; readOnly: boolean } | null>(
125
95
  null,
126
96
  );
127
- const formError = ref<string | null>(null);
128
- const confirmDelete = ref<number | null>(null);
129
-
130
- const driverOptions = computed(() =>
131
- drivers.value.map((d) => {
132
- const translated = t(d.i18n_key);
133
- return { value: d.driver, label: translated === d.i18n_key ? d.label : translated };
134
- }),
135
- );
136
-
137
- const formFields = computed(() => fields(fDriver.value));
138
-
139
- function openNew() {
140
- const first = drivers.value[0]?.driver ?? '';
141
- fDriver.value = first;
142
- fName.value = '';
143
- fReadOnly.value = false;
144
- fEnabled.value = true;
145
- fConfig.value = defaults(first);
146
- fInvalid.value = [];
147
- testResult.value = null;
148
- formError.value = null;
149
- form.value = { kind: 'new' };
150
- }
151
-
152
- function openEdit(row: StorageRow) {
153
- fDriver.value = row.driver;
154
- fName.value = row.name;
155
- fReadOnly.value = !!row.read_only;
156
- fEnabled.value = row.enabled !== false;
157
- // A copy: editing must not mutate the list row under the user while
158
- // they type, and cancelling must actually cancel.
159
- fConfig.value = { ...(row.config ?? {}) };
160
- fInvalid.value = [];
161
- testResult.value = null;
162
- formError.value = null;
163
- form.value = { kind: 'edit', row };
164
- }
165
-
166
- function closeForm() {
167
- form.value = { kind: 'none' };
168
- testResult.value = null;
169
- formError.value = null;
170
- }
171
-
172
- function onDriverChange(next: string) {
173
- fDriver.value = next;
174
- // Descriptor defaults, wholesale. Carrying keys over from the previous
175
- // driver is how a config ends up with fields nothing reads.
176
- fConfig.value = defaults(next);
177
- fInvalid.value = [];
178
- testResult.value = null;
179
- }
180
-
181
- function validate(): boolean {
182
- const missing = missingRequired(fDriver.value, fConfig.value).map((f) => f.key);
183
- fInvalid.value = missing;
184
- if (!fName.value.trim()) formError.value = t('conn.form.nameRequired');
185
- else if (missing.length) formError.value = t('conn.form.fillRequired');
186
- else formError.value = null;
187
- return !formError.value;
188
- }
189
-
190
- async function runTest() {
191
- testing.value = true;
192
- testResult.value = null;
193
- try {
194
- testResult.value = await testStorage({
195
- driver: fDriver.value,
196
- config: fConfig.value,
197
- });
198
- } finally {
199
- testing.value = false;
200
- }
201
- }
202
-
203
- async function save() {
204
- if (!validate()) return;
205
- saving.value = true;
206
- formError.value = null;
207
- try {
208
- const body = {
209
- name: fName.value.trim(),
210
- driver: fDriver.value,
211
- config: fConfig.value,
212
- read_only: fReadOnly.value,
213
- enabled: fEnabled.value,
214
- };
215
- if (form.value.kind === 'edit') await updateStorage(form.value.row.id, body);
216
- else await createStorage(body);
217
- closeForm();
218
- emit('changed');
219
- } catch (e) {
220
- const msg = (e as { detail?: string; message?: string }) ?? {};
221
- // The backend's own words beat a generic status line: "400" says
222
- // nothing, `ROOT_PATH_FORBIDDEN` says exactly which field is wrong.
223
- let detail = '';
224
- try {
225
- detail = msg.detail ? (JSON.parse(msg.detail) as { error?: string }).error ?? '' : '';
226
- } catch {
227
- detail = msg.detail ?? '';
228
- }
229
- formError.value = detail || msg.message || String(e);
230
- emit('error', { message: formError.value });
231
- } finally {
232
- saving.value = false;
233
- }
234
- }
235
-
236
- async function remove(row: StorageRow) {
237
- if (confirmDelete.value !== row.id) {
238
- confirmDelete.value = row.id;
239
- return;
240
- }
241
- confirmDelete.value = null;
242
- try {
243
- await deleteStorage(row.id);
244
- emit('changed');
245
- } catch (e) {
246
- emit('error', { message: (e as Error).message });
247
- }
248
- }
249
-
250
- /** The one line under a storage's name: where it actually points. */
251
- function summaryOf(row: StorageRow): string {
252
- const d = descriptor(row.driver);
253
- const cfg = row.config ?? {};
254
- const parts: string[] = [];
255
- const rootField = d?.fields.find((f) => f.root);
256
- const pick = (key: string, aliases: string[] = []): string => {
257
- for (const k of [key, ...aliases]) {
258
- const v = cfg[k];
259
- if (typeof v === 'string' && v.trim()) return v;
260
- }
261
- return '';
262
- };
263
- const host = pick('endpoint') || pick('url') || pick('host');
264
- if (host) parts.push(host);
265
- const bucket = pick('bucket');
266
- if (bucket) parts.push(bucket);
267
- if (rootField) {
268
- const r = pick(rootField.key, rootField.aliases ?? []);
269
- if (r) parts.push(r);
270
- }
271
- return parts.join(' · ');
272
- }
273
-
274
- // ── outward: the guides ──────────────────────────────────────────────
275
- const protocols = guideProtocols();
276
- const protocol = ref(protocols[0] ?? 'webdav');
277
- const guideStorage = ref<string>('');
278
97
 
279
98
  const origin = computed(() => connectionsOrigin(props.config, publicUrl.value));
280
99
 
@@ -361,7 +180,6 @@ onBeforeUnmount(() => mq?.removeEventListener?.('change', onMq));
361
180
  watch(
362
181
  () => [props.config.apiBase, props.config.endpoint],
363
182
  () => {
364
- closeForm();
365
183
  void load();
366
184
  },
367
185
  );
@@ -394,233 +212,13 @@ watch(
394
212
  </button>
395
213
  </header>
396
214
 
397
- <nav class="fe-conn__tabs" role="tablist">
398
- <button
399
- type="button"
400
- role="tab"
401
- class="fe-conn__tab"
402
- :class="{ 'is-active': tab === 'storages' }"
403
- :aria-selected="tab === 'storages'"
404
- data-testid="tab-storages"
405
- @click="tab = 'storages'"
406
- >
407
- {{ t('conn.tab.storages') }}
408
- </button>
409
- <button
410
- type="button"
411
- role="tab"
412
- class="fe-conn__tab"
413
- :class="{ 'is-active': tab === 'connect' }"
414
- :aria-selected="tab === 'connect'"
415
- data-testid="tab-connect"
416
- @click="tab = 'connect'"
417
- >
418
- {{ t('conn.tab.connect') }}
419
- </button>
420
- </nav>
421
-
422
- <!-- ══ inward ════════════════════════════════════════════════ -->
423
- <section v-if="tab === 'storages'" class="fe-conn__body" role="tabpanel">
424
- <p v-if="loading && !loaded" class="fe-conn__muted">
425
- {{ t('conn.loading') }}
426
- </p>
427
-
428
- <template v-else>
429
- <!-- The honest non-admin state. Not a disabled form: a form you
430
- cannot submit teaches the wrong thing about whose install this
431
- is. -->
432
- <div
433
- v-if="canManage === false"
434
- class="fe-conn__card fe-conn__card--notice"
435
- data-testid="no-admin"
436
- >
437
- <strong>{{ t('conn.denied.title') }}</strong>
438
- <p class="fe-conn__muted">
439
- {{
440
- denial === 'anonymous'
441
- ? t('conn.denied.anonymous')
442
- : denial === 'unreachable'
443
- ? t('conn.denied.unreachable', { error: error ?? '' })
444
- : t('conn.denied.none')
445
- }}
446
- </p>
447
- <p class="fe-conn__muted">{{ t('conn.denied.guideHint') }}</p>
448
- <button type="button" class="fe-conn__btn fe-conn__btn--primary" @click="tab = 'connect'">
449
- {{ t('conn.denied.guideCta') }}
450
- </button>
451
- </div>
452
-
453
- <!-- What they CAN see, even without admin: the storages they may
454
- browse. Otherwise this half of the panel is blank for them. -->
455
- <div v-if="canManage === false && visible.length" class="fe-conn__list">
456
- <div v-for="name in visible" :key="name" class="fe-conn__card">
457
- <div class="fe-conn__rowmain">
458
- <strong class="fe-conn__name">{{ name }}</strong>
459
- <span class="fe-conn__muted">{{ t('conn.visibleOnly') }}</span>
460
- </div>
461
- </div>
462
- </div>
463
-
464
- <template v-if="canManage">
465
- <p v-if="error" class="fe-conn__error">{{ error }}</p>
466
-
467
- <!-- ── the form ── -->
468
- <div v-if="form.kind !== 'none'" class="fe-conn__card fe-conn__form" data-testid="storage-form">
469
- <h3 class="fe-conn__formtitle">
470
- {{ form.kind === 'edit' ? t('conn.form.editTitle', { name: fName }) : t('conn.form.newTitle') }}
471
- </h3>
472
-
473
- <div class="fe-cfield__row">
474
- <label class="fe-cfield__label" for="fe-conn-name">
475
- {{ t('conn.form.name') }}<span class="fe-cfield__req">*</span>
476
- </label>
477
- <input
478
- id="fe-conn-name"
479
- v-model="fName"
480
- class="fe-cfield__input"
481
- data-testid="storage-name"
482
- :placeholder="t('conn.form.namePlaceholder')"
483
- />
484
- <p class="fe-cfield__help">{{ t('conn.form.nameHelp') }}</p>
485
- </div>
486
-
487
- <div class="fe-cfield__row">
488
- <label class="fe-cfield__label" for="fe-conn-driver">{{ t('conn.form.driver') }}</label>
489
- <select
490
- id="fe-conn-driver"
491
- class="fe-cfield__input"
492
- data-testid="storage-driver"
493
- :value="fDriver"
494
- :disabled="form.kind === 'edit'"
495
- @change="onDriverChange(($event.target as HTMLSelectElement).value)"
496
- >
497
- <option v-for="o in driverOptions" :key="o.value" :value="o.value">
498
- {{ o.label }}
499
- </option>
500
- </select>
501
- <p v-if="form.kind === 'edit'" class="fe-cfield__help">
502
- {{ t('conn.form.driverLocked') }}
503
- </p>
504
- </div>
505
-
506
- <StorageFields
507
- v-model="fConfig"
508
- :fields="formFields"
509
- :locale="locale"
510
- :invalid="fInvalid"
511
- />
512
-
513
- <label class="fe-cfield__check">
514
- <input v-model="fReadOnly" type="checkbox" data-testid="storage-readonly" />
515
- <span>{{ t('conn.form.readOnly') }}</span>
516
- </label>
517
- <label class="fe-cfield__check">
518
- <input v-model="fEnabled" type="checkbox" />
519
- <span>{{ t('conn.form.enabled') }}</span>
520
- </label>
215
+ <!-- ⚠ The server's own words when the storage list cannot be fetched. It
216
+ used to be printed by the storage half, which is gone; the guides
217
+ below are built from that same list, so swallowing the failure here
218
+ would leave a page of instructions quietly missing its storages. -->
219
+ <p v-if="error" class="fe-conn__error" data-testid="conn-error">{{ error }}</p>
521
220
 
522
- <p v-if="formError" class="fe-conn__error" data-testid="form-error">{{ formError }}</p>
523
- <p
524
- v-if="testResult"
525
- class="fe-conn__testresult"
526
- :class="testResult.ok ? 'is-ok' : 'is-bad'"
527
- data-testid="test-result"
528
- >
529
- {{
530
- testResult.ok
531
- ? t('conn.form.testOk', { count: testResult.object_count ?? 0 })
532
- : t('conn.form.testFail', { error: testResult.error ?? '' })
533
- }}
534
- </p>
535
-
536
- <div class="fe-conn__actions">
537
- <button
538
- type="button"
539
- class="fe-conn__btn"
540
- data-testid="storage-test"
541
- :disabled="testing"
542
- @click="runTest"
543
- >
544
- {{ testing ? t('conn.form.testing') : t('conn.form.test') }}
545
- </button>
546
- <button
547
- type="button"
548
- class="fe-conn__btn fe-conn__btn--primary"
549
- data-testid="storage-save"
550
- :disabled="saving"
551
- @click="save"
552
- >
553
- {{ saving ? t('conn.form.saving') : t('conn.form.save') }}
554
- </button>
555
- <button type="button" class="fe-conn__btn" @click="closeForm">
556
- {{ t('conn.form.cancel') }}
557
- </button>
558
- </div>
559
- </div>
560
-
561
- <!-- ── the list ── -->
562
- <div v-else>
563
- <div class="fe-conn__listhead">
564
- <span class="fe-conn__muted">
565
- {{ t('conn.list.count', { n: storages.length }) }}
566
- </span>
567
- <button
568
- type="button"
569
- class="fe-conn__btn fe-conn__btn--primary"
570
- data-testid="storage-add"
571
- @click="openNew"
572
- >
573
- + {{ t('conn.list.add') }}
574
- </button>
575
- </div>
576
-
577
- <p v-if="!storages.length" class="fe-conn__empty">
578
- {{ t('conn.list.empty') }}
579
- </p>
580
-
581
- <div class="fe-conn__list" data-testid="storage-list">
582
- <div v-for="row in storages" :key="row.id" class="fe-conn__card">
583
- <div class="fe-conn__rowmain">
584
- <div class="fe-conn__rowtext">
585
- <strong class="fe-conn__name">{{ row.name }}</strong>
586
- <span class="fe-conn__badge">{{ row.driver }}</span>
587
- <span v-if="row.read_only" class="fe-conn__badge fe-conn__badge--warn">
588
- {{ t('conn.list.readOnly') }}
589
- </span>
590
- <span v-if="row.enabled === false" class="fe-conn__badge">
591
- {{ t('conn.list.disabled') }}
592
- </span>
593
- <div v-if="summaryOf(row)" class="fe-conn__muted fe-conn__summary">
594
- {{ summaryOf(row) }}
595
- </div>
596
- </div>
597
- <div class="fe-conn__rowbtns">
598
- <button
599
- type="button"
600
- class="fe-conn__btn"
601
- :data-testid="`storage-edit-${row.name}`"
602
- @click="openEdit(row)"
603
- >
604
- {{ t('conn.list.edit') }}
605
- </button>
606
- <button
607
- type="button"
608
- class="fe-conn__btn fe-conn__btn--danger"
609
- @click="remove(row)"
610
- >
611
- {{ confirmDelete === row.id ? t('conn.list.confirm') : t('conn.list.remove') }}
612
- </button>
613
- </div>
614
- </div>
615
- </div>
616
- </div>
617
- </div>
618
- </template>
619
- </template>
620
- </section>
621
-
622
- <!-- ══ outward ═══════════════════════════════════════════════ -->
623
- <section v-else class="fe-conn__body" role="tabpanel">
221
+ <section class="fe-conn__body">
624
222
  <div class="fe-conn__guidebar">
625
223
  <label v-if="protocols.length > 1" class="fe-conn__pick">
626
224
  <span class="fe-cfield__label">{{ t('conn.guide.protocol') }}</span>
@@ -747,146 +345,18 @@ watch(
747
345
  font-size: 13px;
748
346
  overflow-wrap: anywhere;
749
347
  }
750
- .fe-conn__tabs {
751
- display: flex;
752
- gap: 4px;
753
- border-bottom: 1px solid var(--fe-border);
754
- }
755
- .fe-conn__tab {
756
- font: inherit;
757
- font-size: 13.5px;
758
- border: 0;
759
- background: none;
760
- color: var(--fe-text-muted);
761
- padding: 7px 12px;
762
- cursor: pointer;
763
- border-bottom: 2px solid transparent;
764
- }
765
- .fe-conn__tab:hover {
766
- color: var(--fe-text);
767
- }
768
- .fe-conn__tab.is-active {
769
- color: var(--fe-primary);
770
- border-bottom-color: var(--fe-primary);
771
- font-weight: 600;
772
- }
773
348
  .fe-conn__body {
774
349
  display: flex;
775
350
  flex-direction: column;
776
351
  gap: 12px;
777
352
  min-width: 0;
778
353
  }
779
- .fe-conn__card {
780
- border: 1px solid var(--fe-border);
781
- border-radius: var(--fe-radius);
782
- background: var(--fe-bg-elev);
783
- padding: 12px 14px;
784
- }
785
- .fe-conn__card--notice {
786
- display: flex;
787
- flex-direction: column;
788
- gap: 8px;
789
- align-items: flex-start;
790
- }
791
- .fe-conn__form {
792
- display: flex;
793
- flex-direction: column;
794
- gap: 14px;
795
- }
796
- .fe-conn__formtitle {
797
- margin: 0;
798
- font-size: 14px;
799
- font-weight: 650;
800
- }
801
- .fe-conn__list {
802
- display: flex;
803
- flex-direction: column;
804
- gap: 8px;
805
- }
806
- .fe-conn__listhead {
807
- display: flex;
808
- align-items: center;
809
- justify-content: space-between;
810
- gap: 12px;
811
- margin-bottom: 4px;
812
- }
813
- .fe-conn__rowmain {
814
- display: flex;
815
- align-items: flex-start;
816
- justify-content: space-between;
817
- gap: 12px;
818
- flex-wrap: wrap;
819
- }
820
- .fe-conn__rowtext {
821
- min-width: 0;
822
- display: flex;
823
- align-items: center;
824
- gap: 8px;
825
- flex-wrap: wrap;
826
- }
827
- .fe-conn__rowbtns {
828
- display: flex;
829
- gap: 6px;
830
- flex: 0 0 auto;
831
- }
832
- .fe-conn__name {
833
- font-size: 14px;
834
- }
835
- .fe-conn__summary {
836
- flex-basis: 100%;
837
- font-family: var(--fe-font-mono);
838
- font-size: 12px;
839
- overflow-wrap: anywhere;
840
- }
841
- .fe-conn__badge {
842
- font-size: 11px;
843
- text-transform: uppercase;
844
- letter-spacing: 0.04em;
845
- border: 1px solid var(--fe-border-strong);
846
- border-radius: 999px;
847
- padding: 1px 8px;
848
- color: var(--fe-text-muted);
849
- }
850
- .fe-conn__badge--warn {
851
- color: var(--fe-danger);
852
- border-color: var(--fe-danger);
853
- }
854
- .fe-conn__muted {
855
- color: var(--fe-text-muted);
856
- font-size: 12.5px;
857
- margin: 0;
858
- line-height: 1.5;
859
- }
860
- .fe-conn__empty {
861
- border: 1px dashed var(--fe-border-strong);
862
- border-radius: var(--fe-radius);
863
- padding: 16px;
864
- color: var(--fe-text-muted);
865
- font-size: 13px;
866
- margin: 0;
867
- }
868
354
  .fe-conn__error {
869
355
  margin: 0;
870
356
  color: var(--fe-danger);
871
357
  font-size: 13px;
872
358
  overflow-wrap: anywhere;
873
359
  }
874
- .fe-conn__testresult {
875
- margin: 0;
876
- font-size: 13px;
877
- overflow-wrap: anywhere;
878
- }
879
- .fe-conn__testresult.is-ok {
880
- color: #16a34a;
881
- }
882
- .fe-conn__testresult.is-bad {
883
- color: var(--fe-danger);
884
- }
885
- .fe-conn__actions {
886
- display: flex;
887
- gap: 8px;
888
- flex-wrap: wrap;
889
- }
890
360
  .fe-conn__btn {
891
361
  font: inherit;
892
362
  font-size: 13px;
@@ -904,14 +374,6 @@ watch(
904
374
  opacity: 0.55;
905
375
  cursor: default;
906
376
  }
907
- .fe-conn__btn--primary {
908
- background: var(--fe-primary);
909
- border-color: var(--fe-primary);
910
- color: #fff;
911
- }
912
- .fe-conn__btn--danger {
913
- color: var(--fe-danger);
914
- }
915
377
  .fe-conn__guidebar {
916
378
  display: flex;
917
379
  gap: 12px;