panda-core 0.12.5 → 1.1.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 (251) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +53 -0
  4. data/app/assets/builds/panda_core_admin.css +2 -0
  5. data/app/assets/tailwind/application.css +236 -36
  6. data/app/builders/panda/core/form_builder.rb +120 -18
  7. data/app/components/panda/core/UI/badge.html.erb +10 -0
  8. data/app/components/panda/core/UI/badge.rb +14 -30
  9. data/app/components/panda/core/UI/button.html.erb +5 -0
  10. data/app/components/panda/core/UI/button.rb +11 -15
  11. data/app/components/panda/core/UI/card.html.erb +3 -0
  12. data/app/components/panda/core/UI/card.rb +7 -6
  13. data/app/components/panda/core/admin/attribute_diff_component.html.erb +21 -0
  14. data/app/components/panda/core/admin/attribute_diff_component.rb +29 -0
  15. data/app/components/panda/core/admin/breadcrumb_component.html.erb +36 -0
  16. data/app/components/panda/core/admin/breadcrumb_component.rb +12 -64
  17. data/app/components/panda/core/admin/breadcrumbs_component.html.erb +16 -0
  18. data/app/components/panda/core/admin/breadcrumbs_component.rb +26 -0
  19. data/app/components/panda/core/admin/button_component.html.erb +9 -0
  20. data/app/components/panda/core/admin/button_component.rb +36 -31
  21. data/app/components/panda/core/admin/callout_component.html.erb +17 -0
  22. data/app/components/panda/core/admin/callout_component.rb +57 -0
  23. data/app/components/panda/core/admin/code_block_component.html.erb +9 -0
  24. data/app/components/panda/core/admin/code_block_component.rb +23 -0
  25. data/app/components/panda/core/admin/container_component.html.erb +27 -0
  26. data/app/components/panda/core/admin/container_component.rb +11 -77
  27. data/app/components/panda/core/admin/contributors_summary_component.html.erb +15 -0
  28. data/app/components/panda/core/admin/contributors_summary_component.rb +23 -0
  29. data/app/components/panda/core/admin/dashboard_default_content_component.html.erb +70 -0
  30. data/app/components/panda/core/admin/dashboard_default_content_component.rb +32 -0
  31. data/app/components/panda/core/admin/delete_button_component.html.erb +4 -0
  32. data/app/components/panda/core/admin/delete_button_component.rb +41 -0
  33. data/app/components/panda/core/admin/dropdown_button_component.html.erb +28 -0
  34. data/app/components/panda/core/admin/dropdown_button_component.rb +100 -0
  35. data/app/components/panda/core/admin/dropdown_component.html.erb +32 -0
  36. data/app/components/panda/core/admin/dropdown_component.rb +65 -0
  37. data/app/components/panda/core/admin/empty_state_component.html.erb +16 -0
  38. data/app/components/panda/core/admin/empty_state_component.rb +25 -0
  39. data/app/components/panda/core/admin/file_gallery_component.html.erb +58 -0
  40. data/app/components/panda/core/admin/file_gallery_component.rb +26 -98
  41. data/app/components/panda/core/admin/filter_pills_component.html.erb +6 -0
  42. data/app/components/panda/core/admin/filter_pills_component.rb +29 -0
  43. data/app/components/panda/core/admin/flash_message_component.html.erb +11 -0
  44. data/app/components/panda/core/admin/flash_message_component.rb +33 -60
  45. data/app/components/panda/core/admin/form_error_component.html.erb +7 -0
  46. data/app/components/panda/core/admin/form_error_component.rb +8 -16
  47. data/app/components/panda/core/admin/form_footer_component.html.erb +16 -0
  48. data/app/components/panda/core/admin/form_footer_component.rb +65 -0
  49. data/app/components/panda/core/admin/form_input_component.html.erb +1 -0
  50. data/app/components/panda/core/admin/form_input_component.rb +21 -13
  51. data/app/components/panda/core/admin/form_section_component.html.erb +16 -0
  52. data/app/components/panda/core/admin/form_section_component.rb +64 -0
  53. data/app/components/panda/core/admin/form_select_component.html.erb +5 -0
  54. data/app/components/panda/core/admin/form_select_component.rb +25 -26
  55. data/app/components/panda/core/admin/heading_component.html.erb +26 -0
  56. data/app/components/panda/core/admin/heading_component.rb +23 -60
  57. data/app/components/panda/core/admin/login_form_component.html.erb +38 -0
  58. data/app/components/panda/core/admin/login_form_component.rb +28 -0
  59. data/app/components/panda/core/admin/main_layout_component.html.erb +88 -0
  60. data/app/components/panda/core/admin/main_layout_component.rb +19 -0
  61. data/app/components/panda/core/admin/my_profile/connected_account_component.html.erb +41 -0
  62. data/app/components/panda/core/admin/my_profile/connected_account_component.rb +27 -0
  63. data/app/components/panda/core/admin/navigation/item_component.html.erb +34 -0
  64. data/app/components/panda/core/admin/navigation/item_component.rb +69 -0
  65. data/app/components/panda/core/admin/navigation/sub_item_component.html.erb +9 -0
  66. data/app/components/panda/core/admin/navigation/sub_item_component.rb +44 -0
  67. data/app/components/panda/core/admin/navigation/user_menu_component.html.erb +30 -0
  68. data/app/components/panda/core/admin/navigation/user_menu_component.rb +36 -0
  69. data/app/components/panda/core/admin/page_header_component.html.erb +28 -0
  70. data/app/components/panda/core/admin/page_header_component.rb +19 -35
  71. data/app/components/panda/core/admin/pagination_component.html.erb +33 -0
  72. data/app/components/panda/core/admin/pagination_component.rb +56 -0
  73. data/app/components/panda/core/admin/panel_component.html.erb +19 -0
  74. data/app/components/panda/core/admin/panel_component.rb +4 -29
  75. data/app/components/panda/core/admin/prose_panel_component.html.erb +6 -0
  76. data/app/components/panda/core/admin/prose_panel_component.rb +43 -0
  77. data/app/components/panda/core/admin/search_bar_component.html.erb +19 -0
  78. data/app/components/panda/core/admin/search_bar_component.rb +31 -0
  79. data/app/components/panda/core/admin/search_filter_bar_component.html.erb +22 -0
  80. data/app/components/panda/core/admin/search_filter_bar_component.rb +27 -0
  81. data/app/components/panda/core/admin/secret_field_component.html.erb +24 -0
  82. data/app/components/panda/core/admin/secret_field_component.rb +31 -0
  83. data/app/components/panda/core/admin/sidebar_component.html.erb +160 -0
  84. data/app/components/panda/core/admin/sidebar_component.rb +55 -0
  85. data/app/components/panda/core/admin/slideover_component.html.erb +67 -0
  86. data/app/components/panda/core/admin/slideover_component.rb +31 -70
  87. data/app/components/panda/core/admin/statistics_component.html.erb +4 -0
  88. data/app/components/panda/core/admin/statistics_component.rb +6 -8
  89. data/app/components/panda/core/admin/tab_bar_component.html.erb +48 -0
  90. data/app/components/panda/core/admin/tab_bar_component.rb +32 -67
  91. data/app/components/panda/core/admin/table_component.html.erb +82 -0
  92. data/app/components/panda/core/admin/table_component.rb +54 -87
  93. data/app/components/panda/core/admin/tag_badge_component.html.erb +7 -0
  94. data/app/components/panda/core/admin/tag_badge_component.rb +21 -0
  95. data/app/components/panda/core/admin/tag_component.html.erb +1 -0
  96. data/app/components/panda/core/admin/tag_component.rb +51 -20
  97. data/app/components/panda/core/admin/tag_input_component.html.erb +37 -0
  98. data/app/components/panda/core/admin/tag_input_component.rb +22 -0
  99. data/app/components/panda/core/admin/tags_panel_component.html.erb +8 -0
  100. data/app/components/panda/core/admin/tags_panel_component.rb +33 -0
  101. data/app/components/panda/core/admin/user_activity_component.html.erb +15 -0
  102. data/app/components/panda/core/admin/user_activity_component.rb +9 -22
  103. data/app/components/panda/core/admin/user_display_component.html.erb +8 -0
  104. data/app/components/panda/core/admin/user_display_component.rb +19 -22
  105. data/app/components/panda/core/base.rb +35 -42
  106. data/app/components/panda/core/shared/footer_component.html.erb +2 -0
  107. data/app/components/panda/core/shared/footer_component.rb +11 -0
  108. data/app/components/panda/core/shared/header_component.html.erb +32 -0
  109. data/app/components/panda/core/shared/header_component.rb +19 -0
  110. data/app/constraints/panda/core/admin_constraint.rb +2 -2
  111. data/app/controllers/concerns/panda/core/admin_authorization.rb +58 -0
  112. data/app/controllers/concerns/panda/core/authorizable.rb +107 -0
  113. data/app/controllers/panda/core/admin/base_controller.rb +62 -3
  114. data/app/controllers/panda/core/admin/editor/search_controller.rb +17 -0
  115. data/app/controllers/panda/core/admin/feature_flags_controller.rb +35 -0
  116. data/app/controllers/panda/core/admin/file_categories_controller.rb +84 -0
  117. data/app/controllers/panda/core/admin/files_controller.rb +230 -0
  118. data/app/controllers/panda/core/admin/import_sessions_controller.rb +120 -0
  119. data/app/controllers/panda/core/admin/my_profile/logins_controller.rb +24 -0
  120. data/app/controllers/panda/core/admin/my_profile_controller.rb +31 -0
  121. data/app/controllers/panda/core/admin/search_controller.rb +15 -0
  122. data/app/controllers/panda/core/admin/sessions_controller.rb +84 -10
  123. data/app/controllers/panda/core/admin/tags_controller.rb +77 -0
  124. data/app/controllers/panda/core/admin/test_sessions_controller.rb +5 -6
  125. data/app/controllers/panda/core/admin/users_controller.rb +184 -0
  126. data/app/controllers/panda/core/application_controller.rb +13 -1
  127. data/app/helpers/panda/core/active_storage_helper.rb +24 -0
  128. data/app/helpers/panda/core/feature_flag_helper.rb +11 -0
  129. data/app/helpers/panda/core/form_helper.rb +15 -0
  130. data/app/javascript/panda/core/controllers/avatar_upload_controller.js +73 -0
  131. data/app/javascript/panda/core/controllers/clipboard_controller.js +119 -0
  132. data/app/javascript/panda/core/controllers/collapsible_item_controller.js +45 -0
  133. data/app/javascript/panda/core/controllers/custom_select_controller.js +256 -0
  134. data/app/javascript/panda/core/controllers/datepicker_controller.js +121 -0
  135. data/app/javascript/panda/core/controllers/daterange_controller.js +193 -0
  136. data/app/javascript/panda/core/controllers/file_gallery_controller.js +134 -0
  137. data/app/javascript/panda/core/controllers/file_upload_controller.js +165 -0
  138. data/app/javascript/panda/core/controllers/global_search_controller.js +184 -0
  139. data/app/javascript/panda/core/controllers/index.js +36 -0
  140. data/app/javascript/panda/core/controllers/mobile_sidebar_controller.js +58 -0
  141. data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +49 -24
  142. data/app/javascript/panda/core/controllers/row_link_controller.js +27 -0
  143. data/app/javascript/panda/core/controllers/tag_input_controller.js +183 -0
  144. data/app/javascript/panda/core/tailwindcss-stimulus-components.js +2 -3
  145. data/app/javascript/panda/core/vendor/@fortawesome--fontawesome-free@7.2.0.js +8129 -0
  146. data/app/javascript/panda/core/vendor/@hotwired--turbo.js +7197 -100
  147. data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +511 -2
  148. data/app/javascript/panda/core/vendor/@tailwindplus--elements@1.0.22.js +65 -0
  149. data/app/javascript/panda/core/vendor/cropperjs@2.1.0.js +3 -0
  150. data/app/javascript/panda/core/vendor/vanilla-calendar-pro.js +2 -0
  151. data/app/models/concerns/panda/core/has_metadata.rb +164 -0
  152. data/app/models/concerns/panda/core/importable.rb +79 -0
  153. data/app/models/concerns/panda/core/searchable.rb +66 -0
  154. data/app/models/concerns/panda/core/taggable.rb +37 -0
  155. data/app/models/panda/core/feature_flag.rb +56 -0
  156. data/app/models/panda/core/file_categorization.rb +14 -0
  157. data/app/models/panda/core/file_category.rb +45 -0
  158. data/app/models/panda/core/import_session.rb +79 -0
  159. data/app/models/panda/core/presence.rb +24 -0
  160. data/app/models/panda/core/tag.rb +33 -0
  161. data/app/models/panda/core/tagging.rb +14 -0
  162. data/app/models/panda/core/user.rb +94 -16
  163. data/app/models/panda/core/user_activity.rb +42 -0
  164. data/app/models/panda/core/user_session.rb +35 -0
  165. data/app/services/panda/core/attach_avatar_service.rb +15 -3
  166. data/app/services/panda/core/csv_import_service.rb +65 -0
  167. data/app/services/panda/core/file_categorizer.rb +52 -0
  168. data/app/services/panda/core/file_parser.rb +76 -0
  169. data/app/services/panda/core/invite_user_service.rb +44 -0
  170. data/app/services/panda/core/presence_service.rb +49 -0
  171. data/app/views/layouts/panda/core/admin.html.erb +6 -76
  172. data/app/views/layouts/panda/core/admin_simple.html.erb +2 -2
  173. data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -73
  174. data/app/views/panda/core/admin/dashboard/show.html.erb +12 -15
  175. data/app/views/panda/core/admin/feature_flags/index.html.erb +56 -0
  176. data/app/views/panda/core/admin/file_categories/_form.html.erb +20 -0
  177. data/app/views/panda/core/admin/file_categories/edit.html.erb +5 -0
  178. data/app/views/panda/core/admin/file_categories/index.html.erb +55 -0
  179. data/app/views/panda/core/admin/file_categories/new.html.erb +5 -0
  180. data/app/views/panda/core/admin/files/_file_details.html.erb +156 -0
  181. data/app/views/panda/core/admin/files/index.html.erb +61 -0
  182. data/app/views/panda/core/admin/import_sessions/column_map.html.erb +43 -0
  183. data/app/views/panda/core/admin/import_sessions/index.html.erb +48 -0
  184. data/app/views/panda/core/admin/import_sessions/new.html.erb +37 -0
  185. data/app/views/panda/core/admin/import_sessions/preview.html.erb +51 -0
  186. data/app/views/panda/core/admin/import_sessions/show.html.erb +87 -0
  187. data/app/views/panda/core/admin/my_profile/edit.html.erb +51 -39
  188. data/app/views/panda/core/admin/my_profile/logins/show.html.erb +72 -0
  189. data/app/views/panda/core/admin/sessions/new.html.erb +1 -37
  190. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +1 -18
  191. data/app/views/panda/core/admin/shared/_sidebar.html.erb +1 -99
  192. data/app/views/panda/core/admin/shared/_slideover.html.erb +3 -3
  193. data/app/views/panda/core/admin/tags/_form.html.erb +21 -0
  194. data/app/views/panda/core/admin/tags/edit.html.erb +4 -0
  195. data/app/views/panda/core/admin/tags/index.html.erb +41 -0
  196. data/app/views/panda/core/admin/tags/new.html.erb +4 -0
  197. data/app/views/panda/core/admin/users/activity.html.erb +90 -0
  198. data/app/views/panda/core/admin/users/edit.html.erb +48 -0
  199. data/app/views/panda/core/admin/users/index.html.erb +161 -0
  200. data/app/views/panda/core/admin/users/sessions.html.erb +103 -0
  201. data/app/views/panda/core/admin/users/show.html.erb +224 -0
  202. data/app/views/panda/core/shared/_footer.html.erb +1 -2
  203. data/app/views/panda/core/shared/_header.html.erb +4 -22
  204. data/config/brakeman.ignore +68 -31
  205. data/config/importmap.rb +13 -11
  206. data/config/routes.rb +49 -2
  207. data/db/migrate/20260202171614_create_panda_core_file_categories.rb +30 -0
  208. data/db/migrate/20260203000001_add_user_management_fields_to_panda_core_users.rb +21 -0
  209. data/db/migrate/20260203000002_create_panda_core_user_activities.rb +24 -0
  210. data/db/migrate/20260203000003_create_panda_core_user_sessions.rb +25 -0
  211. data/db/migrate/20260207100000_create_panda_core_presences.rb +21 -0
  212. data/db/migrate/20260213000001_fix_active_storage_attachments_record_id_type.rb +44 -0
  213. data/db/migrate/20260217000001_create_panda_core_feature_flags.rb +14 -0
  214. data/db/migrate/20260303000001_add_metadata_to_panda_core_users.rb +11 -0
  215. data/db/migrate/20260310000001_create_panda_core_tags.rb +27 -0
  216. data/db/migrate/20260310000002_create_panda_core_import_sessions.rb +29 -0
  217. data/lib/generators/panda/core/install_generator.rb +32 -0
  218. data/lib/generators/panda/core/templates/panda.rb +63 -0
  219. data/lib/panda/core/asset_loader.rb +5 -2
  220. data/lib/panda/core/authentication.rb +3 -3
  221. data/lib/panda/core/configuration.rb +195 -1
  222. data/lib/panda/core/engine/admin_controller_config.rb +3 -6
  223. data/lib/panda/core/engine/autoload_config.rb +21 -9
  224. data/lib/panda/core/engine/omniauth_config.rb +108 -38
  225. data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
  226. data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
  227. data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
  228. data/lib/panda/core/engine.rb +90 -25
  229. data/lib/panda/core/middleware.rb +35 -0
  230. data/lib/panda/core/navigation_registry.rb +455 -0
  231. data/lib/panda/core/oauth_providers.rb +27 -0
  232. data/lib/panda/core/permission_registry.rb +89 -0
  233. data/lib/panda/core/search_registry.rb +69 -0
  234. data/lib/panda/core/seeds/file_categories.rb +30 -0
  235. data/lib/panda/core/shared/inflections_config.rb +1 -5
  236. data/lib/panda/core/testing/rails_helper.rb +87 -0
  237. data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
  238. data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
  239. data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
  240. data/lib/panda/core/version.rb +1 -1
  241. data/lib/panda/core/widget_registry.rb +50 -0
  242. data/lib/panda/core.rb +17 -8
  243. data/lib/tasks/panda/core/file_categories.rake +41 -0
  244. data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
  245. data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
  246. data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
  247. data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
  248. data/public/panda-core-assets/panda-core.css +2 -2
  249. data/public/panda-core-assets/vanilla-calendar.css +47 -0
  250. metadata +203 -26
  251. data/lib/panda/core/engine/inflections_config.rb +0 -23
