@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.
Files changed (234) 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-CC6rJXeP.js → DrawioViewer-Bgrj8-zG.js} +45 -44
  7. package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
  8. package/dist/{EpubViewer-CAoTSXTO.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-CdZeK4j4.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 +5227 -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 +1055 -253
  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 +31 -15
  73. package/src/components/RecoveryKeyModal.vue +1 -1
  74. package/src/components/RowActions.vue +134 -0
  75. package/src/components/S3KeysPanel.vue +98 -62
  76. package/src/components/SSHKeysPanel.vue +79 -30
  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 -31
  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 +18 -3
  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 +515 -117
  186. package/src/locales/resolve.ts +17 -8
  187. package/src/locales/tr.ts +536 -152
  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 +2890 -260
  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 +50 -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-BUQWLhwX.js +0 -124
  216. package/dist/ArchiveViewer-BUQWLhwX.js.map +0 -1
  217. package/dist/CsvViewer-Bg2_aqzc.js +0 -141
  218. package/dist/CsvViewer-Bg2_aqzc.js.map +0 -1
  219. package/dist/DrawioViewer-CC6rJXeP.js.map +0 -1
  220. package/dist/EpubViewer-CAoTSXTO.js.map +0 -1
  221. package/dist/IpynbViewer-D0GwGpnE.js +0 -184
  222. package/dist/IpynbViewer-D0GwGpnE.js.map +0 -1
  223. package/dist/MermaidViewer-DGwAvRZg.js +0 -137
  224. package/dist/MermaidViewer-DGwAvRZg.js.map +0 -1
  225. package/dist/PsdViewer-DhGweA0m.js +0 -122
  226. package/dist/PsdViewer-DhGweA0m.js.map +0 -1
  227. package/dist/TiffViewer-D0AurvAh.js +0 -142
  228. package/dist/TiffViewer-D0AurvAh.js.map +0 -1
  229. package/dist/Viewer3D-CdZeK4j4.js.map +0 -1
  230. package/dist/index-BArI-B1M.js +0 -26911
  231. package/dist/index-BArI-B1M.js.map +0 -1
  232. package/dist/useViewerFetch-czqbd2Lj.js +0 -25
  233. package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
  234. package/src/composables/useScrolledX.ts +0 -21
@@ -31,6 +31,8 @@ import { useLocale } from '../composables/useLocale';
31
31
  import { browserProbeURL } from '../lib/externalReach';
32
32
  import { fileIconTile } from '../lib/fileIcons';
33
33
  import { actionIconSvg } from '../lib/actionIcons';
34
+ import { OFFICE_EXTS } from '../lib/serviceGate';
35
+ import { requestFailure, sayFailure } from '../lib/errorWords';
34
36
 
35
37
  const props = defineProps<{
36
38
  open: boolean;
@@ -40,6 +42,13 @@ const props = defineProps<{
40
42
  downloadUrl: (path: string) => string;
41
43
  onlyOfficeBase?: string | null;
42
44
  onlyOfficeConfigEndpoint?: string | null;
45
+ /**
46
+ * Could this person set up a missing service (`capabilities.caller_admin`)?
47
+ * Only changes WHICH sentence a missing document server gets: an
48
+ * administrator is told where to set it up, everybody else that it is not
49
+ * available here and how to get the file anyway.
50
+ */
51
+ canConfigure?: boolean;
43
52
  saveTextEndpoint?: string | null;
44
53
  /** Endpoint for the archive (zip/rar/7z/tar) member list. Forwarded to
45
54
  * ArchiveViewer, which otherwise assumes a same-origin path. */
@@ -136,7 +145,10 @@ const CODE_LANGS: Record<string, string> = {
136
145
  // CSV/TSV land in the rich viewer (`csv` kind below) instead of the
137
146
  // legacy plain-text path so the user gets a proper table preview.
138
147
  const TEXT_PLAIN = ['txt', 'log'];
139
- const OFFICE = ['docx', 'doc', 'xlsx', 'xls', 'pptx', 'ppt', 'odt', 'ods', 'odp', 'rtf'];
148
+ /* The document server's formats — ONE list, shared with the explorer's menu
149
+ * (lib/serviceGate), so a type previewed as "office" is exactly a type whose
150
+ * Open needs ONLYOFFICE. */
151
+ const OFFICE = OFFICE_EXTS;
140
152
 
141
153
  /**
142
154
  * Lazy viewer map — extension → component loader. Each loader is a
@@ -223,7 +235,7 @@ async function loadViewerFor(extension: string): Promise<void> {
223
235
  const mod = (await loader()) as { default?: Component };
224
236
  viewerCmp.value = mod.default ?? (mod as unknown as Component);
225
237
  } catch (err) {
226
- viewerLoadError.value = err instanceof Error ? err.message : 'viewer load failed';
238
+ viewerLoadError.value = said(err, 'err.viewer_failed').text;
227
239
  if (extension === 'pdf') pdfFallbackToNative.value = true;
228
240
  }
229
241
  }
@@ -240,6 +252,9 @@ const viewerProps = computed(() => {
240
252
  ext: e,
241
253
  mime: props.file?.mime_type,
242
254
  t,
255
+ /* The viewer's language as a code, for the one thing `t` cannot give it:
256
+ how a NUMBER is written (a size in the archive list). */
257
+ locale: props.locale,
243
258
  authHeaders: props.authHeaders,
244
259
  authCredentials: props.authCredentials,
245
260
  };
@@ -256,6 +271,7 @@ const viewerProps = computed(() => {
256
271
  }
257
272
  if (e === 'drawio' || e === 'dio') {
258
273
  base.drawioUrl = props.drawioUrl ?? undefined;
274
+ base.canConfigure = props.canConfigure === true;
259
275
  base.saveUrl = props.saveTextEndpoint ?? undefined;
260
276
  base.readOnly = props.openMode === 'view';
261
277
  }
@@ -318,6 +334,26 @@ const canEditKind = computed<boolean>(() =>
318
334
  !!props.file && EDITABLE_EXTS.has(ext(props.file)),
319
335
  );
320
336
 
337
+ /**
338
+ * The Edit button of a document whose editor is an optional service that is
339
+ * not there — ONLYOFFICE for office files, draw.io for diagrams. It led to a
340
+ * tab that could only say so. The owner's rule (lib/serviceGate): greyed with
341
+ * where to set it up for someone who can, not offered to anybody else.
342
+ */
343
+ const editNeeds = computed<'' | 'onlyoffice' | 'drawio'>(() => {
344
+ const e = ext(props.file);
345
+ if (OFFICE.includes(e) && !props.onlyOfficeBase) return 'onlyoffice';
346
+ if ((e === 'drawio' || e === 'dio') && !props.drawioUrl) return 'drawio';
347
+ return '';
348
+ });
349
+ const editTitle = computed(() =>
350
+ editNeeds.value === 'onlyoffice'
351
+ ? t('ctx.needs_onlyoffice')
352
+ : editNeeds.value === 'drawio'
353
+ ? t('ctx.needs_drawio')
354
+ : t('viewer.edit'),
355
+ );
356
+
321
357
  // Keep adapter prefix so backend resolves the right storage — stripping
322
358
  // it defaults to storages[0] and 404s on any non-default adapter.
323
359
  const src = computed(() => (props.file ? props.previewUrl(props.file.path) : ''));
@@ -472,6 +508,24 @@ function onGroundClick(): void {
472
508
 
473
509
  const loading = ref(false);
474
510
  const fetchError = ref<string | null>(null);
511
+ /** An administrator's second line under `fetchError` (lib/errorWords). */
512
+ const fetchErrorDetail = ref<string | null>(null);
513
+
514
+ /**
515
+ * A failure, said (lib/errorWords): the sentence for everybody, the raw words
516
+ * as a second line only for a caller who can administer the instance.
517
+ *
518
+ * ⚠⚠ This modal used to print what it caught: "save failed: 500 {…}",
519
+ * "404 Not Found", "Monaco mount fail: …" — a status code, a JSON body and a
520
+ * library's message, to whoever was editing (QA, 2026-09-21).
521
+ */
522
+ function said(err: unknown, fallbackKey: string) {
523
+ return sayFailure(err, t(fallbackKey), { callerAdmin: props.canConfigure === true });
524
+ }
525
+ function showFetchError(f: { text: string; detail?: string }): void {
526
+ fetchError.value = f.text;
527
+ fetchErrorDetail.value = f.detail ?? null;
528
+ }
475
529
  const rawText = ref<string>('');
476
530
  const MAX_TEXT_BYTES = 1_000_000;
477
531
  const tooLarge = ref(false);
@@ -499,6 +553,7 @@ async function saveMarkdown() {
499
553
  if (!props.saveTextEndpoint || !props.file || mdSaving.value) return;
500
554
  mdSaving.value = true;
501
555
  fetchError.value = null;
556
+ fetchErrorDetail.value = null;
502
557
  try {
503
558
  const headers = {
504
559
  'Content-Type': 'application/json',
@@ -511,11 +566,11 @@ async function saveMarkdown() {
511
566
  body: JSON.stringify({ path: stripAdapter(props.file.path), content: rawText.value }),
512
567
  });
513
568
  if (!res.ok) {
514
- throw new Error(`save failed: ${res.status} ${await res.text()}`);
569
+ throw requestFailure(res.status, await res.text().catch(() => ''), props.locale);
515
570
  }
516
571
  mdDirty.value = false;
517
572
  } catch (err) {
518
- fetchError.value = err instanceof Error ? err.message : String(err);
573
+ showFetchError(said(err, 'err.save_failed'));
519
574
  } finally {
520
575
  mdSaving.value = false;
521
576
  }
@@ -524,6 +579,7 @@ async function saveMarkdown() {
524
579
  async function fetchText(url: string): Promise<void> {
525
580
  loading.value = true;
526
581
  fetchError.value = null;
582
+ fetchErrorDetail.value = null;
527
583
  rawText.value = '';
528
584
  tooLarge.value = false;
529
585
  try {
@@ -534,7 +590,7 @@ async function fetchText(url: string): Promise<void> {
534
590
  headers,
535
591
  });
536
592
  if (!res.ok) {
537
- throw new Error(`${res.status} ${res.statusText}`);
593
+ throw requestFailure(res.status, await res.text().catch(() => ''), props.locale);
538
594
  }
539
595
  const len = Number(res.headers.get('content-length') || '0');
540
596
  if (len > MAX_TEXT_BYTES) {
@@ -549,7 +605,7 @@ async function fetchText(url: string): Promise<void> {
549
605
  rawText.value = text;
550
606
  }
551
607
  } catch (err) {
552
- fetchError.value = err instanceof Error ? err.message : String(err);
608
+ showFetchError(said(err, 'err.load_failed'));
553
609
  } finally {
554
610
  loading.value = false;
555
611
  }
@@ -604,7 +660,7 @@ async function renderMarkdown(text: string): Promise<void> {
604
660
  await enrichMarkdown();
605
661
  } catch (err) {
606
662
  markdownHtml.value = '';
607
- fetchError.value = err instanceof Error ? err.message : String(err);
663
+ showFetchError(said(err, 'err.load_failed'));
608
664
  }
609
665
  }
610
666
 
@@ -724,7 +780,7 @@ async function highlightCode(text: string, language: string): Promise<void> {
724
780
  }
725
781
  } catch (err) {
726
782
  codeHtml.value = '';
727
- fetchError.value = err instanceof Error ? err.message : String(err);
783
+ showFetchError(said(err, 'err.load_failed'));
728
784
  }
729
785
  }
730
786
 
@@ -834,7 +890,7 @@ async function tryMountMonaco(text: string, extension: string): Promise<boolean>
834
890
  monacoReady.value = true;
835
891
  return true;
836
892
  } catch (err) {
837
- fetchError.value = `Monaco mount fail: ${(err as Error).message}`;
893
+ showFetchError(said(err, 'err.viewer_failed'));
838
894
  return false;
839
895
  }
840
896
  }
@@ -860,15 +916,15 @@ async function saveCode(): Promise<void> {
860
916
  }),
861
917
  });
