@maxal_studio/kratosjs-react 1.0.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 (529) hide show
  1. package/README.md +44 -0
  2. package/dist/FieldRenderer.d.ts +13 -0
  3. package/dist/FieldRenderer.js +62 -0
  4. package/dist/FormRenderer.d.ts +7 -0
  5. package/dist/FormRenderer.js +78 -0
  6. package/dist/TableRenderer.d.ts +2 -0
  7. package/dist/TableRenderer.js +1 -0
  8. package/dist/api/actionsApi.d.ts +23 -0
  9. package/dist/api/actionsApi.js +46 -0
  10. package/dist/api/authenticatedFetch.d.ts +8 -0
  11. package/dist/api/authenticatedFetch.js +31 -0
  12. package/dist/api/exportApi.d.ts +18 -0
  13. package/dist/api/exportApi.js +50 -0
  14. package/dist/api/http.d.ts +24 -0
  15. package/dist/api/http.js +52 -0
  16. package/dist/api/resourceApi.d.ts +37 -0
  17. package/dist/api/resourceApi.js +52 -0
  18. package/dist/api/tableApi.d.ts +83 -0
  19. package/dist/api/tableApi.js +51 -0
  20. package/dist/api/urls.d.ts +19 -0
  21. package/dist/api/urls.js +46 -0
  22. package/dist/app.d.ts +101 -0
  23. package/dist/app.js +89 -0
  24. package/dist/auth/AuthContext.d.ts +22 -0
  25. package/dist/auth/AuthContext.js +147 -0
  26. package/dist/auth/LoginPage.d.ts +10 -0
  27. package/dist/auth/LoginPage.js +179 -0
  28. package/dist/auth/ProtectedRoute.d.ts +12 -0
  29. package/dist/auth/ProtectedRoute.js +22 -0
  30. package/dist/auth/authApiClient.d.ts +24 -0
  31. package/dist/auth/authApiClient.js +95 -0
  32. package/dist/auth/types.d.ts +103 -0
  33. package/dist/auth/types.js +1 -0
  34. package/dist/components/ActionFormModal.d.ts +22 -0
  35. package/dist/components/ActionFormModal.js +8 -0
  36. package/dist/components/AdminPanel.d.ts +11 -0
  37. package/dist/components/AdminPanel.js +194 -0
  38. package/dist/components/Checkbox.d.ts +10 -0
  39. package/dist/components/Checkbox.js +8 -0
  40. package/dist/components/CheckboxField.d.ts +7 -0
  41. package/dist/components/CheckboxField.js +26 -0
  42. package/dist/components/ColorPickerField.d.ts +7 -0
  43. package/dist/components/ColorPickerField.js +26 -0
  44. package/dist/components/DateTimePickerField.d.ts +7 -0
  45. package/dist/components/DateTimePickerField.js +64 -0
  46. package/dist/components/FileUploadField.d.ts +9 -0
  47. package/dist/components/FileUploadField.js +478 -0
  48. package/dist/components/GlobalSearch.d.ts +22 -0
  49. package/dist/components/GlobalSearch.js +181 -0
  50. package/dist/components/GroupField.d.ts +7 -0
  51. package/dist/components/GroupField.js +23 -0
  52. package/dist/components/HiddenField.d.ts +3 -0
  53. package/dist/components/HiddenField.js +10 -0
  54. package/dist/components/ModalBreadcrumb.d.ts +5 -0
  55. package/dist/components/ModalBreadcrumb.js +33 -0
  56. package/dist/components/ModalDrawer.d.ts +15 -0
  57. package/dist/components/ModalDrawer.js +40 -0
  58. package/dist/components/RadioField.d.ts +7 -0
  59. package/dist/components/RadioField.js +26 -0
  60. package/dist/components/RepeaterField.d.ts +3 -0
  61. package/dist/components/RepeaterField.js +191 -0
  62. package/dist/components/ResourceModalRenderer.d.ts +10 -0
  63. package/dist/components/ResourceModalRenderer.js +80 -0
  64. package/dist/components/RichEditorField.d.ts +3 -0
  65. package/dist/components/RichEditorField.js +655 -0
  66. package/dist/components/SectionField.d.ts +9 -0
  67. package/dist/components/SectionField.js +111 -0
  68. package/dist/components/SelectField.d.ts +8 -0
  69. package/dist/components/SelectField.js +523 -0
  70. package/dist/components/TabsField.d.ts +10 -0
  71. package/dist/components/TabsField.js +214 -0
  72. package/dist/components/TagsInputField.d.ts +7 -0
  73. package/dist/components/TagsInputField.js +172 -0
  74. package/dist/components/TextInputField.d.ts +7 -0
  75. package/dist/components/TextInputField.js +44 -0
  76. package/dist/components/TextareaField.d.ts +7 -0
  77. package/dist/components/TextareaField.js +31 -0
  78. package/dist/components/ToggleField.d.ts +7 -0
  79. package/dist/components/ToggleField.js +57 -0
  80. package/dist/components/ViewModal.d.ts +25 -0
  81. package/dist/components/ViewModal.js +159 -0
  82. package/dist/components/blocks/BlockRenderer.d.ts +7 -0
  83. package/dist/components/blocks/BlockRenderer.js +36 -0
  84. package/dist/components/blocks/FormBlockRenderer.d.ts +6 -0
  85. package/dist/components/blocks/FormBlockRenderer.js +110 -0
  86. package/dist/components/blocks/TableBlockRenderer.d.ts +6 -0
  87. package/dist/components/blocks/TableBlockRenderer.js +12 -0
  88. package/dist/components/blocks/TabsBlockRenderer.d.ts +7 -0
  89. package/dist/components/blocks/TabsBlockRenderer.js +11 -0
  90. package/dist/components/blocks/WidgetBlockRenderer.d.ts +6 -0
  91. package/dist/components/blocks/WidgetBlockRenderer.js +11 -0
  92. package/dist/components/columns/CheckboxColumnComponent.d.ts +6 -0
  93. package/dist/components/columns/CheckboxColumnComponent.js +21 -0
  94. package/dist/components/columns/ColorColumnComponent.d.ts +3 -0
  95. package/dist/components/columns/ColorColumnComponent.js +11 -0
  96. package/dist/components/columns/DeeplinkWrapper.d.ts +15 -0
  97. package/dist/components/columns/DeeplinkWrapper.js +85 -0
  98. package/dist/components/columns/IconColumnComponent.d.ts +3 -0
  99. package/dist/components/columns/IconColumnComponent.js +52 -0
  100. package/dist/components/columns/ImageColumnComponent.d.ts +3 -0
  101. package/dist/components/columns/ImageColumnComponent.js +98 -0
  102. package/dist/components/columns/MediaColumnComponent.d.ts +3 -0
  103. package/dist/components/columns/MediaColumnComponent.js +160 -0
  104. package/dist/components/columns/SelectColumnComponent.d.ts +6 -0
  105. package/dist/components/columns/SelectColumnComponent.js +26 -0
  106. package/dist/components/columns/TagsColumnComponent.d.ts +3 -0
  107. package/dist/components/columns/TagsColumnComponent.js +18 -0
  108. package/dist/components/columns/TextColumnComponent.d.ts +11 -0
  109. package/dist/components/columns/TextColumnComponent.js +107 -0
  110. package/dist/components/columns/TextInputColumnComponent.d.ts +6 -0
  111. package/dist/components/columns/TextInputColumnComponent.js +18 -0
  112. package/dist/components/columns/ToggleColumnComponent.d.ts +6 -0
  113. package/dist/components/columns/ToggleColumnComponent.js +25 -0
  114. package/dist/components/columns/VideoColumnComponent.d.ts +3 -0
  115. package/dist/components/columns/VideoColumnComponent.js +125 -0
  116. package/dist/components/columns/ViewColumnComponent.d.ts +3 -0
  117. package/dist/components/columns/ViewColumnComponent.js +7 -0
  118. package/dist/components/errors/ErrorBoundary.d.ts +23 -0
  119. package/dist/components/errors/ErrorBoundary.js +33 -0
  120. package/dist/components/filters/CustomFilterComponent.d.ts +10 -0
  121. package/dist/components/filters/CustomFilterComponent.js +33 -0
  122. package/dist/components/filters/DateFilterComponent.d.ts +15 -0
  123. package/dist/components/filters/DateFilterComponent.js +132 -0
  124. package/dist/components/filters/QueryBuilderFilterComponent.d.ts +11 -0
  125. package/dist/components/filters/QueryBuilderFilterComponent.js +200 -0
  126. package/dist/components/layout/Header.d.ts +10 -0
  127. package/dist/components/layout/Header.js +70 -0
  128. package/dist/components/layout/PanelBrandMark.d.ts +8 -0
  129. package/dist/components/layout/PanelBrandMark.js +28 -0
  130. package/dist/components/layout/Sidebar.d.ts +35 -0
  131. package/dist/components/layout/Sidebar.js +125 -0
  132. package/dist/components/modals/RelationCreateModal.d.ts +19 -0
  133. package/dist/components/modals/RelationCreateModal.js +57 -0
  134. package/dist/components/modals/ResourceFormModal.d.ts +37 -0
  135. package/dist/components/modals/ResourceFormModal.js +44 -0
  136. package/dist/components/modals/useResourceForm.d.ts +40 -0
  137. package/dist/components/modals/useResourceForm.js +138 -0
  138. package/dist/components/modals/view/RecordActions.d.ts +17 -0
  139. package/dist/components/modals/view/RecordActions.js +16 -0
  140. package/dist/components/modals/view/RecordDetails.d.ts +13 -0
  141. package/dist/components/modals/view/RecordDetails.js +29 -0
  142. package/dist/components/modals/view/RelationPanel.d.ts +18 -0
  143. package/dist/components/modals/view/RelationPanel.js +16 -0
  144. package/dist/components/modals/view/RelationTabs.d.ts +32 -0
  145. package/dist/components/modals/view/RelationTabs.js +42 -0
  146. package/dist/components/modals/view/useRecordView.d.ts +18 -0
  147. package/dist/components/modals/view/useRecordView.js +114 -0
  148. package/dist/components/pages/PageRenderer.d.ts +6 -0
  149. package/dist/components/pages/PageRenderer.js +107 -0
  150. package/dist/components/table/ColumnTogglePopup.d.ts +11 -0
  151. package/dist/components/table/ColumnTogglePopup.js +16 -0
  152. package/dist/components/table/GridCard.d.ts +21 -0
  153. package/dist/components/table/GridCard.js +30 -0
  154. package/dist/components/table/GridView.d.ts +23 -0
  155. package/dist/components/table/GridView.js +49 -0
  156. package/dist/components/table/LayoutToggle.d.ts +7 -0
  157. package/dist/components/table/LayoutToggle.js +9 -0
  158. package/dist/components/table/TableActionsDropdown.d.ts +13 -0
  159. package/dist/components/table/TableActionsDropdown.js +46 -0
  160. package/dist/components/table/TableBulkActions.d.ts +11 -0
  161. package/dist/components/table/TableBulkActions.js +21 -0
  162. package/dist/components/table/TableHeader.d.ts +14 -0
  163. package/dist/components/table/TableHeader.js +23 -0
  164. package/dist/components/table/TablePagination.d.ts +13 -0
  165. package/dist/components/table/TablePagination.js +55 -0
  166. package/dist/components/table/TableRow.d.ts +21 -0
  167. package/dist/components/table/TableRow.js +32 -0
  168. package/dist/components/table/TableSearchBar.d.ts +11 -0
  169. package/dist/components/table/TableSearchBar.js +12 -0
  170. package/dist/components/table/TableTabs.d.ts +14 -0
  171. package/dist/components/table/TableTabs.js +8 -0
  172. package/dist/components/ui/Badge.d.ts +6 -0
  173. package/dist/components/ui/Badge.js +12 -0
  174. package/dist/components/ui/Button.d.ts +22 -0
  175. package/dist/components/ui/Button.js +22 -0
  176. package/dist/components/ui/Card.d.ts +7 -0
  177. package/dist/components/ui/Card.js +5 -0
  178. package/dist/components/ui/ConfirmDialog.d.ts +19 -0
  179. package/dist/components/ui/ConfirmDialog.js +45 -0
  180. package/dist/components/ui/EmptyState.d.ts +9 -0
  181. package/dist/components/ui/EmptyState.js +6 -0
  182. package/dist/components/ui/ErrorAlert.d.ts +7 -0
  183. package/dist/components/ui/ErrorAlert.js +9 -0
  184. package/dist/components/ui/Input.d.ts +11 -0
  185. package/dist/components/ui/Input.js +10 -0
  186. package/dist/components/ui/Label.d.ts +5 -0
  187. package/dist/components/ui/Label.js +5 -0
  188. package/dist/components/ui/PillButton.d.ts +14 -0
  189. package/dist/components/ui/PillButton.js +19 -0
  190. package/dist/components/ui/Select.d.ts +7 -0
  191. package/dist/components/ui/Select.js +7 -0
  192. package/dist/components/ui/Spinner.d.ts +8 -0
  193. package/dist/components/ui/Spinner.js +14 -0
  194. package/dist/components/ui/Toast.d.ts +21 -0
  195. package/dist/components/ui/Toast.js +47 -0
  196. package/dist/components/ui/index.d.ts +24 -0
  197. package/dist/components/ui/index.js +12 -0
  198. package/dist/components/utils/HintDisplay.d.ts +11 -0
  199. package/dist/components/utils/HintDisplay.js +12 -0
  200. package/dist/components/utils/Icon.d.ts +22 -0
  201. package/dist/components/utils/Icon.js +22 -0
  202. package/dist/components/utils/MediaPreviewModal.d.ts +14 -0
  203. package/dist/components/utils/MediaPreviewModal.js +32 -0
  204. package/dist/components/utils/ViewFieldWrapper.d.ts +11 -0
  205. package/dist/components/utils/ViewFieldWrapper.js +9 -0
  206. package/dist/components/utils/layoutHelpers.d.ts +19 -0
  207. package/dist/components/utils/layoutHelpers.js +257 -0
  208. package/dist/components/widgets/ChartWidget.d.ts +16 -0
  209. package/dist/components/widgets/ChartWidget.js +192 -0
  210. package/dist/components/widgets/StatsWidget.d.ts +16 -0
  211. package/dist/components/widgets/StatsWidget.js +39 -0
  212. package/dist/components/widgets/WidgetRenderer.d.ts +10 -0
  213. package/dist/components/widgets/WidgetRenderer.js +50 -0
  214. package/dist/components/widgets/WidgetShell.d.ts +9 -0
  215. package/dist/components/widgets/WidgetShell.js +7 -0
  216. package/dist/contexts/AuthChallengeRegistryContext.d.ts +15 -0
  217. package/dist/contexts/AuthChallengeRegistryContext.js +15 -0
  218. package/dist/contexts/BlockRegistryContext.d.ts +18 -0
  219. package/dist/contexts/BlockRegistryContext.js +8 -0
  220. package/dist/contexts/ColumnRegistryContext.d.ts +8 -0
  221. package/dist/contexts/ColumnRegistryContext.js +30 -0
  222. package/dist/contexts/FieldRegistryContext.d.ts +13 -0
  223. package/dist/contexts/FieldRegistryContext.js +46 -0
  224. package/dist/contexts/PanelMetadataContext.d.ts +26 -0
  225. package/dist/contexts/PanelMetadataContext.js +26 -0
  226. package/dist/contexts/PanelProviders.d.ts +27 -0
  227. package/dist/contexts/PanelProviders.js +24 -0
  228. package/dist/contexts/ResourceModalContext.d.ts +26 -0
  229. package/dist/contexts/ResourceModalContext.js +76 -0
  230. package/dist/contexts/SlotRegistryContext.d.ts +19 -0
  231. package/dist/contexts/SlotRegistryContext.js +24 -0
  232. package/dist/contexts/TableRefreshContext.d.ts +10 -0
  233. package/dist/contexts/TableRefreshContext.js +30 -0
  234. package/dist/contexts/WidgetRegistryContext.d.ts +17 -0
  235. package/dist/contexts/WidgetRegistryContext.js +14 -0
  236. package/dist/contexts/createRegistryContext.d.ts +19 -0
  237. package/dist/contexts/createRegistryContext.js +20 -0
  238. package/dist/hooks/useAfterStateUpdated.d.ts +6 -0
  239. package/dist/hooks/useAfterStateUpdated.js +62 -0
  240. package/dist/hooks/useValidation.d.ts +26 -0
  241. package/dist/hooks/useValidation.js +76 -0
  242. package/dist/i18n/I18nProvider.d.ts +27 -0
  243. package/dist/i18n/I18nProvider.js +101 -0
  244. package/dist/i18n/LocaleSwitcher.d.ts +10 -0
  245. package/dist/i18n/LocaleSwitcher.js +30 -0
  246. package/dist/i18n/activeLocale.d.ts +11 -0
  247. package/dist/i18n/activeLocale.js +34 -0
  248. package/dist/i18n/buildClientI18n.d.ts +28 -0
  249. package/dist/i18n/buildClientI18n.js +67 -0
  250. package/dist/i18n/index.d.ts +11 -0
  251. package/dist/i18n/index.js +9 -0
  252. package/dist/i18n/locales/core/en.d.ts +225 -0
  253. package/dist/i18n/locales/core/en.js +252 -0
  254. package/dist/i18n/locales/core/index.d.ts +2 -0
  255. package/dist/i18n/locales/core/index.js +4 -0
  256. package/dist/i18n/locales/core/sq.d.ts +253 -0
  257. package/dist/i18n/locales/core/sq.js +255 -0
  258. package/dist/i18n/useFormatter.d.ts +18 -0
  259. package/dist/i18n/useFormatter.js +37 -0
  260. package/dist/i18n/useLocale.d.ts +11 -0
  261. package/dist/i18n/useLocale.js +11 -0
  262. package/dist/i18n/useTranslation.d.ts +12 -0
  263. package/dist/i18n/useTranslation.js +12 -0
  264. package/dist/index.d.ts +106 -0
  265. package/dist/index.js +101 -0
  266. package/dist/pages/ResourceListPage.d.ts +8 -0
  267. package/dist/pages/ResourceListPage.js +139 -0
  268. package/dist/plugin.d.ts +79 -0
  269. package/dist/plugin.js +34 -0
  270. package/dist/runtime/conditions.d.ts +35 -0
  271. package/dist/runtime/conditions.js +97 -0
  272. package/dist/runtime/formTraversal.d.ts +25 -0
  273. package/dist/runtime/formTraversal.js +37 -0
  274. package/dist/runtime/serializedFunctions.d.ts +41 -0
  275. package/dist/runtime/serializedFunctions.js +264 -0
  276. package/dist/slots/Slot.d.ts +24 -0
  277. package/dist/slots/Slot.js +29 -0
  278. package/dist/slots/SlotCluster.d.ts +22 -0
  279. package/dist/slots/SlotCluster.js +49 -0
  280. package/dist/slots/index.d.ts +7 -0
  281. package/dist/slots/index.js +4 -0
  282. package/dist/slots/mergeSlots.d.ts +18 -0
  283. package/dist/slots/mergeSlots.js +35 -0
  284. package/dist/slots/types.d.ts +87 -0
  285. package/dist/slots/types.js +30 -0
  286. package/dist/styles.css +1 -0
  287. package/dist/table/TableContext.d.ts +36 -0
  288. package/dist/table/TableContext.js +13 -0
  289. package/dist/table/TableRenderer.d.ts +29 -0
  290. package/dist/table/TableRenderer.js +159 -0
  291. package/dist/table/components/FiltersPanel.d.ts +11 -0
  292. package/dist/table/components/FiltersPanel.js +52 -0
  293. package/dist/table/components/TableToolbar.d.ts +28 -0
  294. package/dist/table/components/TableToolbar.js +27 -0
  295. package/dist/table/components/TableToolbarButton.d.ts +6 -0
  296. package/dist/table/components/TableToolbarButton.js +9 -0
  297. package/dist/table/components/TableView.d.ts +12 -0
  298. package/dist/table/components/TableView.js +21 -0
  299. package/dist/table/defaultRowActions.d.ts +21 -0
  300. package/dist/table/defaultRowActions.js +37 -0
  301. package/dist/table/hooks/useColumnVisibility.d.ts +13 -0
  302. package/dist/table/hooks/useColumnVisibility.js +59 -0
  303. package/dist/table/hooks/useEditableRows.d.ts +22 -0
  304. package/dist/table/hooks/useEditableRows.js +63 -0
  305. package/dist/table/hooks/useTableActions.d.ts +54 -0
  306. package/dist/table/hooks/useTableActions.js +313 -0
  307. package/dist/table/hooks/useTableData.d.ts +28 -0
  308. package/dist/table/hooks/useTableData.js +63 -0
  309. package/dist/table/hooks/useTableLayout.d.ts +12 -0
  310. package/dist/table/hooks/useTableLayout.js +31 -0
  311. package/dist/table/hooks/useTableQuery.d.ts +29 -0
  312. package/dist/table/hooks/useTableQuery.js +135 -0
  313. package/dist/types/index.d.ts +224 -0
  314. package/dist/types/index.js +6 -0
  315. package/dist/utils/classNames.d.ts +7 -0
  316. package/dist/utils/classNames.js +9 -0
  317. package/dist/utils/columnMediaDimensions.d.ts +13 -0
  318. package/dist/utils/columnMediaDimensions.js +29 -0
  319. package/dist/utils/columnVisibilityStorage.d.ts +22 -0
  320. package/dist/utils/columnVisibilityStorage.js +56 -0
  321. package/dist/utils/fieldErrors.d.ts +13 -0
  322. package/dist/utils/fieldErrors.js +25 -0
  323. package/dist/utils/formatValue.d.ts +28 -0
  324. package/dist/utils/formatValue.js +109 -0
  325. package/dist/utils/layoutStorage.d.ts +23 -0
  326. package/dist/utils/layoutStorage.js +53 -0
  327. package/dist/utils/redirectHandler.d.ts +7 -0
  328. package/dist/utils/redirectHandler.js +25 -0
  329. package/dist/utils/tableFormatters.d.ts +14 -0
  330. package/dist/utils/tableFormatters.js +93 -0
  331. package/dist/utils/widgetVisibilityStorage.d.ts +11 -0
  332. package/dist/utils/widgetVisibilityStorage.js +39 -0
  333. package/package.json +101 -0
  334. package/src/FieldRenderer.test.tsx +44 -0
  335. package/src/FieldRenderer.tsx +104 -0
  336. package/src/FormRenderer.containers.test.tsx +121 -0
  337. package/src/FormRenderer.test.tsx +174 -0
  338. package/src/FormRenderer.tsx +140 -0
  339. package/src/TableRenderer.tsx +2 -0
  340. package/src/api/actionsApi.ts +76 -0
  341. package/src/api/authenticatedFetch.ts +40 -0
  342. package/src/api/exportApi.ts +66 -0
  343. package/src/api/http.test.ts +58 -0
  344. package/src/api/http.ts +68 -0
  345. package/src/api/resourceApi.ts +88 -0
  346. package/src/api/tableApi.test.ts +108 -0
  347. package/src/api/tableApi.ts +107 -0
  348. package/src/api/urls.ts +50 -0
  349. package/src/app.test.tsx +67 -0
  350. package/src/app.tsx +181 -0
  351. package/src/auth/AuthContext.tsx +188 -0
  352. package/src/auth/LoginPage.tsx +380 -0
  353. package/src/auth/ProtectedRoute.tsx +39 -0
  354. package/src/auth/authApiClient.ts +109 -0
  355. package/src/auth/authFlow.test.tsx +168 -0
  356. package/src/auth/types.ts +104 -0
  357. package/src/components/ActionFormModal.tsx +45 -0
  358. package/src/components/AdminPanel.tsx +368 -0
  359. package/src/components/Checkbox.tsx +59 -0
  360. package/src/components/CheckboxField.tsx +88 -0
  361. package/src/components/ColorPickerField.tsx +93 -0
  362. package/src/components/DateTimePickerField.tsx +112 -0
  363. package/src/components/FileUploadField.tsx +841 -0
  364. package/src/components/GlobalSearch.tsx +436 -0
  365. package/src/components/GroupField.tsx +85 -0
  366. package/src/components/HiddenField.tsx +14 -0
  367. package/src/components/ModalBreadcrumb.tsx +74 -0
  368. package/src/components/ModalDrawer.tsx +137 -0
  369. package/src/components/RadioField.tsx +80 -0
  370. package/src/components/RepeaterField.tsx +546 -0
  371. package/src/components/ResourceModalRenderer.tsx +144 -0
  372. package/src/components/RichEditorField.tsx +942 -0
  373. package/src/components/SectionField.tsx +242 -0
  374. package/src/components/SelectField.tsx +843 -0
  375. package/src/components/TabsField.test.tsx +151 -0
  376. package/src/components/TabsField.tsx +386 -0
  377. package/src/components/TagsInputField.tsx +411 -0
  378. package/src/components/TextInputField.tsx +91 -0
  379. package/src/components/TextareaField.tsx +110 -0
  380. package/src/components/ToggleField.tsx +126 -0
  381. package/src/components/ViewModal.tsx +353 -0
  382. package/src/components/blocks/BlockRenderer.tsx +56 -0
  383. package/src/components/blocks/FormBlockRenderer.tsx +160 -0
  384. package/src/components/blocks/TableBlockRenderer.tsx +33 -0
  385. package/src/components/blocks/TabsBlockRenderer.tsx +49 -0
  386. package/src/components/blocks/WidgetBlockRenderer.tsx +19 -0
  387. package/src/components/columns/CheckboxColumnComponent.tsx +38 -0
  388. package/src/components/columns/ColorColumnComponent.tsx +23 -0
  389. package/src/components/columns/CustomColumn.test.tsx +55 -0
  390. package/src/components/columns/DeeplinkWrapper.tsx +103 -0
  391. package/src/components/columns/IconColumnComponent.tsx +55 -0
  392. package/src/components/columns/ImageColumnComponent.tsx +220 -0
  393. package/src/components/columns/MediaColumnComponent.tsx +294 -0
  394. package/src/components/columns/SelectColumnComponent.tsx +49 -0
  395. package/src/components/columns/TagsColumnComponent.tsx +46 -0
  396. package/src/components/columns/TextColumnComponent.tsx +191 -0
  397. package/src/components/columns/TextInputColumnComponent.tsx +35 -0
  398. package/src/components/columns/ToggleColumnComponent.tsx +56 -0
  399. package/src/components/columns/VideoColumnComponent.tsx +236 -0
  400. package/src/components/columns/ViewColumnComponent.tsx +9 -0
  401. package/src/components/errors/ErrorBoundary.tsx +58 -0
  402. package/src/components/filters/CustomFilterComponent.tsx +130 -0
  403. package/src/components/filters/DateFilterComponent.tsx +272 -0
  404. package/src/components/filters/QueryBuilderFilterComponent.tsx +502 -0
  405. package/src/components/layout/Header.tsx +212 -0
  406. package/src/components/layout/PanelBrandMark.tsx +61 -0
  407. package/src/components/layout/Sidebar.tsx +283 -0
  408. package/src/components/modals/RelationCreateModal.tsx +107 -0
  409. package/src/components/modals/ResourceFormModal.test.tsx +119 -0
  410. package/src/components/modals/ResourceFormModal.tsx +128 -0
  411. package/src/components/modals/useResourceForm.ts +207 -0
  412. package/src/components/modals/view/RecordActions.tsx +69 -0
  413. package/src/components/modals/view/RecordDetails.tsx +60 -0
  414. package/src/components/modals/view/RelationPanel.tsx +76 -0
  415. package/src/components/modals/view/RelationTabs.tsx +145 -0
  416. package/src/components/modals/view/useRecordView.ts +134 -0
  417. package/src/components/pages/PageRenderer.tsx +173 -0
  418. package/src/components/table/ColumnTogglePopup.tsx +85 -0
  419. package/src/components/table/GridCard.tsx +155 -0
  420. package/src/components/table/GridView.tsx +138 -0
  421. package/src/components/table/LayoutToggle.tsx +24 -0
  422. package/src/components/table/TableActionsDropdown.tsx +114 -0
  423. package/src/components/table/TableBulkActions.tsx +65 -0
  424. package/src/components/table/TableHeader.tsx +96 -0
  425. package/src/components/table/TablePagination.tsx +169 -0
  426. package/src/components/table/TableRow.tsx +155 -0
  427. package/src/components/table/TableSearchBar.tsx +66 -0
  428. package/src/components/table/TableTabs.tsx +49 -0
  429. package/src/components/ui/Badge.tsx +30 -0
  430. package/src/components/ui/Button.test.tsx +78 -0
  431. package/src/components/ui/Button.tsx +102 -0
  432. package/src/components/ui/Card.tsx +23 -0
  433. package/src/components/ui/ConfirmDialog.tsx +112 -0
  434. package/src/components/ui/EmptyState.tsx +24 -0
  435. package/src/components/ui/ErrorAlert.tsx +37 -0
  436. package/src/components/ui/Input.tsx +48 -0
  437. package/src/components/ui/Label.tsx +15 -0
  438. package/src/components/ui/PillButton.tsx +72 -0
  439. package/src/components/ui/Select.tsx +33 -0
  440. package/src/components/ui/Spinner.tsx +39 -0
  441. package/src/components/ui/Toast.tsx +105 -0
  442. package/src/components/ui/index.ts +24 -0
  443. package/src/components/utils/HintDisplay.tsx +26 -0
  444. package/src/components/utils/Icon.tsx +36 -0
  445. package/src/components/utils/MediaPreviewModal.tsx +114 -0
  446. package/src/components/utils/ViewFieldWrapper.tsx +23 -0
  447. package/src/components/utils/layoutHelpers.ts +267 -0
  448. package/src/components/widgets/ChartWidget.tsx +247 -0
  449. package/src/components/widgets/StatsWidget.tsx +72 -0
  450. package/src/components/widgets/WidgetRenderer.tsx +108 -0
  451. package/src/components/widgets/WidgetShell.tsx +37 -0
  452. package/src/contexts/AuthChallengeRegistryContext.tsx +29 -0
  453. package/src/contexts/BlockRegistryContext.tsx +28 -0
  454. package/src/contexts/ColumnRegistryContext.tsx +38 -0
  455. package/src/contexts/FieldRegistryContext.tsx +56 -0
  456. package/src/contexts/PanelMetadataContext.tsx +60 -0
  457. package/src/contexts/PanelProviders.tsx +85 -0
  458. package/src/contexts/ResourceModalContext.tsx +137 -0
  459. package/src/contexts/SlotRegistryContext.tsx +35 -0
  460. package/src/contexts/TableRefreshContext.tsx +44 -0
  461. package/src/contexts/WidgetRegistryContext.tsx +34 -0
  462. package/src/contexts/createRegistryContext.tsx +29 -0
  463. package/src/hooks/useAfterStateUpdated.ts +70 -0
  464. package/src/hooks/useValidation.test.ts +59 -0
  465. package/src/hooks/useValidation.ts +95 -0
  466. package/src/i18n/I18nProvider.tsx +128 -0
  467. package/src/i18n/LocaleSwitcher.tsx +50 -0
  468. package/src/i18n/activeLocale.ts +39 -0
  469. package/src/i18n/buildClientI18n.ts +101 -0
  470. package/src/i18n/i18n.test.tsx +140 -0
  471. package/src/i18n/index.ts +12 -0
  472. package/src/i18n/locales/core/en.ts +274 -0
  473. package/src/i18n/locales/core/index.ts +5 -0
  474. package/src/i18n/locales/core/sq.ts +275 -0
  475. package/src/i18n/useFormatter.ts +42 -0
  476. package/src/i18n/useLocale.ts +16 -0
  477. package/src/i18n/useTranslation.ts +17 -0
  478. package/src/index.ts +244 -0
  479. package/src/pages/ResourceListPage.tsx +205 -0
  480. package/src/plugin.ts +110 -0
  481. package/src/runtime/conditions.test.ts +99 -0
  482. package/src/runtime/conditions.ts +148 -0
  483. package/src/runtime/formTraversal.ts +41 -0
  484. package/src/runtime/serializedFunctions.test.ts +59 -0
  485. package/src/runtime/serializedFunctions.ts +284 -0
  486. package/src/slots/Slot.test.tsx +89 -0
  487. package/src/slots/Slot.tsx +47 -0
  488. package/src/slots/SlotCluster.test.tsx +95 -0
  489. package/src/slots/SlotCluster.tsx +107 -0
  490. package/src/slots/index.ts +15 -0
  491. package/src/slots/mergeSlots.test.ts +71 -0
  492. package/src/slots/mergeSlots.ts +40 -0
  493. package/src/slots/slotNames.test.ts +21 -0
  494. package/src/slots/types.ts +119 -0
  495. package/src/styles.css +437 -0
  496. package/src/table/TableContext.tsx +41 -0
  497. package/src/table/TableRenderer.test.tsx +197 -0
  498. package/src/table/TableRenderer.tsx +390 -0
  499. package/src/table/components/FiltersPanel.tsx +193 -0
  500. package/src/table/components/TableToolbar.tsx +153 -0
  501. package/src/table/components/TableToolbarButton.tsx +14 -0
  502. package/src/table/components/TableView.tsx +106 -0
  503. package/src/table/defaultRowActions.ts +43 -0
  504. package/src/table/hooks/useColumnVisibility.test.ts +51 -0
  505. package/src/table/hooks/useColumnVisibility.ts +71 -0
  506. package/src/table/hooks/useEditableRows.test.ts +69 -0
  507. package/src/table/hooks/useEditableRows.ts +89 -0
  508. package/src/table/hooks/useTableActions.ts +393 -0
  509. package/src/table/hooks/useTableData.ts +89 -0
  510. package/src/table/hooks/useTableLayout.ts +45 -0
  511. package/src/table/hooks/useTableQuery.test.ts +116 -0
  512. package/src/table/hooks/useTableQuery.ts +172 -0
  513. package/src/test/mockFetch.ts +67 -0
  514. package/src/test/setup.ts +25 -0
  515. package/src/types/index.ts +228 -0
  516. package/src/utils/classNames.ts +10 -0
  517. package/src/utils/columnMediaDimensions.ts +45 -0
  518. package/src/utils/columnVisibilityStorage.ts +55 -0
  519. package/src/utils/fieldErrors.test.ts +35 -0
  520. package/src/utils/fieldErrors.ts +27 -0
  521. package/src/utils/formatValue.test.tsx +65 -0
  522. package/src/utils/formatValue.tsx +117 -0
  523. package/src/utils/layoutStorage.ts +52 -0
  524. package/src/utils/redirectHandler.ts +29 -0
  525. package/src/utils/tableFormatters.test.ts +54 -0
  526. package/src/utils/tableFormatters.ts +104 -0
  527. package/src/utils/widgetVisibilityStorage.ts +38 -0
  528. package/tailwind.config.js +9 -0
  529. package/vite.config.ts +17 -0