@@ -1,73 +1 @@
1
- <div class="mt-5">
2
- <div class="p-6 bg-white shadow rounded-lg">
3
- <h2 class="text-lg font-medium text-gray-900">Welcome to Panda Admin</h2>
4
- <p class="mt-1 text-sm text-gray-500">Manage your application from this central dashboard.</p>
5
- </div>
6
-
7
- <div class="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
8
- <% if defined?(Panda::CMS) %>
9
- <div class="bg-white overflow-hidden shadow rounded-lg">
10
- <div class="px-4 py-5 sm:p-6">
11
- <div class="flex items-center">
12
- <div class="flex-shrink-0">
13
- <i class="fa-solid fa-file-lines text-3xl text-gray-400"></i>
14
- </div>
15
- <div class="ml-5 w-0 flex-1">
16
- <dt class="text-sm font-medium text-gray-500 truncate">Content Management</dt>
17
- <dd class="mt-1 text-lg font-semibold text-gray-900">
18
- <%= link_to "Manage CMS", panda_cms.admin_cms_dashboard_path, class: "text-primary-600 hover:text-primary-800" %>
19
- </dd>
20
- </div>
21
- </div>
22
- <div class="mt-3">
23
- <p class="text-sm text-gray-500">Pages, posts, menus, and content blocks</p>
24
- </div>
25
- </div>
26
- </div>
27
- <% end %>
28
-
29
- <div class="bg-white overflow-hidden shadow rounded-lg">
30
- <div class="px-4 py-5 sm:p-6">
31
- <div class="flex items-center">
32
- <div class="flex-shrink-0">
33
- <i class="fa-regular fa-user text-3xl text-gray-400"></i>
34
- </div>
35
- <div class="ml-5 w-0 flex-1">
36
- <dt class="text-sm font-medium text-gray-500 truncate">My Profile</dt>
37
- <dd class="mt-1 text-lg font-semibold text-gray-900">
38
- <%= link_to "Edit Profile", edit_admin_my_profile_path, class: "text-primary-600 hover:text-primary-800" %>
39
- </dd>
40
- </div>
41
- </div>
42
- <div class="mt-3">
43
- <p class="text-sm text-gray-500">Update your personal information</p>
44
- </div>
45
- </div>
46
- </div>
47
-
48
- <%# Hook for additional dashboard cards %>
49
- <% if Panda::Core.config.respond_to?(:admin_dashboard_cards) %>
50
- <% cards = Panda::Core.config.admin_dashboard_cards&.call(current_user) %>
51
- <% cards&.each do |card| %>
52
- <div class="bg-white overflow-hidden shadow rounded-lg">
53
- <div class="px-4 py-5 sm:p-6">
54
- <div class="flex items-center">
55
- <div class="flex-shrink-0">
56
- <i class="<%= card[:icon] %> text-3xl text-gray-400"></i>
57
- </div>
58
- <div class="ml-5 w-0 flex-1">
59
- <dt class="text-sm font-medium text-gray-500 truncate"><%= card[:title] %></dt>
60
- <dd class="mt-1 text-lg font-semibold text-gray-900">
61
- <%= link_to card[:link_text], card[:path], class: "text-primary-600 hover:text-primary-800" %>
62
- </dd>
63
- </div>
64
- </div>
65
- <div class="mt-3">
66
- <p class="text-sm text-gray-500"><%= card[:description] %></p>
67
- </div>
68
- </div>
69
- </div>
70
- <% end %>
71
- <% end %>
72
- </div>
73
- </div>
1
+ <%= render Panda::Core::Admin::DashboardDefaultContentComponent.new(user: current_user) %>
@@ -1,21 +1,18 @@
1
1
  <div class="" data-controller="dashboard">