862
918
  if (!res.ok) {
863
- const txt = await res.text().catch(() => '');
864
- throw new Error(`${res.status} ${res.statusText}${txt ? ' — ' + txt.slice(0, 150) : ''}`);
919
+ throw requestFailure(res.status, await res.text().catch(() => ''), props.locale);
865
920
  }
866
921
  saveOk.value = true;
867
922
  setTimeout(() => {
868
923
  saveOk.value = false;
869
924
  }, 2500);
870
925
  } catch (err) {
871
- saveError.value = (err as Error).message;
926
+ const f = said(err, 'err.save_failed');
927
+ saveError.value = f.detail ? `${f.text}\n${f.detail}` : f.text;
872
928
  } finally {
873
929
  saving.value = false;
874
930
  }
@@ -881,6 +937,7 @@ async function runOrchestration(open: boolean, url: string, k: string): Promise<
881
937
  markdownHtml.value = '';
882
938
  codeHtml.value = '';
883
939
  fetchError.value = null;
940
+ fetchErrorDetail.value = null;
884
941
  officeError.value = null;
885
942
  viewerCmp.value = null;
886
943
  viewerLoadError.value = null;
@@ -963,11 +1020,42 @@ const officeEl = ref<HTMLDivElement | null>(null);
963
1020
  const officeError = ref<string | null>(null);
