@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
@@ -0,0 +1,152 @@
1
+ /**
2
+ * tour — "has this person already been offered the first-use tour?"
3
+ *
4
+ * ⚠⚠ Why this module exists (v0.43.0). The tour used to open 900 ms after
5
+ * EVERY explorer mount whose browser had no `filex.tourDone` flag, and the
6
+ * flag was written only when the tour was CLOSED. So a person who opened a
7
+ * second tab before closing the tour got it again; so did every new tab, every
8
+ * route that remounted the explorer, every other browser, every other device.
9
+ * A browser-automation run met it on each fresh mount and its clicks landed on
10
+ * the tour card one run in two. The owner's rule: the tour is offered to a
11
+ * PERSON once — never once per mount.
12
+ *
13
+ * The answer is kept in two places and read as ONE (an "or"):
14
+ *
15
+ * · the account document (`lib/prefs`, key `tour`) — the person's answer,
16
+ * on every browser and device that signs in as them;
17
+ * · this browser's `filex.tourDone` — the answer before the account's
18
+ * arrives, and the only one an embed without an account copy has.
19
+ *
20
+ * ⚠ It only ever goes one way. Nothing clears it: a tour that has been seen
21
+ * cannot be un-seen, and "Restart the tour" opens it without resetting it.
22
+ * That is why the local flag is NOT a per-person mirror cleared at sign-out —
23
+ * a flag that can only say "yes" cannot leak a wrong "no" to the next person.
24
+ *
25
+ * ⚠ It is recorded when the tour is OFFERED, not when it is closed. Closing
26
+ * was the old trigger, and it is exactly what made a second tab, opened while
27
+ * the first still showed the tour, offer it again.
28
+ */
29
+ import {
30
+ currentPrefs,
31
+ onPrefs,
32
+ onPrefsSettled,
33
+ prefsConfigured,
34
+ prefsHydrated,
35
+ prefsSettled,
36
+ savePref,
37
+ } from './prefs';
38
+
39
+ /** This browser's copy. The name is the one every earlier release wrote. */
40
+ export const TOUR_LS_KEY = 'filex.tourDone';
41
+
42
+ /** The value the account document carries once the tour was offered. */
43
+ export const TOUR_DONE = 'done';
44
+
45
+ /** How long after a mount the tour opens — the listing and toolbar are laid out by then. */
46
+ export const TOUR_DELAY_MS = 900;
47
+
48
+ /**
49
+ * How long a mount waits for the account's answer before it decides on this
50
+ * browser's alone. Bounded: a server that never answers must not mean a tour
51
+ * that never comes, nor one that lands in the middle of somebody's work.
52
+ */
53
+ export const TOUR_ACCOUNT_WAIT_MS = 5000;
54
+
55
+ function seenHere(): boolean {
56
+ try {
57
+ return !!localStorage.getItem(TOUR_LS_KEY);
58
+ } catch {
59
+ // No storage (blocked site data, a private window that refuses it): never
60
+ // auto-nag. The tour is one menu row away for anybody who wants it.
61
+ return true;
62
+ }
63
+ }
64
+
65
+ /** Has this person — here, or on the account — already been offered the tour? */
66
+ export function tourSeen(): boolean {
67
+ return seenHere() || !!currentPrefs().tour;
68
+ }
69
+
70
+ let accountWriteQueued: (() => void) | null = null;
71
+
72
+ /**
73
+ * Record that the tour has been offered: in this browser at once, and on the
74
+ * account as soon as the account's document is known.
75
+ *
76
+ * ⚠⚠ Never written to the account BEFORE its document has arrived. `PUT
77
+ * /api/me/prefs` replaces the whole document, and a write made from an empty
78
+ * copy would erase the person's palette, density and language to record a
79
+ * tour.
80
+ */
81
+ export function markTourSeen(): void {
82
+ try {
83
+ localStorage.setItem(TOUR_LS_KEY, '1');
84
+ } catch {
85
+ /* see seenHere */
86
+ }
87
+ if (!prefsConfigured()) return;
88
+ if (prefsHydrated()) {
89
+ if (!currentPrefs().tour) savePref('tour', TOUR_DONE);
90
+ return;
91
+ }
92
+ if (accountWriteQueued) return;
93
+ const off = onPrefs((p) => {
94
+ off();
95
+ accountWriteQueued = null;
96
+ if (!p.tour) savePref('tour', TOUR_DONE);
97
+ });
98
+ accountWriteQueued = off;
99
+ }
100
+
101
+ /**
102
+ * Offer the tour once, `delayMs` after a mount, unless this person has already
103
+ * been offered it. Returns a cancel for the unmount.
104
+ *
105
+ * With an account behind the explorer the decision waits for the account's
106
+ * answer (at most `accountWaitMs`), so a person who took the tour on another
107
+ * device is not shown it again while the answer is on its way.
108
+ */
109
+ export function offerTourOnce(
110
+ show: () => void,
111
+ opts: { delayMs?: number; accountWaitMs?: number } = {},
112
+ ): () => void {
113
+ const delayMs = opts.delayMs ?? TOUR_DELAY_MS;
114
+ const waitMs = opts.accountWaitMs ?? TOUR_ACCOUNT_WAIT_MS;
115
+ let cancelled = false;
116
+ let delay: ReturnType<typeof setTimeout> | undefined;
117
+ let giveUp: ReturnType<typeof setTimeout> | undefined;
118
+ let unwait: (() => void) | undefined;
119
+
120
+ const cancel = () => {
121
+ cancelled = true;
122
+ if (delay) clearTimeout(delay);
123
+ if (giveUp) clearTimeout(giveUp);
124
+ unwait?.();
125
+ };
126
+ if (tourSeen()) return cancel;
127
+
128
+ const decide = () => {
129
+ if (giveUp) clearTimeout(giveUp);
130
+ unwait?.();
131
+ if (cancelled || tourSeen()) return;
132
+ markTourSeen();
133
+ show();
134
+ };
135
+
136
+ delay = setTimeout(() => {
137
+ if (cancelled || tourSeen()) return;
138
+ if (!prefsConfigured() || prefsSettled()) {
139
+ decide();
140
+ return;
141
+ }
142
+ giveUp = setTimeout(decide, waitMs);
143
+ unwait = onPrefsSettled(decide);
144
+ }, delayMs);
145
+ return cancel;
146
+ }
147
+
148
+ /** Testing seam. */
149
+ export function resetTourState(): void {
150
+ accountWriteQueued?.();
151
+ accountWriteQueued = null;
152
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * trashEmpty — "empty the trash" from the explorer's trash view, followed to
3
+ * the end.
4
+ *
5
+ * ⚠⚠ POST /api/admin/trash/empty used to purge inside the request, so for a
6
+ * large trash it never answered: nginx gave up with a 504 at sixty seconds and
7
+ * the view reported "504". The endpoint now answers within seconds — 200 with
8
+ * the final counts when the purge is done, 202 `{running: true, …}` while it
9
+ * goes on in the background — and GET on the same path reports the run until
10
+ * it ends. A 2xx is therefore not "emptied" any more; a status whose
11
+ * `running` is false is.
12
+ */
13
+
14
+ /** One run as the endpoint reports it. `{running: false}` alone (no
15
+ * `started_at`) is "no run known" — the server restarted under it. */
16
+ export interface TrashEmptyStatus {
17
+ /** The ops row behind the run (GET /api/files/ops/{id}, and its cancel). */
18
+ op_id?: number;
19
+ running: boolean;
20
+ /** Waiting its turn behind another purge; `running` is true meanwhile. */
21
+ queued?: boolean;
22
+ /** Somebody stopped it; what it had not reached is still in the trash. */
23
+ cancelled?: boolean;
24
+ total?: number;
25
+ scanned?: number;
26
+ purged?: number;
27
+ failed?: number;
28
+ bytes?: number;
29
+ error?: string;
30
+ started_at?: string;
31
+ finished_at?: string;
32
+ }
33
+
34
+ export interface TrashEmptyIO {
35
+ /** POST /api/admin/trash/empty. */
36
+ start(): Promise<Response>;
37
+ /** GET /api/admin/trash/empty. */
38
+ status(): Promise<Response>;
39
+ /** The run the server started (or, on BUSY, the caller's own run it is
40
+ * following) — once, whether or not it is still going. */
41
+ onStart?(st: TrashEmptyStatus): void;
42
+ /** Each look at a run that is still going. */
43
+ onProgress?(st: TrashEmptyStatus): void;
44
+ /** Another press had already started this caller's run; it is followed. */
45
+ onBusy?(): void;
46
+ /** True once nobody is watching any more — the view went away. */
47
+ stopped?(): boolean;
48
+ /** The pause between looks (a seam for tests). */
49
+ sleep?(ms: number): Promise<void>;
50
+ }
51
+
52
+ /** A purge holds the trash and it is not one this caller may follow. */
53
+ export class TrashEmptyBusy extends Error {}
54
+
55
+ export const TRASH_EMPTY_POLL_MS = 1500;
56
+
57
+ type Json = Record<string, unknown>;
58
+
59
+ async function body(res: Response): Promise<Json> {
60
+ try {
61
+ const parsed: unknown = await res.json();
62
+ return parsed && typeof parsed === 'object' ? (parsed as Json) : {};
63
+ } catch {
64
+ return {};
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Starts the purge and resolves with its final status — or `null` once
70
+ * `stopped()` says nobody is watching, which ends the watching, never the
71
+ * purge. Throws TrashEmptyBusy, or the server's own sentence for any other
72
+ * refusal.
73
+ */
74
+ export async function emptyTrashAndFollow(
75
+ io: TrashEmptyIO,
76
+ pollMs = TRASH_EMPTY_POLL_MS,
77
+ ): Promise<TrashEmptyStatus | null> {
78
+ const sleep = io.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
79
+ const res = await io.start();
80
+ const first = await body(res);
81
+ let st: TrashEmptyStatus;
82
+ if (res.status === 409 && first.code === 'BUSY') {
83
+ // The server hands back the caller's own run when it is that one; a run
84
+ // it does not describe (another tenant's, the nightly sweep) cannot be
85
+ // followed from here, and must not be reported as this caller's ending.
86
+ const job = first.job as TrashEmptyStatus | undefined;
87
+ if (!job?.running) throw new TrashEmptyBusy(String(first.error ?? 'busy'));
88
+ io.onBusy?.();
89
+ st = job;
90
+ } else if (!res.ok) {
91
+ throw new Error(typeof first.error === 'string' && first.error ? first.error : String(res.status));
92
+ } else {
93
+ st = first as unknown as TrashEmptyStatus;
94
+ }
95
+ io.onStart?.(st);
96
+ while (st.running) {
97
+ io.onProgress?.(st);
98
+ await sleep(pollMs);
99
+ if (io.stopped?.()) return null;
100
+ try {
101
+ const look = await io.status();
102
+ const next = look.ok ? await body(look) : {};
103
+ // Only an answer that says whether it is running moves the state on;
104
+ // anything else is a look that failed.
105
+ if (typeof next.running === 'boolean') st = next as unknown as TrashEmptyStatus;
106
+ } catch {
107
+ // One look that failed is not the end of the run: look again.
108
+ }
109
+ }
110
+ return st;
111
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The Trash's "time left" for one item, in words: the server's own count of
3
+ * whole days before it is purged (`ttl_days`, trash/service.go — retention
4
+ * minus the days since deletion, never below zero).
5
+ *
6
+ * 30 → "30 days" 1 → "1 day" 0 → "Due for deletion" none → "—"
7
+ *
8
+ * ⚠ ONE sentence for one concept, shared by the explorer's Trash view
9
+ * (ListView) and the admin panel's Trash page. They used to carry two key
10
+ * families for it — `trash.days_left` in the explorer's table and
11
+ * `trash.days_remaining` in the admin panel's — and the admin page printed
12
+ * "0 days" for an item the explorer called due for deletion. The keys live in
13
+ * the explorer's table (`trash.days_remaining`, `_one`, `_due`); a caller
14
+ * hands its own `t` so a surface keeps its own reactivity.
15
+ */
16
+ export function trashTimeLeft(
17
+ ttl: number | null | undefined,
18
+ t: (key: string, vars?: Record<string, string | number>) => string,
19
+ ): string {
20
+ if (typeof ttl !== 'number' || !Number.isFinite(ttl)) return '—';
21
+ if (ttl <= 0) return t('trash.days_remaining_due');
22
+ return t('trash.days_remaining', { n: ttl });
23
+ }