2
- <%= render Panda::Core::Admin::ContainerComponent.new do |container| %>
3
- <% container.heading(text: "Dashboard", level: 1) %>
4
-
5
- <%# Hook for dashboard widgets %>
6
- <% if Panda::Core.config.admin_dashboard_widgets %>
7
- <% widgets = Panda::Core.config.admin_dashboard_widgets.call(current_user) %>
8
- <% if widgets && widgets.any? %>
9
- <div class="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-3">
10
- <% widgets.each do |widget| %>
11
- <%= render widget %>
12
- <% end %>
13
- </div>
14
- <% else %>
15
- <%= render "panda/core/admin/dashboard/default_content" %>
16
- <% end %>
2
+ <% container = Panda::Core::Admin::ContainerComponent.new %>
3
+ <% container.with_heading_slot(text: "Dashboard", level: 1) %>
4
+ <% container.with_body_slot do %>
5
+ <%# Dashboard widgets from WidgetRegistry (includes legacy lambda + registered widgets) %>
6
+ <% widgets = Panda::Core::WidgetRegistry.build(current_user) %>
7
+ <% if widgets.any? %>
8
+ <div class="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-3">
9
+ <% widgets.each do |widget| %>
10
+ <%= render widget %>
11
+ <% end %>
12
+ </div>
17
13
  <% else %>