964
1021
  let officeEditor: any = null;
965
1022
 
1023
+ /** "No document server here", in the sentence this person can act on. */
1024
+ function officeUnconfigured(): string {
1025
+ return t(props.canConfigure ? 'viewer.office_unconfigured_admin' : 'viewer.office_unconfigured');
1026
+ }
1027
+
1028
+ /**
1029
+ * ⚠⚠ Every failure below becomes a SENTENCE, never the HTTP layer. This
1030
+ * function used to throw `Config fetch ${status}: ${body}` and put it on
1031
+ * screen, which is how a person clicking "Open" on a .docx read
1032
+ * `Config fetch 503: {"error":"onlyoffice not configured"}` (owner,
1033
+ * 2026-09-21: "böyle hata vermek yerine … düzgün hata mesajı vermemiz
1034
+ * lazım"). The status still reaches the console for whoever is debugging it.
1035
+ */
1036
+ async function officeConfigError(res: Response): Promise<string> {
1037
+ let body = '';
1038
+ try {
1039
+ body = (await res.text()).slice(0, 200);
1040
+ } catch {
1041
+ /* nothing to read */
1042
+ }
1043
+ console.warn('[filex] ONLYOFFICE config request failed', res.status, body);
1044
+ if (res.status === 503 && /not configured/i.test(body)) return officeUnconfigured();
1045
+ if (res.status === 403) return t('viewer.office_forbidden');
1046
+ return t('viewer.office_failed');
1047
+ }
1048
+
966
1049
  async function mountOnlyOfficeEditor(): Promise<void> {
967
1050
  officeError.value = null;
968
1051
  if (!props.file || kind.value !== 'office') return;
969
- if (!props.onlyOfficeConfigEndpoint) {
970
- officeError.value = t('viewer.office_unconfigured');
1052
+ /* ⚠ BOTH halves. The standalone /files/edit route always handed over the
1053
+ * config ENDPOINT and left the base null when the capabilities probe said
1054
+ * the service was off — so this check, reading the endpoint alone, let the
1055
+ * request go out and put the server's 503 on screen. No base means no
1056
+ * document server, whatever else is configured. */
1057
+ if (!props.onlyOfficeConfigEndpoint || !props.onlyOfficeBase) {
1058
+ officeError.value = officeUnconfigured();
971
1059
  return;
972
1060
  }
973
1061
  if (!officeEl.value) return;
@@ -989,7 +1077,8 @@ async function mountOnlyOfficeEditor(): Promise<void> {
989
1077
  }),
990
1078
  });