@@ -0,0 +1,841 @@
1
+ import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
2
+ import { useFormContext } from 'react-hook-form';
3
+ import { getFieldError } from '../utils/fieldErrors';
4
+ import { useDropzone } from 'react-dropzone';
5
+ import { Check, Upload, File, Trash2, Loader2 } from 'lucide-react';
6
+ import { FieldProps } from '../types';
7
+ import { HintDisplay } from './utils/HintDisplay';
8
+ import { ViewFieldWrapper } from './utils/ViewFieldWrapper';
9
+ import { MediaPreviewModal } from './utils/MediaPreviewModal';
10
+ import { authenticatedFetch } from '../api/authenticatedFetch';
11
+ import { useValidation } from '../hooks/useValidation';
12
+ import { translate } from '../i18n/activeLocale';
13
+
14
+ /**
15
+ * Represents a file in the upload field
16
+ */
17
+ interface FileItem {
18
+ /** Unique identifier for this file item */
19
+ id: string;
20
+ /** Storage key (from server after upload) */
21
+ key?: string;
22
+ /** Storage adapter name */
23
+ bucket?: string;
24
+ /** Display URL (server URL or local blob) */
25
+ url?: string;
26
+ /** Local file object (for new uploads) */
27
+ file?: File;
28
+ /** Local blob URL for preview */
29
+ blobUrl?: string;
30
+ /** Upload status */
31
+ status: 'pending' | 'uploading' | 'uploaded' | 'existing' | 'error';
32
+ /** Error message if status is 'error' */
33
+ error?: string;
34
+ }
35
+
36
+ /**
37
+ * Server response for media upload
38
+ */
39
+ interface UploadResponse {
40
+ data: {
41
+ url: string;
42
+ key: string;
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Generate a unique ID for file tracking
48
+ */
49
+ const generateId = () => `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
50
+
51
+ /**
52
+ * Convert File to base64 string
53
+ */
54
+ const fileToBase64 = (file: File): Promise<string> => {
55
+ return new Promise((resolve, reject) => {
56
+ const reader = new FileReader();
57
+ reader.onload = () => {
58
+ const result = reader.result as string;
59
+ resolve(result.split(',')[1]);
60
+ };
61
+ reader.onerror = reject;
62
+ reader.readAsDataURL(file);
63
+ });
64
+ };
65
+
66
+ /**
67
+ * Format bytes to human readable size
68
+ */
69
+ const formatFileSize = (bytes: number): string => {
70
+ if (bytes === 0) return '0 Bytes';
71
+ const k = 1024;
72
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
73
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
74
+ return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;
75
+ };
76
+
77
+ /**
78
+ * Check if a URL points to an image
79
+ */
80
+ const isImageUrl = (url?: string): boolean => {
81
+ if (!url) return false;
82
+ if (url.startsWith('blob:')) return true;
83
+ return /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(url);
84
+ };
85
+
86
+ /**
87
+ * Check if a URL points to a video
88
+ */
89
+ const isVideoUrl = (url?: string): boolean => {
90
+ if (!url) return false;
91
+ return /\.(mp4|webm|ogg|mov|m4v)$/i.test(url);
92
+ };
93
+
94
+ /**
95
+ * Check if a URL points to an audio file
96
+ */
97
+ const isAudioUrl = (url?: string): boolean => {
98
+ if (!url) return false;
99
+ return /\.(mp3|wav|ogg|m4a)$/i.test(url);
100
+ };
101
+
102
+ type ViewMediaType = 'image' | 'video' | 'audio';
103
+
104
+ function FileUploadViewField({ label, value }: { label?: string; value: any }) {
105
+ const [previewOpen, setPreviewOpen] = useState(false);
106
+ const [previewUrl, setPreviewUrl] = useState<string | null>(null);
107
+ const [previewType, setPreviewType] = useState<ViewMediaType>('image');
108
+
109
+ const displayValue = value || null;
110
+ const filesArray = Array.isArray(displayValue) ? displayValue : displayValue ? [displayValue] : [];
111
+
112
+ if (filesArray.length === 0) {
113
+ return <ViewFieldWrapper label={label}>-</ViewFieldWrapper>;
114
+ }
115
+
116
+ const openPreview = (url: string, type: ViewMediaType) => {
117
+ setPreviewUrl(url);
118
+ setPreviewType(type);
119
+ setPreviewOpen(true);
120
+ };
121
+
122
+ return (
123
+ <ViewFieldWrapper label={label}>
124
+ <div className="flex flex-wrap gap-4">
125
+ {filesArray.map((file: any, index: number) => {
126
+ const fileUrl = typeof file === 'object' && file !== null ? file.url || file.key : file;
127
+ if (!fileUrl) {
128
+ return null;
129
+ }
130
+
131
+ const image = isImageUrl(fileUrl);
132
+ const video = isVideoUrl(fileUrl);
133
+
134
+ if (video) {
135
+ return (
136
+ <div key={index} className="relative">
137
+ <video
138
+ src={fileUrl}
139
+ controls
140
+ className="w-72 h-44 rounded-lg border border-border object-cover bg-black"
141
+ onClick={e => {
142
+ e.stopPropagation();
143
+ openPreview(fileUrl, 'video');
144
+ }}
145
+ />
146
+ </div>
147
+ );
148
+ }
149
+
150
+ if (image) {
151
+ return (
152
+ <div key={index} className="relative">
153
+ <img
154
+ src={fileUrl}
155
+ alt={`${label || translate('core:common.image')} ${index + 1}`}
156
+ className="w-40 h-40 object-cover rounded-lg border border-border cursor-pointer hover:opacity-80 transition-opacity"
157
+ onClick={() => openPreview(fileUrl, 'image')}
158
+ />
159
+ </div>
160
+ );
161
+ }
162
+
163
+ return (
164
+ <div
165
+ key={index}
166
+ className="flex items-center gap-2 p-2 border border-border rounded-lg bg-surface">
167
+ <File size={20} className="text-fg-secondary" />
168
+ <span className="text-sm text-fg">
169
+ {typeof file === 'object' && file !== null
170
+ ? file.key || translate('core:common.file')
171
+ : translate('core:common.file')}
172
+ </span>
173
+ </div>
174
+ );
175
+ })}
176
+ </div>
177
+
178
+ {previewUrl && (
179
+ <MediaPreviewModal
180
+ isOpen={previewOpen}
181
+ onClose={() => setPreviewOpen(false)}
182
+ mediaUrl={previewUrl}
183
+ mediaType={previewType}
184
+ title={label}
185
+ autoplay={previewType === 'video'}
186
+ controls
187
+ />
188
+ )}
189
+ </ViewFieldWrapper>
190
+ );
191
+ }
192
+
193
+ export function FileUploadField({
194
+ name,
195
+ label,
196
+ helperText,
197
+ hint,
198
+ hintIcon,
199
+ hintColor,
200
+ acceptedFileTypes = [],
201
+ maxSize,
202
+ minSize,
203
+ multiple = false,
204
+ maxFiles,
205
+ disabled,
206
+ required,
207
+ visibility,
208
+ directory,
209
+ bucket,
210
+ apiBaseUrl,
211
+ resource,
212
+ mode,
213
+ value,
214
+ validation,
215
+ operation,
216
+ }: FieldProps & {
217
+ visibility?: 'public' | 'private';
218
+ directory?: string;
219
+ bucket?: string;
220
+ apiBaseUrl?: string;
221
+ resource?: string;
222
+ }) {
223
+ // View mode: render formatted display (with image/video preview)
224
+ if (mode === 'view') {
225
+ return <FileUploadViewField label={label} value={value} />;
226
+ }
227
+
228
+ const {
229
+ setValue,
230
+ watch,
231
+ register,
232
+ formState: { errors: formErrors },
233
+ } = useFormContext();
234
+ const [files, setFiles] = useState<FileItem[]>([]);
235
+ const [errors, setErrors] = useState<string[]>([]);
236
+ const initializedRef = useRef(false);
237
+
238
+ // Preview state for edit mode
239
+ const [editPreviewOpen, setEditPreviewOpen] = useState(false);
240
+ const [editPreviewUrl, setEditPreviewUrl] = useState<string | null>(null);
241
+ const [editPreviewType, setEditPreviewType] = useState<ViewMediaType>('image');
242
+
243
+ const currentValue = watch(name);
244
+
245
+ // Evaluate validation conditions with form context
246
+ const validationResult = useValidation(validation?.rules || [], operation, name);
247
+ const isRequired = validationResult.required !== undefined;
248
+ const fieldError = getFieldError(formErrors, name);
249
+
250
+ // Register field with React Hook Form for validation
251
+ React.useEffect(() => {
252
+ register(name, validationResult);
253
+ }, [register, name, validationResult]);
254
+
255
+ // Compute endpoints
256
+ // Use generic media endpoint if no resource is provided, otherwise use resource-specific endpoint
257
+ const uploadEndpoint = useMemo(() => {
258
+ if (!apiBaseUrl) return null;
259
+ if (resource) {
260
+ return `${apiBaseUrl}/${resource}/media/upload`;
261
+ }
262
+ return `${apiBaseUrl}/media/upload`;
263
+ }, [apiBaseUrl, resource]);
264
+
265
+ const deleteEndpoint = useMemo(() => {
266
+ if (!apiBaseUrl) return null;
267
+ if (resource) {
268
+ return `${apiBaseUrl}/${resource}/media/delete`;
269
+ }
270
+ return `${apiBaseUrl}/media/delete`;
271
+ }, [apiBaseUrl, resource]);
272
+
273
+ /**
274
+ * Initialize from existing value (server sends { url, key } objects)
275
+ * Only runs once on mount if there's an initial value
276
+ */
277
+ useEffect(() => {
278
+ if (initializedRef.current) return;
279
+ if (!currentValue) return;
280
+
281
+ initializedRef.current = true;
282
+ const existingFiles: FileItem[] = [];
283
+ const keys: string[] = [];
284
+
285
+ const processItem = (item: any): FileItem | null => {
286
+ if (!item) return null;
287
+
288
+ // New format: { key, bucket, url }
289
+ if (typeof item === 'object' && item.key) {
290
+ keys.push(item.key);
291
+ return {
292
+ id: generateId(),
293
+ key: item.key,
294
+ bucket: item.bucket,
295
+ url: item.url || item.key, // Use url if available, fallback to key
296
+ status: 'existing',
297
+ };
298
+ }
299
+
300
+ // String form: treat the value as a storage key
301
+ if (typeof item === 'string') {
302
+ keys.push(item);
303
+ return {
304
+ id: generateId(),
305
+ key: item,
306
+ url: item,
307
+ status: 'existing',
308
+ };
309
+ }
310
+
311
+ return null;
312
+ };
313
+
314
+ if (Array.isArray(currentValue)) {
315
+ currentValue.forEach(item => {
316
+ const fileItem = processItem(item);
317
+ if (fileItem) existingFiles.push(fileItem);
318
+ });
319
+ } else {
320
+ const fileItem = processItem(currentValue);
321
+ if (fileItem) existingFiles.push(fileItem);
322
+ }
323
+
324
+ setFiles(existingFiles);
325
+ // Set form value to just keys (don't validate on initial load)
326
+ setValue(name, multiple ? keys : keys[0] || null, { shouldValidate: false });
327
+ // eslint-disable-next-line react-hooks/exhaustive-deps
328
+ }, []); // Only run once on mount
329
+
330
+ /**
331
+ * Upload a single file to the server
332
+ */
333
+ const uploadFile = useCallback(
334
+ async (file: File): Promise<UploadResponse | null> => {
335
+ if (!uploadEndpoint) {
336
+ console.warn('No upload endpoint configured');
337
+ return null;
338
+ }
339
+
340
+ try {
341
+ const base64 = await fileToBase64(file);
342
+ const response = await authenticatedFetch(
343
+ uploadEndpoint,
344
+ {
345
+ method: 'POST',
346
+ headers: { 'Content-Type': 'application/json' },
347
+ body: JSON.stringify({
348
+ file: base64,
349
+ filename: file.name,
350
+ contentType: file.type,
351
+ fieldName: name,
352
+ isArray: multiple,
353
+ path: directory,
354
+ visibility,
355
+ bucket, // Pass bucket adapter name
356
+ }),
357
+ },
358
+ apiBaseUrl,
359
+ );
360
+
361
+ if (!response.ok) {
362
+ const error = await response.json();
363
+ throw new Error(`Upload failed: ${error.message || response.statusText}`);
364
+ }
365
+
366
+ return await response.json();
367
+ } catch (error) {
368
+ console.error('Upload error:', error);
369
+ return null;
370
+ }
371
+ },
372
+ [uploadEndpoint, name, multiple, directory, visibility],
373
+ );
374
+
375
+ /**
376
+ * Delete a file from bucket
377
+ */
378
+ const deleteFromStorage = useCallback(
379
+ async (payload: string | { key: string; bucket?: string }): Promise<boolean> => {
380
+ if (!deleteEndpoint) return true;
381
+
382
+ try {
383
+ const body = typeof payload === 'string' ? { key: payload } : payload;
384
+ const response = await authenticatedFetch(
385
+ deleteEndpoint,
386
+ {
387
+ method: 'POST',
388
+ headers: { 'Content-Type': 'application/json' },
389
+ body: JSON.stringify(body),
390
+ },
391
+ apiBaseUrl,
392
+ );
393
+ return response.ok;
394
+ } catch (error) {
395
+ console.error('Delete error:', error);
396
+ return false;
397
+ }
398
+ },
399
+ [deleteEndpoint],
400
+ );
401
+
402
+ /**
403
+ * Update form value with current keys
404
+ * Backend will automatically detect deletions by comparing with existing record
405
+ */
406
+ const updateFormValue = useCallback(
407
+ (fileList: FileItem[]) => {
408
+ // Get keys from uploaded/existing files
409
+ const keys = fileList
410
+ .filter(f => f.key && (f.status === 'uploaded' || f.status === 'existing'))
411
+ .map(f => f.key!);
412
+
413
+ // Set main field value - backend will detect deletions automatically
414
+ // Trigger validation when value changes
415
+ if (multiple) {
416
+ setValue(name, keys.length > 0 ? keys : [], { shouldValidate: true });
417
+ } else {
418
+ setValue(name, keys[0] || null, { shouldValidate: true });
419
+ }
420
+ },
421
+ [multiple, name, setValue],
422
+ );
423
+
424
+ /**
425
+ * Handle file drop/selection
426
+ */
427
+ const onDrop = useCallback(
428
+ async (acceptedFiles: File[], rejectedFiles: any[]) => {
429
+ setErrors([]);
430
+
431
+ // Handle rejected files
432
+ const newErrors: string[] = [];
433
+ rejectedFiles.forEach(({ file, errors: fileErrors }) => {
434
+ fileErrors.forEach((error: any) => {
435
+ switch (error.code) {
436
+ case 'file-too-large':
437
+ newErrors.push(`${file.name}: File is too large`);
438
+ break;
439
+ case 'file-too-small':
440
+ newErrors.push(`${file.name}: File is too small`);
441
+ break;
442
+ case 'file-invalid-type':
443
+ newErrors.push(`${file.name}: Invalid file type`);
444
+ break;
445
+ case 'too-many-files':
446
+ newErrors.push(`Too many files. Maximum is ${maxFiles}`);
447
+ break;
448
+ default:
449
+ newErrors.push(`${file.name}: ${error.message}`);
450
+ }
451
+ });
452
+ });
453
+
454
+ if (newErrors.length > 0) {
455
+ setErrors(newErrors);
456
+ return;
457
+ }
458
+
459
+ if (acceptedFiles.length === 0) return;
460
+
461
+ // Create file items with pending status
462
+ const newFileItems: FileItem[] = acceptedFiles.map(file => ({
463
+ id: generateId(),
464
+ file,
465
+ blobUrl: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,
466
+ status: 'pending' as const,
467
+ }));
468
+
469
+ // Handle single file replacement
470
+ let updatedFiles: FileItem[];
471
+
472
+ if (!multiple) {
473
+ // Clean up existing file
474
+ const existingFile = files[0];
475
+ if (existingFile) {
476
+ // Revoke blob URL
477
+ if (existingFile.blobUrl) {
478
+ URL.revokeObjectURL(existingFile.blobUrl);
479
+ }
480
+
481
+ if (existingFile.key && existingFile.status === 'uploaded') {
482
+ // Delete immediately (not saved yet) - backend will handle existing files
483
+ const deletePayload = existingFile.bucket
484
+ ? { key: existingFile.key, bucket: existingFile.bucket }
485
+ : { key: existingFile.key };
486
+ deleteFromStorage(deletePayload);
487
+ }
488
+ // Note: For existing files, backend will detect deletion automatically
489
+ }
490
+ updatedFiles = newFileItems;
491
+ } else {
492
+ updatedFiles = [...files, ...newFileItems];
493
+ }
494
+
495
+ // Update state with pending files
496
+ setFiles(updatedFiles);
497
+
498
+ // Upload files
499
+ if (uploadEndpoint) {
500
+ // Mark files as uploading
501
+ setFiles(prev =>
502
+ prev.map(f =>
503
+ newFileItems.find(nf => nf.id === f.id) ? { ...f, status: 'uploading' as const } : f,
504
+ ),
505
+ );
506
+
507
+ // Upload all new files and collect results
508
+ const uploadResults: { id: string; result: UploadResponse | null }[] = [];
509
+ for (const fileItem of newFileItems) {
510
+ const result = await uploadFile(fileItem.file!);
511
+ uploadResults.push({ id: fileItem.id, result });
512
+ }
513
+
514
+ // Build the final file list
515
+ const finalFiles: FileItem[] = [];
516
+
517
+ // Start with existing/previously uploaded files (for multiple mode)
518
+ if (multiple) {
519
+ for (const f of updatedFiles) {
520
+ // Keep files that aren't part of this upload batch
521
+ if (!newFileItems.find(nf => nf.id === f.id)) {
522
+ finalFiles.push(f);
523
+ }
524
+ }
525
+ }
526
+
527
+ // Add newly uploaded files with their results
528
+ for (const fileItem of newFileItems) {
529
+ const uploadResult = uploadResults.find(r => r.id === fileItem.id);
530
+
531
+ if (uploadResult?.result?.data?.url && uploadResult?.result?.data?.key) {
532
+ // Revoke blob URL since we have server URL now
533
+ if (fileItem.blobUrl) {
534
+ URL.revokeObjectURL(fileItem.blobUrl);
535
+ }
536
+ // Create new object explicitly to avoid any issues with spread
537
+ // Extract bucket from formatted data if available
538
+ const bucketName = bucket;
539
+ finalFiles.push({
540
+ id: fileItem.id,
541
+ file: fileItem.file,
542
+ key: uploadResult.result.data.key,
543
+ bucket: bucketName,
544
+ url: uploadResult.result.data.url,
545
+ status: 'uploaded' as const,
546
+ });
547
+ } else {
548
+ finalFiles.push({
549
+ id: fileItem.id,
550
+ file: fileItem.file,
551
+ blobUrl: fileItem.blobUrl,
552
+ status: 'error' as const,
553
+ error: 'Upload failed',
554
+ });
555
+ }
556
+ }
557
+
558
+ // Update state and form value separately (not inside setFiles callback)
559
+ setFiles(finalFiles);
560
+ updateFormValue(finalFiles);
561
+ } else {
562
+ // No upload endpoint - just update state
563
+ setFiles(updatedFiles);
564
+ }
565
+ },
566
+ [files, multiple, maxFiles, uploadEndpoint, uploadFile, deleteFromStorage, updateFormValue],
567
+ );
568
+
569
+ /**
570
+ * Remove a file
571
+ * Backend will automatically detect deletions by comparing with existing record
572
+ */
573
+ const removeFile = useCallback(
574
+ async (fileId: string) => {
575
+ const fileToRemove = files.find(f => f.id === fileId);
576
+ if (!fileToRemove) return;
577
+
578
+ // Revoke blob URL
579
+ if (fileToRemove.blobUrl) {
580
+ URL.revokeObjectURL(fileToRemove.blobUrl);
581
+ }
582
+
583
+ if (fileToRemove.key && fileToRemove.status === 'uploaded') {
584
+ // Delete immediately (not saved to DB yet) - backend will handle existing files
585
+ const deletePayload = fileToRemove.bucket
586
+ ? { key: fileToRemove.key, bucket: fileToRemove.bucket }
587
+ : { key: fileToRemove.key };
588
+ const deleted = await deleteFromStorage(deletePayload);
589
+ if (!deleted) {
590
+ setErrors(['Failed to delete file from bucket']);
591
+ return;
592
+ }
593
+ }
594
+ // Note: For existing files, backend will detect deletion automatically
595
+
596
+ // Update files state
597
+ const updatedFiles = files.filter(f => f.id !== fileId);
598
+ setFiles(updatedFiles);
599
+
600
+ // Update form value - backend will detect deletions
601
+ updateFormValue(updatedFiles);
602
+ },
603
+ [files, deleteFromStorage, updateFormValue],
604
+ );
605
+
606
+ // Calculate upload limits
607
+ const validFileCount = files.filter(f => f.status !== 'error').length;
608
+ const maxAllowed = multiple ? maxFiles || Infinity : 1;
609
+ const remainingSlots = Math.max(0, maxAllowed - validFileCount);
610
+ const isAtLimit = remainingSlots <= 0;
611
+
612
+ // Dropzone config
613
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
614
+ onDrop,
615
+ accept:
616
+ acceptedFileTypes.length > 0
617
+ ? acceptedFileTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {})
618
+ : undefined,
619
+ maxSize: maxSize ? maxSize * 1024 : undefined,
620
+ minSize: minSize ? minSize * 1024 : undefined,
621
+ multiple,
622
+ maxFiles: remainingSlots,
623
+ disabled: disabled || isAtLimit,
624
+ });
625
+
626
+ // Get display URL for a file
627
+ const getDisplayUrl = (file: FileItem): string | undefined => file.url || file.blobUrl;
628
+
629
+ // Get display name for a file
630
+ const getDisplayName = (file: FileItem): string => {
631
+ if (file.file?.name) return file.file.name;
632
+ if (file.url) return file.url.split('/').pop() || 'File';
633
+ if (file.key) return file.key.split('/').pop() || 'File';
634
+ return 'File';
635
+ };
636
+
637
+ const openEditPreview = (url: string, type: ViewMediaType) => {
638
+ setEditPreviewUrl(url);
639
+ setEditPreviewType(type);
640
+ setEditPreviewOpen(true);
641
+ };
642
+
643
+ return (
644
+ <div className="space-y-2">
645
+ {/* Label */}
646
+ {label && (
647
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
648
+ {label}
649
+ {isRequired && <span className="text-red-500 dark:text-red-400 ml-1">*</span>}
650
+ </label>
651
+ )}
652
+
653
+ {/* Helper text */}
654
+ {helperText && <p className="text-sm text-gray-500 dark:text-gray-400">{helperText}</p>}
655
+
656
+ {/* Hint */}
657
+ <HintDisplay hint={hint} hintIcon={hintIcon} hintColor={hintColor} />
658
+
659
+ {/* Dropzone */}
660
+ {isAtLimit ? (
661
+ <div className="border-2 border-dashed rounded-lg p-6 text-center border-border bg-gray-50 dark:bg-gray-800/50">
662
+ <Check className="mx-auto h-12 w-12 text-green-500 dark:text-green-400" strokeWidth={1.5} />
663
+ <p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
664
+ {multiple ? (
665
+ <>
666
+ Maximum of <span className="font-semibold">{maxFiles}</span> files reached
667
+ </>
668
+ ) : (
669
+ <>{translate('core:file.uploaded')}</>
670
+ )}
671
+ </p>
672
+ <p className="mt-1 text-xs text-gray-500">
673
+ Remove {multiple ? 'a file' : 'the file'} to upload a new one
674
+ </p>
675
+ </div>
676
+ ) : (
677
+ <div
678
+ {...getRootProps()}
679
+ className={`
680
+ border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors
681
+ ${isDragActive ? 'border-accent bg-accent-soft dark:bg-accent-soft' : 'border-border hover:border-gray-400'}
682
+ ${disabled ? 'opacity-50 cursor-not-allowed' : ''}
683
+ `}>
684
+ <input {...getInputProps()} />
685
+ <Upload className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" strokeWidth={1.5} />
686
+ <p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
687
+ {isDragActive ? (
688
+ <span className="font-semibold text-accent">{translate('core:file.drop_here')}</span>
689
+ ) : (
690
+ <>
691
+ <span className="font-semibold text-accent">
692
+ {translate('core:file.click_to_upload')}
693
+ </span>{' '}
694
+ or drag and drop
695
+ </>
696
+ )}
697
+ </p>
698
+ <p className="mt-1 text-xs text-gray-500">
699
+ {acceptedFileTypes.length > 0 ? acceptedFileTypes.join(', ') : 'Any file type'}
700
+ {maxSize && ` • Max ${maxSize}KB`}
701
+ {multiple && maxFiles && ` • ${validFileCount}/${maxFiles} files`}
702
+ </p>
703
+ </div>
704
+ )}
705
+
706
+ {/* Errors */}
707
+ {(errors.length > 0 || fieldError) && (
708
+ <div className="space-y-1">
709
+ {errors.map((error, index) => (
710
+ <p key={index} className="text-sm text-red-600 dark:text-red-400">
711
+ {error}
712
+ </p>
713
+ ))}
714
+ {fieldError && (
715
+ <p className="text-sm text-red-600 dark:text-red-400">{fieldError.message as string}</p>
716
+ )}
717
+ </div>
718
+ )}
719
+
720
+ {/* File list */}
721
+ {files.length > 0 && (
722
+ <div className="space-y-2 mt-4">
723
+ {files.map(file => {
724
+ const displayUrl = getDisplayUrl(file);
725
+ const displayName = getDisplayName(file);
726
+ const isImage = (displayUrl && isImageUrl(displayUrl)) || file.file?.type?.startsWith('image/');
727
+ const isVideo = (displayUrl && isVideoUrl(displayUrl)) || file.file?.type?.startsWith('video/');
728
+ const isAudio = (displayUrl && isAudioUrl(displayUrl)) || file.file?.type?.startsWith('audio/');
729
+ const isLoading = file.status === 'pending' || file.status === 'uploading';
730
+ const hasError = file.status === 'error';
731
+
732
+ return (
733
+ <div
734
+ key={file.id}
735
+ className={`flex items-center justify-between p-3 bg-muted rounded-lg border border-border
736
+ ${isLoading ? 'opacity-60' : ''}
737
+ ${hasError ? 'border-red-500 dark:border-red-400' : ''}
738
+ `}>
739
+ <div className="flex items-center space-x-3 flex-1 min-w-0">
740
+ {/* Thumbnail */}
741
+ {displayUrl && (isImage || isVideo || isAudio) ? (
742
+ <button
743
+ type="button"
744
+ onClick={() =>
745
+ openEditPreview(
746
+ displayUrl,
747
+ isVideo ? 'video' : isAudio ? 'audio' : 'image',
748
+ )
749
+ }
750
+ className="h-14 w-14 rounded overflow-hidden flex items-center justify-center bg-muted hover:opacity-80 transition-opacity">
751
+ {isImage ? (
752
+ <img
753
+ src={displayUrl}
754
+ alt={displayName}
755
+ className="h-full w-full object-cover"
756
+ />
757
+ ) : isVideo ? (
758
+ <div className="h-full w-full flex items-center justify-center bg-black text-white text-xs">
759
+ <span className="px-1">{translate('core:file.video')}</span>
760
+ </div>
761
+ ) : (
762
+ <div className="h-full w-full flex items-center justify-center bg-linear-to-r from-blue-600 to-purple-600 text-white text-xs">
763
+ <span className="px-1">{translate('core:file.audio')}</span>
764
+ </div>
765
+ )}
766
+ </button>
767
+ ) : (
768
+ <div className="h-10 w-10 bg-muted rounded flex items-center justify-center">
769
+ <File className="h-6 w-6 text-gray-400 dark:text-gray-500" />
770
+ </div>
771
+ )}
772
+
773
+ {/* File info */}
774
+ <div className="flex-1 min-w-0">
775
+ <p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
776
+ {displayName}
777
+ </p>
778
+ {file.status === 'pending' && (
779
+ <p className="text-xs text-gray-500 flex items-center gap-1">
780
+ <Loader2 className="h-3 w-3 animate-spin" />
781
+ Waiting...
782
+ </p>
783
+ )}
784
+ {file.status === 'uploading' && (
785
+ <p className="text-xs text-accent flex items-center gap-1">
786
+ <Loader2 className="h-3 w-3 animate-spin" />
787
+ Uploading...
788
+ </p>
789
+ )}
790
+ {file.status === 'error' && (
791
+ <p className="text-xs text-red-500 dark:text-red-400">
792
+ {file.error || translate('core:common.error')}
793
+ </p>
794
+ )}
795
+ {file.status === 'uploaded' && (
796
+ <p className="text-xs text-green-500 dark:text-green-400">
797
+ {translate('core:file.saved')}
798
+ </p>
799
+ )}
800
+ {file.status === 'existing' && (
801
+ <p className="text-xs text-green-500 dark:text-green-400">
802
+ {translate('core:file.saved')}
803
+ </p>
804
+ )}
805
+ {file.file && file.status !== 'error' && (
806
+ <p className="text-xs text-gray-500 dark:text-gray-400">
807
+ {formatFileSize(file.file.size)}
808
+ </p>
809
+ )}
810
+ </div>
811
+ </div>
812
+
813
+ {/* Remove button */}
814
+ <button
815
+ type="button"
816
+ onClick={() => removeFile(file.id)}
817
+ className="ml-3 text-red-500 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 focus:outline-none"
818
+ disabled={disabled || isLoading}>
819
+ <Trash2 className="h-5 w-5" />
820
+ </button>
821
+ </div>
822
+ );
823
+ })}
824
+ </div>
825
+ )}
826
+
827
+ {/* Edit mode media preview */}
828
+ {editPreviewUrl && (
829
+ <MediaPreviewModal
830
+ isOpen={editPreviewOpen}
831
+ onClose={() => setEditPreviewOpen(false)}
832
+ mediaUrl={editPreviewUrl}
833
+ mediaType={editPreviewType}
834
+ title={typeof label === 'string' ? label : undefined}
835
+ autoplay={editPreviewType === 'video' || editPreviewType === 'audio'}
836
+ controls
837
+ />
838
+ )}
839
+ </div>
840
+ );
841
+ }