18
14
  <%= render "panda/core/admin/dashboard/default_content" %>
19
15
  <% end %>
20
16
  <% end %>
17
+ <%= render container %>
21
18
  </div>
@@ -0,0 +1,56 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "Feature Flags", level: 1) %>
3
+
4
+ <% if @feature_flags.any? %>
5
+ <% @grouped_flags.each do |namespace, flags| %>
6
+ <div class="mb-8">
7
+ <h2 class="text-lg font-semibold text-gray-700 mb-3"><%= namespace.titleize %></h2>
8
+
9
+ <%= render Panda::Core::Admin::TableComponent.new(term: "feature flag", rows: flags, icon: "fa-solid fa-toggle-on") do |table| %>
10
+ <% table.column("Flag", width: "35%") do |flag| %>
11
+ <div>
12
+ <div class="font-medium text-gray-900"><%= flag.key %></div>
13
+ <% if flag.description.present? %>
14
+ <div class="text-sm text-gray-500"><%= flag.description %></div>
15
+ <% end %>
16
+ </div>
17
+ <% end %>
18
+
19
+ <% table.column("Status", width: "20%") do |flag| %>
20
+ <% if flag.enabled? %>
21
+ <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
22
+ <i class="fa-solid fa-check mr-1"></i> Enabled
23
+ </span>
24
+ <% else %>
25
+ <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">
26
+ <i class="fa-solid fa-xmark mr-1"></i> Disabled
27
+ </span>
28
+ <% end %>
29
+ <% end %>
30
+
31
+ <% table.column("Last Updated", width: "20%") do |flag| %>
32
+ <span class="text-sm text-gray-500"><%= flag.updated_at.strftime("%d %b %Y %H:%M") %></span>
33
+ <% end %>
34
+
35
+ <% table.column("", width: "25%") do |flag| %>
36
+ <div class="flex justify-end">
37
+ <%= button_to admin_feature_flag_path(flag), method: :patch, class: "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition #{flag.enabled? ? 'text-red-600 hover:text-red-700 hover:bg-red-50' : 'text-green-600 hover:text-green-700 hover:bg-green-50'}" do %>
38
+ <% if flag.enabled? %>
39
+ <i class="fa-solid fa-toggle-on"></i> Disable
40
+ <% else %>
41
+ <i class="fa-solid fa-toggle-off"></i> Enable
42
+ <% end %>
43
+ <% end %>
44
+ </div>
45
+ <% end %>
46
+ <% end %>
47
+ </div>
48
+ <% end %>
49
+ <% else %>
50
+ <div class="text-center py-12">
51
+ <i class="fa-solid fa-toggle-off text-4xl text-gray-300 mb-3"></i>
52
+ <p class="text-gray-500">No feature flags registered.</p>
53
+ <p class="text-sm text-gray-400 mt-1">Feature flags are registered by application code during initialization.</p>
54
+ </div>
55
+ <% end %>
56
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <%= panda_form_with model: file_category,
2
+ url: file_category.persisted? ? admin_file_category_path(file_category) : admin_file_categories_path,
3
+ method: file_category.persisted? ? :patch : :post,
4
+ local: true do |f| %>
5
+ <%= render Panda::Core::Admin::FormErrorComponent.new(model: file_category) %>
6
+
7
+ <%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
8
+ <% panel.with_heading_slot { "Category Settings" } %>
9
+ <div class="space-y-4">
10
+ <%= f.text_field :name, placeholder: "e.g. Project Assets" %>
11
+ <%= f.text_field :icon, label: "Icon (FontAwesome class)", placeholder: "e.g. fa-solid fa-folder", meta: "Use fa-solid or fab icon classes only." %>
12
+ <%= f.number_field :position, label: "Display Order", value: file_category.position || 100 %>
13
+ <%= f.collection_select :parent_id, Panda::Core::FileCategory.roots.ordered.where.not(id: file_category.id), :id, :name, { include_blank: "None (top-level)", label: "Parent Category" } %>
14
+ </div>
15
+ <% end %>
16
+
17
+ <%= render Panda::Core::Admin::FormFooterComponent.new(
18
+ submit_text: file_category.persisted? ? "Update Category" : "Create Category"
19
+ ) %>
20
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "Edit File Category: #{@file_category.name}", level: 1) %>
3
+
4
+ <%= render partial: "form", locals: { file_category: @file_category } %>
5
+ <% end %>
@@ -0,0 +1,55 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "File Categories", level: 1) do |heading| %>
3
+ <% heading.with_button(action: :add, text: "Add Category", href: new_admin_file_category_path) %>
4
+ <% end %>
5
+
6
+ <% if @file_categories.any? %>
7
+ <%= render Panda::Core::Admin::TableComponent.new(term: "file category", rows: @file_categories, icon: "fa-solid fa-folder") do |table| %>
8
+ <% table.column("Category", width: "40%") do |category| %>
9
+ <div class="flex items-center gap-3">
10
+ <% if category.icon.present? %>
11
+ <i class="<%= category.icon %> text-gray-400"></i>
12
+ <% else %>
13
+ <i class="fa-solid fa-folder text-gray-400"></i>
14
+ <% end %>
15
+ <div>
16
+ <div class="font-medium text-gray-900"><%= category.name %></div>
17
+ <div class="text-sm text-gray-500"><%= category.slug %></div>
18
+ </div>
19
+ </div>
20
+ <% end %>
21
+ <% table.column("Type", width: "15%") do |category| %>
22
+ <% if category.system? %>
23
+ <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">
24
+ System
25
+ </span>
26
+ <% else %>
27
+ <span class="inline-flex items-center rounded-md bg-primary-50 px-2 py-1 text-xs font-medium text-primary-700 ring-1 ring-inset ring-primary-600/20">
28
+ Custom
29
+ </span>
30
+ <% end %>
31
+ <% end %>
32
+ <% table.column("Files", width: "15%") do |category| %>
33
+ <span class="text-sm text-gray-500"><%= category.file_categorizations.count %></span>
34
+ <% end %>
35
+ <% table.column("", width: "30%") do |category| %>
36
+ <div class="flex justify-end gap-2">
37
+ <% unless category.system? %>
38
+ <%= link_to edit_admin_file_category_path(category), class: "text-sm text-primary-600 hover:text-primary-700" do %>
39
+ <i class="fa-solid fa-pencil"></i> Edit
40
+ <% end %>
41
+ <%= button_to admin_file_category_path(category), method: :delete, class: "text-sm text-red-600 hover:text-red-700", form: { data: { turbo_confirm: "Are you sure you want to delete this category? Files will become uncategorized." } } do %>
42
+ <i class="fa-solid fa-trash"></i> Delete
43
+ <% end %>
44
+ <% end %>
45
+ </div>
46
+ <% end %>
47
+ <% end %>
48
+ <% else %>
49
+ <div class="text-center py-12">
50
+ <i class="fa-solid fa-folder text-4xl text-gray-300 mb-3"></i>
51
+ <p class="text-gray-500">No file categories found.</p>
52
+ <p class="text-sm text-gray-400 mt-1">Run <code>rails panda:core:file_categories:seed</code> to create default categories.</p>
53
+ </div>
54
+ <% end %>
55
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "New File Category", level: 1) %>
3
+
4
+ <%= render partial: "form", locals: { file_category: @file_category } %>
5
+ <% end %>
@@ -0,0 +1,156 @@
1
+ <%# Flash messages %>
2
+ <% if local_assigns[:notice] %>
3
+ <div class="rounded-lg bg-green-50 dark:bg-green-900/20 p-3">
4
+ <div class="flex items-center gap-2">
5
+ <i class="fa-solid fa-check-circle text-green-500"></i>
6
+ <p class="text-sm font-medium text-green-800 dark:text-green-200"><%= notice %></p>
7
+ </div>
8
+ </div>
9
+ <% end %>
10
+ <% if local_assigns[:error] %>
11
+ <div class="rounded-lg bg-red-50 dark:bg-red-900/20 p-3">
12
+ <div class="flex items-center gap-2">
13
+ <i class="fa-solid fa-exclamation-circle text-red-500"></i>
14
+ <p class="text-sm font-medium text-red-800 dark:text-red-200"><%= error %></p>
15
+ </div>
16
+ </div>
17
+ <% end %>
18
+
19
+ <%# Image/file preview %>
20
+ <div class="block overflow-hidden w-full rounded-lg aspect-h-7 aspect-w-10">
21
+ <% if file.image? %>
22
+ <%= image_tag main_app.rails_blob_path(file, only_path: true), alt: file.filename.to_s, class: "object-cover", onerror: "this.onerror=null;this.style.display='none';this.nextElementSibling.classList.remove('hidden')" %>
23
+ <div class="hidden flex items-center justify-center h-full bg-gray-100 dark:bg-white/5" role="img" aria-label="Image preview not available">
24
+ <i class="fa-solid fa-file-image text-4xl text-gray-400"></i>
25
+ </div>
26
+ <% else %>
27
+ <div class="flex items-center justify-center h-full bg-gray-100 dark:bg-white/5">
28
+ <div class="text-center">
29
+ <i class="fa-solid fa-file text-4xl text-gray-400"></i>
30
+ <p class="mt-1 text-xs text-gray-500 uppercase"><%= file.content_type&.split("/")&.last || "file" %></p>
31
+ </div>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+
36
+ <%# Edit form %>
37
+ <% ext = File.extname(file.filename.to_s) %>
38
+ <% base_name = File.basename(file.filename.to_s, ext) %>
39
+ <% current_categorization = Panda::Core::FileCategorization.find_by(blob_id: file.id) %>
40
+
41
+ <%= panda_form_with url: admin_file_path(file), method: :patch, scope: :blob, data: { turbo: true } do |f| %>
42
+ <%# Filename with read-only extension suffix %>
43
+ <div class="panda-core-field-container mb-4">
44
+ <label for="blob_filename" class="block text-sm/6 font-medium text-gray-900 dark:text-gray-100">Filename</label>
45
+ <div class="flex">
46
+ <input type="text" name="blob[filename]" id="blob_filename" value="<%= base_name %>" autocomplete="off"
47
+ class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-primary-500 <%= "rounded-r-none" if ext.present? %>">
48
+ <% if ext.present? %>
49
+ <span class="inline-flex items-center rounded-r-md border border-l-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500 dark:border-white/10 dark:bg-white/10 dark:text-gray-400 whitespace-nowrap"><%= ext %></span>
50
+ <% end %>
51
+ </div>
52
+ </div>
53
+
54
+ <%= f.select :file_category_id,
55
+ options_for_select(
56
+ file_categories.map { |c| [c.name, c.id] },
57
+ current_categorization&.file_category_id
58
+ ),
59
+ { include_blank: "No category", label: "Category" } %>
60
+
61
+ <%= f.text_area :description, value: file.metadata["description"].to_s, label: "Description" %>
62
+
63
+ <%= render Panda::Core::Admin::FormFooterComponent.new(submit_text: "Save") %>
64
+ <% end %>
65
+
66
+ <%# Information %>
67
+ <div class="mt-2">
68
+ <h3 class="font-medium text-gray-900 dark:text-gray-100">Information</h3>
69
+ <dl class="mt-2 border-t border-b border-gray-200 dark:border-white/10 divide-y divide-gray-200 dark:divide-white/10">
70
+ <div class="flex justify-between py-3 text-sm">
71
+ <dt class="font-medium text-gray-500 dark:text-gray-400">Created</dt>
72
+ <dd class="text-gray-900 dark:text-gray-100 whitespace-nowrap"><%= file.created_at.strftime("%B %-d, %Y") %></dd>
73
+ </div>
74
+ <% if file.metadata["uploaded_by_name"].present? %>
75
+ <div class="flex justify-between py-3 text-sm">
76
+ <dt class="font-medium text-gray-500 dark:text-gray-400">Uploaded by</dt>
77
+ <dd class="text-gray-900 dark:text-gray-100 whitespace-nowrap"><%= file.metadata["uploaded_by_name"] %></dd>
78
+ </div>
79
+ <% end %>
80
+ </dl>
81
+ </div>
82
+
83
+ <%# Helper to format content type as human-readable label %>
84
+ <%
85
+ friendly_content_type = lambda do |ct|
86
+ subtype = ct.to_s.split("/").last&.upcase
87
+ case ct.to_s
88
+ when /\Aimage\// then "#{subtype == "JPEG" ? "JPG" : subtype} Image"
89
+ when /\Avideo\// then "#{subtype} Video"
90
+ when /\Aaudio\// then "#{subtype} Audio"
91
+ when "application/pdf" then "PDF Document"
92
+ else subtype || ct.to_s
93
+ end
94
+ end
95
+ %>
96
+
97
+ <%# Versions (images only — always show original, plus any variants) %>
98
+ <% if file.image? %>
99
+ <% variant_blobs = file.variant_records.includes(image_attachment: :blob).filter_map { |vr| vr.image&.blob } %>
100
+ <% variant_blobs.sort_by! { |b| [-(b.metadata["width"].to_i * b.metadata["height"].to_i), -b.byte_size] } %>
101
+ <div class="mt-4">
102
+ <h3 class="font-medium text-gray-900 dark:text-gray-100">Versions</h3>
103
+ <div class="mt-2 space-y-3">
104
+ <%# Original %>
105
+ <div class="border border-gray-200 dark:border-white/10 rounded-lg p-3">
106
+ <div class="flex items-center justify-between">
107
+ <div>
108
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Original</p>
109
+ <p class="text-xs text-gray-500 dark:text-gray-400">
110
+ <% if file.metadata["width"] && file.metadata["height"] %>
111
+ <%= file.metadata["width"] %>&times;<%= file.metadata["height"] %> &middot;
112
+ <% end %>
113
+ <%= number_to_human_size(file.byte_size) %> &middot;
114
+ <%= friendly_content_type.call(file.content_type) %>
115
+ </p>
116
+ </div>
117
+ <%= link_to main_app.rails_blob_path(file, disposition: "attachment"), class: "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" do %>
118
+ <i class="fa-solid fa-download text-sm"></i>
119
+ <% end %>
120
+ </div>
121
+ </div>
122
+ <%# Variant records (sorted by dimensions desc) %>
123
+ <% variant_blobs.each do |variant_blob| %>
124
+ <div class="border border-gray-200 dark:border-white/10 rounded-lg p-3">
125
+ <div class="flex items-center justify-between">
126
+ <div>
127
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Variant</p>
128
+ <p class="text-xs text-gray-500 dark:text-gray-400">
129
+ <% if variant_blob.metadata["width"] && variant_blob.metadata["height"] %>
130
+ <%= variant_blob.metadata["width"] %>&times;<%= variant_blob.metadata["height"] %> &middot;
131
+ <% end %>
132
+ <%= number_to_human_size(variant_blob.byte_size) %> &middot;
133
+ <%= friendly_content_type.call(variant_blob.content_type) %>
134
+ </p>
135
+ </div>
136
+ <%= link_to main_app.rails_blob_path(variant_blob, disposition: "attachment"), class: "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" do %>
137
+ <i class="fa-solid fa-download text-sm"></i>
138
+ <% end %>
139
+ </div>
140
+ </div>
141
+ <% end %>
142
+ </div>
143
+ </div>
144
+ <% end %>
145
+
146
+ <%# Action buttons %>
147
+ <div class="flex gap-x-3 mt-4">
148
+ <%= link_to main_app.rails_blob_path(file, disposition: "attachment"), class: "flex-1 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-xl shadow-sm hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 text-center dark:bg-white dark:text-gray-900 dark:hover:bg-gray-200" do %>
149
+ <i class="fa-solid fa-download mr-1"></i> Download Original
150
+ <% end %>
151
+ <%= button_to admin_file_path(file), method: :delete, class: "flex-1", form: { data: { turbo_confirm: "Are you sure you want to delete this file?" } } do %>
152
+ <span class="block w-full py-2 px-3 text-sm font-medium text-red-700 bg-white rounded-xl border border-red-200 shadow-sm hover:bg-red-50 text-center cursor-pointer dark:bg-transparent dark:text-red-400 dark:border-red-800 dark:hover:bg-red-950">
153
+ <i class="fa-solid fa-trash mr-1"></i> Delete
154
+ </span>
155
+ <% end %>
156
+ </div>
@@ -0,0 +1,61 @@
1
+ <div data-controller="file-gallery" data-file-gallery-files-path-value="<%= admin_files_path %>">
2
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
3
+ <% component.with_heading_slot(text: "Files", level: 1) do |heading| %>
4
+ <% heading.with_button(text: "Upload", icon: "upload", action: :add, as_button: true, data: { action: "click->file-gallery#openUpload" }) %>
5
+ <% end %>
6
+
7
+ <% component.with_slideover_slot(title: "File Details") do %>
8
+ <div class="px-4 sm:px-6 pt-4 pb-16 space-y-6" id="file-gallery-slideover-content">
9
+ <p class="text-sm text-gray-500">Select a file to view details</p>
10
+ </div>
11
+ <% end %>
12
+
13
+ <% if @file_categories.any? %>
14
+ <% filter_items = @file_categories.map { |c| { label: c.name, value: c.slug } } %>
15
+ <% filter_items << { label: "Uncategorized", value: "uncategorized" } %>
16
+ <%= render Panda::Core::Admin::FilterPillsComponent.new(
17
+ items: filter_items,
18
+ url_helper: ->(value) { value ? admin_files_path(category: value) : admin_files_path },
19
+ active_value: @active_category
20
+ ) %>
21
+ <% end %>
22
+
23
+ <div class="pb-24">
24
+ <%= render Panda::Core::Admin::FileGalleryComponent.new(files: @files, blob_categories: @blob_categories) %>
25
+ </div>
26
+ <% end %>
27
+
28
+ <%# Upload slideover - managed by file-gallery controller %>
29
+ <div class="hidden fixed inset-0 bg-gray-500/75 transition-opacity duration-500 ease-in-out dark:bg-gray-900/50 z-40"
30
+ data-file-gallery-target="uploadBackdrop"
31
+ data-action="click->file-gallery#closeUpload"></div>
32
+
33
+ <div class="hidden ml-auto block size-full max-w-md transform fixed right-0 top-0 h-full z-50"
34
+ data-file-gallery-target="uploadPanel">
35
+ <div class="relative flex h-full flex-col bg-white shadow-xl dark:bg-gray-800">
36
+ <div class="bg-gradient-admin px-4 py-6 sm:px-6">
37
+ <div class="flex items-center justify-between">
38
+ <h2 class="text-base font-semibold text-white">Upload File</h2>
39
+ <button type="button" data-action="click->file-gallery#closeUpload"
40
+ class="inline-flex items-center gap-1 rounded-full bg-white/10 px-2 py-1 text-xs text-white/80 hover:text-white cursor-pointer">
41
+ <span>Close</span>
42
+ <i class="fa-solid fa-xmark"></i>
43
+ </button>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="flex-1 overflow-y-auto">
48
+ <div class="px-4 sm:px-6 pt-4 pb-16 space-y-6">
49
+ <%= panda_form_with url: admin_files_path, scope: :file_upload do |f| %>
50
+ <%= f.file_field :file, label: "File", accept: "*/*", file_types_display: "All file types", max_size: "50MB" %>
51
+ <%= f.select :file_category_id,
52
+ options_for_select(@file_categories.map { |c| [c.name, c.id] }),
53
+ { label: "Category", prompt: "Select a category" },
54
+ { required: true } %>
55
+ <%= render Panda::Core::Admin::FormFooterComponent.new(submit_text: "Upload") %>
56
+ <% end %>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
@@ -0,0 +1,43 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "Map Columns", level: 1) %>
3
+
4
+ <%= render Panda::Core::Admin::PanelComponent.new do %>
5
+ <p class="text-sm text-gray-600 mb-6">
6
+ Match each column from your file to a field in your data. Leave unmatched columns as "Skip" to ignore them.
7
+ </p>
8
+
9
+ <%= form_tag update_mapping_admin_import_session_path(@import_session), method: :patch, class: "space-y-4" do %>
10
+ <div class="divide-y divide-gray-100">
11
+ <% @headers.each do |header| %>
12
+ <div class="flex items-center gap-4 py-3">
13
+ <div class="w-1/3">
14
+ <span class="text-sm font-medium text-gray-900"><%= header %></span>
15
+ </div>
16
+ <div class="flex items-center text-gray-400">
17
+ <i class="fa-solid fa-arrow-right"></i>
18
+ </div>
19
+ <div class="flex-1">
20
+ <select name="mapping[<%= header %>]"
21
+ class="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-1 focus:ring-primary">
22
+ <option value="">— Skip —</option>
23
+ <% @field_options.each do |field| %>
24
+ <% auto_match = header.parameterize(separator: "_") == field.name.to_s %>
25
+ <option value="<%= field.name %>" <%= "selected" if auto_match %>>
26
+ <%= field.label %><%= " (required)" if field.required %>
27
+ </option>
28
+ <% end %>
29
+ </select>
30
+ </div>
31
+ </div>
32
+ <% end %>
33
+ </div>
34
+
35
+ <div class="flex items-center gap-3 pt-4">
36
+ <button type="submit" class="inline-flex items-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/90 transition cursor-pointer">
37
+ <i class="fa-solid fa-eye mr-2"></i> Preview Import
38
+ </button>
39
+ <a href="javascript:history.back()" class="text-sm text-gray-500 hover:text-gray-700">Back</a>
40
+ </div>
41
+ <% end %>
42
+ <% end %>
43
+ <% end %>
@@ -0,0 +1,48 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "Import History", level: 1) %>
3
+
4
+ <% if @import_sessions.any? %>
5
+ <%= render Panda::Core::Admin::TableComponent.new(term: "import", rows: @import_sessions, icon: "fa-solid fa-file-import") do |table| %>
6
+ <% table.column("Type", width: "20%") do |session| %>
7
+ <span class="font-medium"><%= session.importable_type %></span>
8
+ <% end %>
9
+
10
+ <% table.column("Status", width: "15%") do |session| %>
11
+ <% case session.status %>
12
+ <% when "complete" %>
13
+ <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">Complete</span>
14
+ <% when "failed" %>
15
+ <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/20">Failed</span>
16
+ <% when "importing" %>
17
+ <span class="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-600/20">Importing</span>
18
+ <% else %>
19
+ <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10"><%= session.status.capitalize %></span>
20
+ <% end %>
21
+ <% end %>
22
+
23
+ <% table.column("Records", width: "15%") do |session| %>
24
+ <span class="text-sm text-gray-500"><%= session.imported_count %> / <%= session.total_rows %></span>
25
+ <% end %>
26
+
27
+ <% table.column("By", width: "20%") do |session| %>
28
+ <span class="text-sm text-gray-500"><%= session.user.name %></span>
29
+ <% end %>
30
+
31
+ <% table.column("Date", width: "20%") do |session| %>
32
+ <span class="text-sm text-gray-500"><%= session.created_at.strftime("%d %b %Y %H:%M") %></span>
33
+ <% end %>
34
+
35
+ <% table.column("", width: "10%") do |session| %>
36
+ <%= link_to admin_import_session_path(session), class: "text-sm text-primary hover:text-primary/80" do %>
37
+ View <i class="fa-solid fa-arrow-right ml-1"></i>
38
+ <% end %>
39
+ <% end %>
40
+ <% end %>
41
+ <% else %>
42
+ <%= render Panda::Core::Admin::EmptyStateComponent.new(
43
+ title: "No imports yet",
44
+ description: "Imports will appear here once you've imported data.",
45
+ icon: "fa-solid fa-file-import"
46
+ ) %>
47
+ <% end %>
48
+ <% end %>
@@ -0,0 +1,37 @@
1
+ <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading_slot(text: "Import #{@importable_type.pluralize}", level: 1) %>
3
+
4
+ <%= render Panda::Core::Admin::PanelComponent.new do %>
5
+ <%= form_tag admin_import_sessions_path, multipart: true, class: "space-y-6" do %>
6
+ <%= hidden_field_tag :importable_type, @importable_type %>
7
+
8
+ <% if flash[:error] %>
9
+ <div class="rounded-lg bg-red-50 p-4 mb-4">
10
+ <div class="flex items-center gap-2">
11
+ <i class="fa-solid fa-circle-exclamation text-red-600"></i>
12
+ <p class="text-sm font-medium text-red-800"><%= flash[:error] %></p>
13
+ </div>
14
+ </div>
15
+ <% end %>
16
+
17
+ <div>
18
+ <label class="block text-sm font-medium text-gray-700 mb-2">Upload file</label>
19
+ <input type="file" name="import_file" accept=".csv,.tsv,.xlsx,.xls" required
20
+ class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4
21
+ file:rounded-lg file:border-0 file:text-sm file:font-medium
22
+ file:bg-primary/10 file:text-primary hover:file:bg-primary/20
23
+ file:cursor-pointer">
24
+ <p class="mt-2 text-sm text-gray-500">
25
+ Supported formats: CSV, TSV, or XLSX. The first row should contain column headers.
26
+ </p>
27
+ </div>
28
+
29
+ <div class="flex items-center gap-3">
30
+ <button type="submit" class="inline-flex items-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/90 transition cursor-pointer">
31
+ <i class="fa-solid fa-upload mr-2"></i> Upload & Continue
32
+ </button>
33
+ <a href="javascript:history.back()" class="text-sm text-gray-500 hover:text-gray-700">Cancel</a>
34
+ </div>
35
+ <% end %>
36
+ <% end %>
37
+ <% end %>