991
1079
  if (!res.ok) {
992
- throw new Error(`Config fetch ${res.status}: ${(await res.text()).slice(0, 200)}`);
1080
+ officeError.value = await officeConfigError(res);
1081
+ return;
993
1082
  }
994
1083
  const { config, documentServerUrl } = (await res.json()) as {
995
1084
  config: any;
@@ -1014,7 +1103,11 @@ async function mountOnlyOfficeEditor(): Promise<void> {
1014
1103
  }
1015
1104
  officeEditor = new W.DocsAPI.DocEditor(mountId, config);
1016
1105
  } catch (err) {
1017
- officeError.value = err instanceof Error ? err.message : String(err);
1106
+ /* The document server's script did not load, or did not define its API:
1107
+ the server is configured but not answering. The detail is for the
1108
+ console, the sentence for the person. */
1109
+ console.warn('[filex] ONLYOFFICE did not start', err);
1110
+ officeError.value = t(props.canConfigure ? 'viewer.office_unreachable_admin' : 'viewer.office_unreachable');
1018
1111
  }
1019
1112
  }
1020
1113
 
@@ -1141,11 +1234,12 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
1141
1234
  @change="(v: boolean) => emit('starred', v)"
1142
1235
  />
1143
1236
  <button
1144
- v-if="openMode === 'view' && canEditKind && newTabEnabled !== false /* wiring:e2 */"
1237
+ v-if="openMode === 'view' && canEditKind && newTabEnabled !== false /* wiring:e2 */ && (!editNeeds || canConfigure)"
1145
1238
  type="button"
1146
1239
  class="fe-viewer__act"
1147
- :title="t('viewer.edit')"
1148
- :aria-label="t('viewer.edit')"
1240
+ :disabled="!!editNeeds"
1241
+ :title="editTitle"
1242
+ :aria-label="editTitle"
1149
1243
  @click="openEditInNewTab"
1150
1244
  ><span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('rename')"></span></button>
1151
1245
  <button
@@ -1239,15 +1333,19 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
1239
1333
  v-else
1240
1334
  ref="markdownEl"
1241
1335
  class="fe-preview__md-split-output fe-preview__md"
1336
+ dir="auto"
1242
1337
  v-html="markdownHtml"
1243
1338
  ></div>
1244
1339
  </div>
1245
1340
  </div>
1246
1341
  <div v-else-if="fetchError" class="fe-preview__fallback">
1247
1342
  <p>{{ fetchError }}</p>
1343
+ <p v-if="fetchErrorDetail" class="fe-preview__error-detail" data-testid="preview-error-detail">{{ fetchErrorDetail }}</p>
1248
1344
  <a :href="download" class="fe-btn fe-btn--primary">{{ t('viewer.download') }}</a>
1249
1345
  </div>
1250
- <div v-else-if="markdownHtml" ref="markdownEl" class="fe-preview__md" v-html="markdownHtml"></div>
1346
+ <!-- ⚠ RTL: a document reads in ITS direction, not the interface's —
1347
+ `auto` lets an English README stay left to right in Arabic. -->
1348
+ <div v-else-if="markdownHtml" ref="markdownEl" class="fe-preview__md" dir="auto" v-html="markdownHtml"></div>
1251
1349
  <pre v-else class="fe-preview__pre">{{ rawText }}</pre>
1252
1350
  </template>
1253
1351
 
@@ -1287,6 +1385,7 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
1287
1385
  </div>
1288
1386
  <div v-else-if="fetchError" class="fe-preview__fallback">
1289
1387
  <p>{{ fetchError }}</p>
1388
+ <p v-if="fetchErrorDetail" class="fe-preview__error-detail" data-testid="preview-error-detail">{{ fetchErrorDetail }}</p>
1290
1389
  <a :href="download" class="fe-btn fe-btn--primary">{{ t('viewer.download') }}</a>
1291
1390
  </div>
1292
1391
  <div v-else class="fe-preview__code-wrap">
@@ -2,12 +2,17 @@
2
2
  import { ref, watch } from 'vue';
3
3
  import type { LocaleCode } from '../types/ExplorerConfig';
4
4
  import { useLocale } from '../composables/useLocale';
5
+ import { isInternalName } from '../lib/internalPaths';
5
6
  import Modal from './Modal.vue';
6
7
 
7
8
  const props = defineProps<{
8
9
  open: boolean;
9
10
  locale: LocaleCode;
10
11
  currentName: string;
12
+ /** Why the last attempt did not happen — the host's answer from the server
13
+ * (a taken name, a refusal, an outage). Shown under the field until the
14
+ * name is edited. */
15
+ error?: string | null;
11
16
  }>();
12
17
 
13
18
  const emit = defineEmits<{
@@ -17,20 +22,49 @@ const emit = defineEmits<{
17
22
 
18
23
  const { t } = useLocale(() => props.locale);
19
24
  const name = ref('');
25
+ // What the dialog is saying right now — ONE line, whoever said it: the host's
26
+ // error, or the dialog's own refusal of a name that is not one (a slash, "."
27
+ // or "..", a name filex keeps for itself). Either goes away once the person
28
+ // edits the name.
29
+ const shownError = ref<string | null>(null);
20
30
 
21
31
  watch(
22
32
  () => [props.open, props.currentName] as const,
23
33
  ([isOpen, cur]) => {
24
34
  if (isOpen) {
25
35
  name.value = cur;
36
+ shownError.value = props.error ?? null;
26
37
  }
27
38
  },
28
39
  );
29
40
 
41
+ watch(
42
+ () => props.error,
43
+ (err) => {
44
+ shownError.value = err ?? null;
45
+ },
46
+ { immediate: true },
47
+ );
48
+
49
+ function onInput() {
50
+ shownError.value = null;
51
+ }
52
+
30
53
  function submit() {
31
54
  const clean = name.value.trim();
32
55
  if (!clean) return;
33
- if (/[\\/]/.test(clean) || clean === '.' || clean === '..') return;
56
+ if (/[\\/]/.test(clean) || clean === '.' || clean === '..') {
57
+ // Refused here with a word, not by doing nothing: a Save button that
58
+ // silently ignores the click reads as a broken dialog.
59
+ shownError.value = t('modal.newfolder.invalid');
60
+ return;
61
+ }
62
+ // A document renamed to `.versions` would vanish from every view the
63
+ // moment it was renamed; the server refuses it, this says why.
64
+ if (isInternalName(clean)) {
65
+ shownError.value = t('names.reserved', { name: clean });
66
+ return;
67
+ }
34
68
  emit('submit', clean);
35
69
  }
36
70
  </script>
@@ -43,8 +77,10 @@ function submit() {
43
77
  type="text"
44
78
  class="fe-input"
45
79
  autocomplete="off"
80
+ @input="onInput"
46
81
  @keydown.enter.prevent="submit"
47
82
  />
83
+ <p v-if="shownError" class="fe-form__error" role="alert" data-testid="rename-error">{{ shownError }}</p>
48
84
  </form>
49
85
  <template #actions>
50
86
  <button type="button" class="fe-btn" @click="emit('close')">