uchi 0.1.2 → 0.1.4

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 (276) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +222 -0
  3. data/app/assets/config/uchi_manifest.js +2 -0
  4. data/app/assets/javascripts/uchi/application.js +6095 -0
  5. data/app/assets/javascripts/uchi.js +4 -0
  6. data/app/assets/stylesheets/uchi/application.css +3971 -0
  7. data/app/assets/stylesheets/uchi/uchi.css +17 -0
  8. data/app/assets/tailwind/uchi.css +21 -0
  9. data/app/components/flowbite/breadcrumb.rb +33 -0
  10. data/app/components/flowbite/breadcrumb_home.rb +26 -0
  11. data/app/components/flowbite/breadcrumb_item/current.rb +33 -0
  12. data/app/components/flowbite/breadcrumb_item/first.rb +35 -0
  13. data/app/components/flowbite/breadcrumb_item.rb +48 -0
  14. data/app/components/flowbite/breadcrumb_separator.rb +30 -0
  15. data/app/components/flowbite/button/outline.rb +22 -0
  16. data/app/components/flowbite/button/pill.rb +40 -0
  17. data/app/components/flowbite/button.rb +92 -0
  18. data/app/components/flowbite/card.rb +45 -0
  19. data/app/components/flowbite/input/checkbox.rb +73 -0
  20. data/app/components/flowbite/input/date.rb +11 -0
  21. data/app/components/flowbite/input/date_time.rb +11 -0
  22. data/app/components/flowbite/input/email.rb +12 -0
  23. data/app/components/flowbite/input/field.rb +117 -0
  24. data/app/components/flowbite/input/file.rb +30 -0
  25. data/app/components/flowbite/input/hint.rb +57 -0
  26. data/app/components/flowbite/input/label.rb +82 -0
  27. data/app/components/flowbite/input/number.rb +11 -0
  28. data/app/components/flowbite/input/password.rb +11 -0
  29. data/app/components/flowbite/input/phone.rb +11 -0
  30. data/app/components/flowbite/input/radio_button.rb +50 -0
  31. data/app/components/flowbite/input/select.rb +49 -0
  32. data/app/components/flowbite/input/textarea.rb +42 -0
  33. data/app/components/flowbite/input/url.rb +12 -0
  34. data/app/components/flowbite/input/validation_error.rb +11 -0
  35. data/app/components/flowbite/input_field/checkbox.html.erb +14 -0
  36. data/app/components/flowbite/input_field/checkbox.rb +54 -0
  37. data/app/components/flowbite/input_field/date.rb +13 -0
  38. data/app/components/flowbite/input_field/date_time.rb +13 -0
  39. data/app/components/flowbite/input_field/email.rb +13 -0
  40. data/app/components/flowbite/input_field/file.rb +13 -0
  41. data/app/components/flowbite/input_field/input_field.html.erb +8 -0
  42. data/app/components/flowbite/input_field/number.rb +13 -0
  43. data/app/components/flowbite/input_field/password.rb +13 -0
  44. data/app/components/flowbite/input_field/phone.rb +13 -0
  45. data/app/components/flowbite/input_field/radio_button.html.erb +14 -0
  46. data/app/components/flowbite/input_field/radio_button.rb +86 -0
  47. data/app/components/flowbite/input_field/select.rb +31 -0
  48. data/app/components/flowbite/input_field/text.rb +8 -0
  49. data/app/components/flowbite/input_field/textarea.rb +13 -0
  50. data/app/components/flowbite/input_field/url.rb +13 -0
  51. data/app/components/flowbite/input_field.rb +192 -0
  52. data/app/components/flowbite/link.rb +21 -0
  53. data/app/components/flowbite/style.rb +13 -0
  54. data/app/components/flowbite/toast/icon.html.erb +5 -0
  55. data/app/components/flowbite/toast/icon.rb +57 -0
  56. data/app/components/flowbite/toast/toast.html.erb +11 -0
  57. data/app/components/flowbite/toast.rb +34 -0
  58. data/app/components/uchi/field/base.rb +57 -0
  59. data/app/components/uchi/field/belongs_to/edit.html.erb +1 -0
  60. data/app/components/uchi/field/belongs_to/index.html.erb +1 -0
  61. data/app/components/uchi/field/belongs_to/show.html.erb +3 -0
  62. data/app/components/uchi/field/belongs_to.rb +105 -0
  63. data/app/components/uchi/field/blank/edit.html.erb +1 -0
  64. data/app/components/uchi/field/blank/index.html.erb +1 -0
  65. data/app/components/uchi/field/blank/show.html.erb +1 -0
  66. data/app/components/uchi/field/blank.rb +16 -0
  67. data/app/components/uchi/field/boolean/edit.html.erb +1 -0
  68. data/app/components/uchi/field/boolean/index.html.erb +9 -0
  69. data/app/components/uchi/field/boolean/show.html.erb +9 -0
  70. data/app/components/uchi/field/boolean.rb +27 -0
  71. data/app/components/uchi/field/date/edit.html.erb +1 -0
  72. data/app/components/uchi/field/date/index.html.erb +1 -0
  73. data/app/components/uchi/field/date/show.html.erb +1 -0
  74. data/app/components/uchi/field/date.rb +27 -0
  75. data/app/components/uchi/field/date_time/edit.html.erb +1 -0
  76. data/app/components/uchi/field/date_time/index.html.erb +1 -0
  77. data/app/components/uchi/field/date_time/show.html.erb +1 -0
  78. data/app/components/uchi/field/date_time.rb +27 -0
  79. data/app/components/uchi/field/file/edit.html.erb +1 -0
  80. data/app/components/uchi/field/file/index.html.erb +6 -0
  81. data/app/components/uchi/field/file/show.html.erb +8 -0
  82. data/app/components/uchi/field/file.rb +37 -0
  83. data/app/components/uchi/field/has_and_belongs_to_many/edit.html.erb +9 -0
  84. data/app/components/uchi/field/has_and_belongs_to_many/index.html.erb +1 -0
  85. data/app/components/uchi/field/has_and_belongs_to_many/show.html.erb +28 -0
  86. data/app/components/uchi/field/has_and_belongs_to_many.rb +131 -0
  87. data/app/components/uchi/field/has_many/edit.html.erb +1 -0
  88. data/app/components/uchi/field/has_many/index.html.erb +1 -0
  89. data/app/components/uchi/field/has_many/show.html.erb +28 -0
  90. data/app/components/uchi/field/has_many.rb +107 -0
  91. data/app/components/uchi/field/id/index.html.erb +4 -0
  92. data/app/components/uchi/field/id/show.html.erb +4 -0
  93. data/app/components/uchi/field/id.rb +26 -0
  94. data/app/components/uchi/field/image/edit.html.erb +1 -0
  95. data/app/components/uchi/field/image/index.html.erb +6 -0
  96. data/app/components/uchi/field/image/show.html.erb +6 -0
  97. data/app/components/uchi/field/image.rb +38 -0
  98. data/app/components/uchi/field/number/edit.html.erb +1 -0
  99. data/app/components/uchi/field/number/index.html.erb +1 -0
  100. data/app/components/uchi/field/number/show.html.erb +1 -0
  101. data/app/components/uchi/field/number.rb +32 -0
  102. data/app/components/uchi/field/string/edit.html.erb +1 -0
  103. data/app/components/uchi/field/string/index.html.erb +1 -0
  104. data/app/components/uchi/field/string/show.html.erb +1 -0
  105. data/app/components/uchi/field/string.rb +33 -0
  106. data/app/components/uchi/field/text/edit.html.erb +1 -0
  107. data/app/components/uchi/field/text/index.html.erb +1 -0
  108. data/app/components/uchi/field/text/show.html.erb +1 -0
  109. data/app/components/uchi/field/text.rb +38 -0
  110. data/app/components/uchi/ui/breadcrumb/breadcrumb.html.erb +13 -0
  111. data/app/components/uchi/ui/breadcrumb.rb +14 -0
  112. data/app/components/uchi/ui/form/footer/footer.html.erb +5 -0
  113. data/app/components/uchi/ui/form/footer.rb +15 -0
  114. data/app/components/uchi/ui/form/input/collection_checkboxes.html.erb +32 -0
  115. data/app/components/uchi/ui/form/input/collection_checkboxes.rb +125 -0
  116. data/app/components/uchi/ui/frame/frame.html.erb +3 -0
  117. data/app/components/uchi/ui/frame.rb +10 -0
  118. data/app/components/uchi/ui/index/records_table/records_table.html.erb +67 -0
  119. data/app/components/uchi/ui/index/records_table/search_form/search_form.html.erb +21 -0
  120. data/app/components/uchi/ui/index/records_table/search_form.rb +49 -0
  121. data/app/components/uchi/ui/index/records_table.rb +29 -0
  122. data/app/components/uchi/ui/index/turbo_frame.rb +50 -0
  123. data/app/components/uchi/ui/page_header/page_header.html.erb +24 -0
  124. data/app/components/uchi/ui/page_header.rb +18 -0
  125. data/app/components/uchi/ui/pagination/current_link.html.erb +3 -0
  126. data/app/components/uchi/ui/pagination/current_link.rb +10 -0
  127. data/app/components/uchi/ui/pagination/gap.html.erb +3 -0
  128. data/app/components/uchi/ui/pagination/gap.rb +10 -0
  129. data/app/components/uchi/ui/pagination/item.rb +24 -0
  130. data/app/components/uchi/ui/pagination/link.html.erb +3 -0
  131. data/app/components/uchi/ui/pagination/link.rb +10 -0
  132. data/app/components/uchi/ui/pagination/next_link.html.erb +8 -0
  133. data/app/components/uchi/ui/pagination/next_link.rb +22 -0
  134. data/app/components/uchi/ui/pagination/pagination.html.erb +15 -0
  135. data/app/components/uchi/ui/pagination/previous_link.html.erb +8 -0
  136. data/app/components/uchi/ui/pagination/previous_link.rb +22 -0
  137. data/app/components/uchi/ui/pagination.rb +48 -0
  138. data/app/components/uchi/ui/show/attribute_fields/attribute_fields.html.erb +14 -0
  139. data/app/components/uchi/ui/show/attribute_fields.rb +18 -0
  140. data/app/components/uchi/ui/spinner/spinner.html.erb +7 -0
  141. data/app/components/uchi/ui/spinner.rb +15 -0
  142. data/app/controllers/uchi/application_controller.rb +4 -0
  143. data/app/controllers/uchi/controller.rb +13 -0
  144. data/app/controllers/uchi/repository_controller.rb +166 -0
  145. data/app/helpers/uchi/application_helper.rb +17 -0
  146. data/app/jobs/uchi/application_job.rb +4 -0
  147. data/app/mailers/uchi/application_mailer.rb +6 -0
  148. data/app/views/layouts/uchi/_flash_messages.html.erb +10 -0
  149. data/app/views/layouts/uchi/application.html.erb +33 -0
  150. data/app/views/uchi/repository/edit.html.erb +40 -0
  151. data/app/views/uchi/repository/index.html.erb +49 -0
  152. data/app/views/uchi/repository/new.html.erb +37 -0
  153. data/app/views/uchi/repository/show.html.erb +41 -0
  154. data/lib/generators/uchi/controller/controller_generator.rb +16 -0
  155. data/lib/generators/uchi/controller/templates/controller.rb.tt +11 -0
  156. data/lib/generators/uchi/install/install_generator.rb +13 -0
  157. data/lib/generators/uchi/repository/repository_generator.rb +16 -0
  158. data/lib/generators/uchi/repository/templates/repository.rb.tt +11 -0
  159. data/lib/tasks/uchi_tasks.rake +4 -0
  160. data/lib/uchi/application_record.rb +5 -0
  161. data/lib/uchi/engine.rb +24 -0
  162. data/lib/uchi/field/configuration.rb +142 -0
  163. data/lib/uchi/field.rb +80 -0
  164. data/lib/uchi/i18n.rb +13 -0
  165. data/lib/uchi/pagination/controller.rb +26 -0
  166. data/lib/uchi/pagination/page.rb +20 -0
  167. data/lib/uchi/pagy/LICENSE.txt +21 -0
  168. data/lib/uchi/pagy/classes/exceptions.rb +35 -0
  169. data/lib/uchi/pagy/classes/offset/offset.rb +56 -0
  170. data/lib/uchi/pagy/classes/request.rb +38 -0
  171. data/lib/uchi/pagy/modules/abilities/configurable.rb +38 -0
  172. data/lib/uchi/pagy/modules/abilities/linkable.rb +62 -0
  173. data/lib/uchi/pagy/modules/abilities/rangeable.rb +17 -0
  174. data/lib/uchi/pagy/modules/abilities/shiftable.rb +14 -0
  175. data/lib/uchi/pagy/modules/console.rb +40 -0
  176. data/lib/uchi/pagy/toolbox/helpers/loader.rb +19 -0
  177. data/lib/uchi/pagy/toolbox/helpers/page_url.rb +25 -0
  178. data/lib/uchi/pagy/toolbox/paginators/method.rb +21 -0
  179. data/lib/uchi/pagy/toolbox/paginators/offset.rb +35 -0
  180. data/lib/uchi/pagy.rb +60 -0
  181. data/lib/uchi/repository/routes.rb +62 -0
  182. data/lib/uchi/repository/translate.rb +284 -0
  183. data/lib/uchi/repository.rb +156 -0
  184. data/lib/uchi/sort_order.rb +35 -0
  185. data/lib/uchi/version.rb +5 -0
  186. data/lib/uchi.rb +18 -0
  187. data/uchi.gemspec +35 -0
  188. metadata +197 -107
  189. data/.github/dependabot.yml +0 -17
  190. data/.github/workflows/build.yml +0 -23
  191. data/.github/workflows/lint.yml +0 -30
  192. data/package.json +0 -31
  193. data/sig/uchi.rbs +0 -4
  194. data/test/components/uchi/field/belongs_to_test.rb +0 -134
  195. data/test/components/uchi/field/blank_test.rb +0 -119
  196. data/test/components/uchi/field/boolean_test.rb +0 -163
  197. data/test/components/uchi/field/date_test.rb +0 -163
  198. data/test/components/uchi/field/date_time_test.rb +0 -152
  199. data/test/components/uchi/field/has_many_test.rb +0 -138
  200. data/test/components/uchi/field/id_test.rb +0 -113
  201. data/test/components/uchi/field/number_test.rb +0 -163
  202. data/test/components/uchi/field/string_test.rb +0 -159
  203. data/test/controllers/uchi/authors_controller_test.rb +0 -119
  204. data/test/controllers/uchi/repository_controller_test.rb +0 -93
  205. data/test/controllers/uchi/scoped_repository_controller_test.rb +0 -73
  206. data/test/dummy/Rakefile +0 -6
  207. data/test/dummy/app/assets/images/.keep +0 -0
  208. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  209. data/test/dummy/app/controllers/application_controller.rb +0 -4
  210. data/test/dummy/app/controllers/concerns/.keep +0 -0
  211. data/test/dummy/app/controllers/uchi/authors_controller.rb +0 -7
  212. data/test/dummy/app/controllers/uchi/books_controller.rb +0 -7
  213. data/test/dummy/app/controllers/uchi/titles_controller.rb +0 -7
  214. data/test/dummy/app/helpers/application_helper.rb +0 -2
  215. data/test/dummy/app/jobs/application_job.rb +0 -7
  216. data/test/dummy/app/mailers/application_mailer.rb +0 -4
  217. data/test/dummy/app/models/application_record.rb +0 -3
  218. data/test/dummy/app/models/author.rb +0 -3
  219. data/test/dummy/app/models/book.rb +0 -3
  220. data/test/dummy/app/models/concerns/.keep +0 -0
  221. data/test/dummy/app/models/title.rb +0 -3
  222. data/test/dummy/app/uchi/repositories/author.rb +0 -20
  223. data/test/dummy/app/uchi/repositories/book.rb +0 -16
  224. data/test/dummy/app/uchi/repositories/title.rb +0 -17
  225. data/test/dummy/app/views/layouts/application.html.erb +0 -27
  226. data/test/dummy/app/views/layouts/mailer.html.erb +0 -13
  227. data/test/dummy/app/views/layouts/mailer.text.erb +0 -1
  228. data/test/dummy/app/views/pwa/manifest.json.erb +0 -22
  229. data/test/dummy/app/views/pwa/service-worker.js +0 -26
  230. data/test/dummy/bin/dev +0 -2
  231. data/test/dummy/bin/rails +0 -4
  232. data/test/dummy/bin/rake +0 -4
  233. data/test/dummy/bin/setup +0 -34
  234. data/test/dummy/config/application.rb +0 -29
  235. data/test/dummy/config/boot.rb +0 -5
  236. data/test/dummy/config/cable.yml +0 -10
  237. data/test/dummy/config/database.yml +0 -32
  238. data/test/dummy/config/environment.rb +0 -5
  239. data/test/dummy/config/environments/development.rb +0 -69
  240. data/test/dummy/config/environments/production.rb +0 -89
  241. data/test/dummy/config/environments/test.rb +0 -53
  242. data/test/dummy/config/initializers/content_security_policy.rb +0 -25
  243. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -8
  244. data/test/dummy/config/initializers/inflections.rb +0 -16
  245. data/test/dummy/config/locales/da.yml +0 -51
  246. data/test/dummy/config/locales/en.yml +0 -31
  247. data/test/dummy/config/puma.rb +0 -38
  248. data/test/dummy/config/routes.rb +0 -9
  249. data/test/dummy/config/storage.yml +0 -34
  250. data/test/dummy/config.ru +0 -6
  251. data/test/dummy/db/migrate/20251002183635_create_authors.rb +0 -11
  252. data/test/dummy/db/migrate/20251005131726_create_books.rb +0 -9
  253. data/test/dummy/db/migrate/20251005131811_create_titles.rb +0 -11
  254. data/test/dummy/db/schema.rb +0 -38
  255. data/test/dummy/log/.keep +0 -0
  256. data/test/dummy/public/400.html +0 -114
  257. data/test/dummy/public/404.html +0 -114
  258. data/test/dummy/public/406-unsupported-browser.html +0 -114
  259. data/test/dummy/public/422.html +0 -114
  260. data/test/dummy/public/500.html +0 -114
  261. data/test/dummy/public/icon.png +0 -0
  262. data/test/dummy/public/icon.svg +0 -3
  263. data/test/dummy/storage/.keep +0 -0
  264. data/test/dummy/test/fixtures/authors.yml +0 -11
  265. data/test/dummy/test/models/author_test.rb +0 -7
  266. data/test/dummy/tmp/.keep +0 -0
  267. data/test/dummy/tmp/pids/.keep +0 -0
  268. data/test/dummy/tmp/storage/.keep +0 -0
  269. data/test/test_helper.rb +0 -15
  270. data/test/uchi/field_test.rb +0 -63
  271. data/test/uchi/i18n_test.rb +0 -18
  272. data/test/uchi/repository/routes_test.rb +0 -49
  273. data/test/uchi/repository/translate_test.rb +0 -263
  274. data/test/uchi/repository_test.rb +0 -137
  275. data/test/uchi/sort_order_test.rb +0 -47
  276. data/test/uchi_test.rb +0 -7
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class Blank < Field
6
+ class Edit < Uchi::Field::Base::Edit
7
+ end
8
+
9
+ class Index < Uchi::Field::Base::Index
10
+ end
11
+
12
+ class Show < Uchi::Field::Base::Show
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::Checkbox.new(**options)) %>
@@ -0,0 +1,9 @@
1
+ <% if field.value(record) %>
2
+ <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
3
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.5 11.5 11 14l4-4m6 2a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
4
+ </svg>
5
+ <% else %>
6
+ <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
7
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.757 12h8.486M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
8
+ </svg>
9
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <% if field.value(record) %>
2
+ <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
3
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.5 11.5 11 14l4-4m6 2a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
4
+ </svg>
5
+ <% else %>
6
+ <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
7
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.757 12h8.486M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
8
+ </svg>
9
+ <% end %>
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class Boolean < Field
6
+ class Edit < Uchi::Field::Base::Edit
7
+ private
8
+
9
+ def options
10
+ options = {
11
+ attribute: field.name,
12
+ form: form,
13
+ label: {content: label}
14
+ }
15
+ options[:hint] = {content: hint} if hint.present?
16
+ options
17
+ end
18
+ end
19
+
20
+ class Index < Uchi::Field::Base::Index
21
+ end
22
+
23
+ class Show < Uchi::Field::Base::Show
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::Date.new(**options)) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class Date < Field
6
+ class Edit < Uchi::Field::Base::Edit
7
+ private
8
+
9
+ def options
10
+ options = {
11
+ attribute: field.name,
12
+ form: form,
13
+ label: {content: label}
14
+ }
15
+ options[:hint] = {content: hint} if hint.present?
16
+ options
17
+ end
18
+ end
19
+
20
+ class Index < Uchi::Field::Base::Index
21
+ end
22
+
23
+ class Show < Uchi::Field::Base::Show
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::DateTime.new(**options)) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class DateTime < Field
6
+ class Edit < Uchi::Field::Base::Edit
7
+ private
8
+
9
+ def options
10
+ options = {
11
+ attribute: field.name,
12
+ form: form,
13
+ label: {content: label}
14
+ }
15
+ options[:hint] = {content: hint} if hint.present?
16
+ options
17
+ end
18
+ end
19
+
20
+ class Index < Uchi::Field::Base::Index
21
+ end
22
+
23
+ class Show < Uchi::Field::Base::Show
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::File.new(**options)) %>
@@ -0,0 +1,6 @@
1
+ <% attachment = field.value(record) %>
2
+ <% if attachment.attached? %>
3
+ <%= attachment.filename.to_s %>
4
+ <% else %>
5
+ <span class="text-gray-400">—</span>
6
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <% attachment = field.value(record) %>
2
+ <% if attachment.attached? %>
3
+ <%= render Flowbite::Link.new(href: Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)) do %>
4
+ <%= attachment.filename %>
5
+ <% end %>
6
+ <% else %>
7
+ <span class="text-gray-400">—</span>
8
+ <% end %>
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class File < Field
6
+ class Edit < Uchi::Field::Base::Edit
7
+ private
8
+
9
+ def options
10
+ options = {
11
+ attribute: field.name,
12
+ form: form,
13
+ label: {content: label}
14
+ }
15
+ options[:hint] = {content: hint} if hint.present?
16
+ options
17
+ end
18
+ end
19
+
20
+ class Index < Uchi::Field::Base::Index
21
+ end
22
+
23
+ class Show < Uchi::Field::Base::Show
24
+ end
25
+
26
+ protected
27
+
28
+ def default_searchable?
29
+ false
30
+ end
31
+
32
+ def default_sortable?
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ <%= render(Uchi::Ui::Form::Input::CollectionCheckboxes.new(
2
+ attribute: field.param_key,
3
+ collection: collection,
4
+ form: form,
5
+ hint: hint,
6
+ label: label,
7
+ text_method: :first,
8
+ value_method: :last,
9
+ )) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record).count %>
@@ -0,0 +1,28 @@
1
+ <div class="my-10">
2
+ <h2 class="px-4 my-6 text-2xl font-bold md:px-0"><%= repository.translate.field_label(field) %></h2>
3
+
4
+ <%= render(Uchi::Ui::Index::TurboFrame.new(
5
+ repository: associated_repository,
6
+ scope: {
7
+ field: field.name,
8
+ id: record.id,
9
+ inverse_of: inverse_association&.name,
10
+ model: record.model_name,
11
+ },
12
+ src: associated_repository.routes.path_for(
13
+ :index,
14
+ scope: {
15
+ field: field.name,
16
+ id: record.id,
17
+ inverse_of: inverse_association&.name,
18
+ model: record.model_name,
19
+ },
20
+ )
21
+ )) do %>
22
+ <div class="relative overflow-x-auto bg-white border border-gray-200 shadow-sm md:rounded-lg dark:bg-gray-800 dark:border-gray-700">
23
+ <div class="grid grid place-items-center p-8">
24
+ <%= render(Uchi::Ui::Spinner.new(message: repository.translate.loading_message)) %>
25
+ </div>
26
+ </div>
27
+ <% end %>
28
+ </div>
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class HasAndBelongsToMany < Field
6
+ DEFAULT_COLLECTION_QUERY = ->(query) { query }.freeze
7
+
8
+ class Edit < Uchi::Field::Base::Edit
9
+ def collection
10
+ associated_repository = field.associated_repository(record: record)
11
+
12
+ field
13
+ .collection(record: record)
14
+ .map { |associated_record|
15
+ [
16
+ associated_repository.title(associated_record),
17
+ associated_record.id
18
+ ]
19
+ }
20
+ end
21
+ end
22
+
23
+ class Index < Uchi::Field::Base::Index
24
+ end
25
+
26
+ class Show < Uchi::Field::Base::Show
27
+ def associated_records
28
+ records = field.value(record)
29
+
30
+ associated_repository.find_all(scope: records)
31
+ end
32
+
33
+ def associated_repository
34
+ field.associated_repository(record: record)
35
+ end
36
+
37
+ private
38
+
39
+ # Returns the Fields to be displayed for each associated record.
40
+ #
41
+ # @return [Array<Uchi::Field>]
42
+ def fields
43
+ all_fields = associated_repository.fields_for_index
44
+ return all_fields unless inverse_association
45
+
46
+ # Remove the field that represents the inverse association to avoid
47
+ # a column containing nothing but the scoped record.
48
+ all_fields.reject { |field| field.name == inverse_association.name }
49
+ end
50
+
51
+ # Returns the inverse association reflection, if any, for this
52
+ # association.
53
+ #
54
+ # @return [ActiveRecord::Reflection, nil]
55
+ def inverse_association
56
+ reflection = record.class.reflect_on_association(field.name)
57
+ reflection&.inverse_of
58
+ end
59
+
60
+ # Returns the ActiveRecord::Reflection for this association.
61
+ #
62
+ # @return [ActiveRecord::Reflection]
63
+ def reflection
64
+ @reflection ||= record.class.reflect_on_association(field.name)
65
+ end
66
+ end
67
+
68
+ def associated_repository(record:)
69
+ reflection = record.class.reflect_on_association(name)
70
+ model = reflection.klass
71
+ repository_class = Uchi::Repository.for_model(model)
72
+ raise NameError, "No repository found for associated model #{model}" unless repository_class
73
+
74
+ repository_class.new
75
+ end
76
+
77
+ def collection(record:)
78
+ query = associated_repository(record: record).find_all
79
+ @collection_query.call(query)
80
+ end
81
+
82
+ def initialize(name)
83
+ super
84
+ @collection_query = DEFAULT_COLLECTION_QUERY
85
+ end
86
+
87
+ # Sets or gets a custom query for filtering the collection of associated records.
88
+ #
89
+ # When called with an argument, sets the query and returns self for chaining.
90
+ # When called without arguments, returns the current query.
91
+ #
92
+ # @param query_proc [Proc, Symbol] A callable that receives an ActiveRecord query
93
+ # and returns a modified query.
94
+ # @return [self, Proc] Returns self for method chaining when setting,
95
+ # or the query proc when getting
96
+ #
97
+ # @example Setting
98
+ # Field::HasAndBelongsToMany.new(:tags).collection_query(->(query) {
99
+ # query.where(active: true)
100
+ # })
101
+ #
102
+ # @example Getting
103
+ # field.collection_query # => #<Proc...>
104
+ def collection_query(query_proc = Configuration::Unset)
105
+ return @collection_query if query_proc == Configuration::Unset
106
+
107
+ @collection_query = query_proc
108
+ self
109
+ end
110
+
111
+ def group_as(_action)
112
+ :associations
113
+ end
114
+
115
+ # Returns the key to use for this field in params
116
+ def param_key
117
+ association.association_foreign_key.pluralize.to_sym
118
+ end
119
+
120
+ def permitted_param
121
+ {param_key => []}
122
+ end
123
+
124
+ protected
125
+
126
+ def association
127
+ @association ||= repository.model.reflect_on_association(name)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1 @@
1
+ <%# This space intentionally left blank %>
@@ -0,0 +1 @@
1
+ <%= field.value(record).count %>
@@ -0,0 +1,28 @@
1
+ <div class="my-10">
2
+ <h2 class="px-4 my-6 text-2xl font-bold md:px-0"><%= repository.translate.field_label(field) %></h2>
3
+
4
+ <%= render(Uchi::Ui::Index::TurboFrame.new(
5
+ repository: associated_repository,
6
+ scope: {
7
+ field: field.name,
8
+ id: record.id,
9
+ inverse_of: inverse_association&.name,
10
+ model: record.model_name,
11
+ },
12
+ src: associated_repository.routes.path_for(
13
+ :index,
14
+ scope: {
15
+ field: field.name,
16
+ id: record.id,
17
+ inverse_of: inverse_association&.name,
18
+ model: record.model_name,
19
+ },
20
+ )
21
+ )) do %>
22
+ <div class="relative overflow-x-auto bg-white border border-gray-200 shadow-sm md:rounded-lg dark:bg-gray-800 dark:border-gray-700">
23
+ <div class="grid grid place-items-center p-8">
24
+ <%= render(Uchi::Ui::Spinner.new(message: repository.translate.loading_message)) %>
25
+ </div>
26
+ </div>
27
+ <% end %>
28
+ </div>
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class HasMany < Field
6
+ DEFAULT_COLLECTION_QUERY = ->(query) { query }.freeze
7
+
8
+ class Edit < Uchi::Field::Base::Edit
9
+ end
10
+
11
+ class Index < Uchi::Field::Base::Index
12
+ end
13
+
14
+ class Show < Uchi::Field::Base::Show
15
+ def associated_records
16
+ records = field.value(record)
17
+
18
+ associated_repository.find_all(scope: records)
19
+ end
20
+
21
+ def associated_repository
22
+ reflection = record.class.reflect_on_association(field.name)
23
+ model = reflection.klass
24
+ repository_class = Uchi::Repository.for_model(model)
25
+ raise NameError, "No repository found for associated model #{model}" unless repository_class
26
+
27
+ repository_class.new
28
+ end
29
+
30
+ private
31
+
32
+ # Returns the Fields to be displayed for each associated record.
33
+ #
34
+ # @return [Array<Uchi::Field>]
35
+ def fields
36
+ all_fields = associated_repository.fields_for_index
37
+ return all_fields unless inverse_association
38
+
39
+ # Remove the field that represents the inverse association to avoid
40
+ # a column containing nothing but the scoped record.
41
+ all_fields.reject { |field| field.name == inverse_association.name }
42
+ end
43
+
44
+ # Returns the inverse association reflection, if any, for this
45
+ # association.
46
+ #
47
+ # @return [ActiveRecord::Reflection, nil]
48
+ def inverse_association
49
+ reflection = record.class.reflect_on_association(field.name)
50
+ reflection&.inverse_of
51
+ end
52
+
53
+ # Returns the ActiveRecord::Reflection for this association.
54
+ #
55
+ # @return [ActiveRecord::Reflection]
56
+ def reflection
57
+ @reflection ||= record.class.reflect_on_association(field.name)
58
+ end
59
+ end
60
+
61
+ def initialize(name)
62
+ super
63
+ @collection_query = DEFAULT_COLLECTION_QUERY
64
+ end
65
+
66
+ # Sets or gets a custom query for filtering the collection of associated records.
67
+ #
68
+ # When called with an argument, sets the query and returns self for chaining.
69
+ # When called without arguments, returns the current query.
70
+ #
71
+ # @param query_proc [Proc, Symbol] A callable that receives an ActiveRecord query
72
+ # and returns a modified query.
73
+ # @return [self, Proc] Returns self for method chaining when setting,
74
+ # or the query proc when getting
75
+ #
76
+ # @example Setting
77
+ # Field::HasMany.new(:users).collection_query(->(query) {
78
+ # query.where(active: true)
79
+ # })
80
+ #
81
+ # @example Getting
82
+ # field.collection_query # => #<Proc...>
83
+ def collection_query(query_proc = Configuration::Unset)
84
+ return @collection_query if query_proc == Configuration::Unset
85
+
86
+ @collection_query = query_proc
87
+ self
88
+ end
89
+
90
+ def group_as(_action)
91
+ :associations
92
+ end
93
+
94
+ def param_key
95
+ # TODO: This is too naive. We need to match this to the actual foreign
96
+ # key of the model.
97
+ :"#{name}_id"
98
+ end
99
+
100
+ protected
101
+
102
+ def default_on
103
+ [:show]
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,4 @@
1
+ <%= render(Flowbite::Link.new(
2
+ href: @repository.routes.path_for(:show, id: record.id),
3
+ target: "_top").with_content(field.value(record))
4
+ ) %>
@@ -0,0 +1,4 @@
1
+ <%= render(Flowbite::Link.new(
2
+ href: @repository.routes.path_for(:show, id: record.id),
3
+ target: "_top").with_content(field.value(record))
4
+ ) %>
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ # The Id field is intended for unique identifiers, such as primary keys. It
6
+ # contains a link to the records show page, and is not considered editable.
7
+ # As such it is only displayed in index and show views by default.
8
+ class Id < Number
9
+ class Index < Uchi::Field::Base::Index
10
+ end
11
+
12
+ class Show < Uchi::Field::Base::Show
13
+ end
14
+
15
+ protected
16
+
17
+ def default_on
18
+ [:index, :show]
19
+ end
20
+
21
+ def default_searchable?
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::File.new(**options)) %>
@@ -0,0 +1,6 @@
1
+ <% attachment = field.value(record) %>
2
+ <% if attachment.attached? %>
3
+ <%= image_tag attachment.variant(resize_to_limit: [100, 100]), class: "h-12 w-12 object-cover rounded" %>
4
+ <% else %>
5
+ <span class="text-gray-400">—</span>
6
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <% attachment = field.value(record) %>
2
+ <% if attachment.attached? %>
3
+ <%= image_tag attachment, class: "max-w-full h-auto rounded-lg shadow-md" %>
4
+ <% else %>
5
+ <span class="text-gray-400">—</span>
6
+ <% end %>
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class Image < Field
6
+ class Edit < Uchi::Field::Base::Edit
7
+ private
8
+
9
+ def options
10
+ options = {
11
+ attribute: field.name,
12
+ form: form,
13
+ label: {content: label},
14
+ input: {options: {accept: "image/*"}}
15
+ }
16
+ options[:hint] = {content: hint} if hint.present?
17
+ options
18
+ end
19
+ end
20
+
21
+ class Index < Uchi::Field::Base::Index
22
+ end
23
+
24
+ class Show < Uchi::Field::Base::Show
25
+ end
26
+
27
+ protected
28
+
29
+ def default_searchable?
30
+ false
31
+ end
32
+
33
+ def default_sortable?
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::Number.new(**options)) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class Number < Field
6
+ class Edit < Uchi::Field::Base::Edit
7
+ private
8
+
9
+ def options
10
+ options = {
11
+ attribute: field.name,
12
+ form: form,
13
+ label: {content: label}
14
+ }
15
+ options[:hint] = {content: hint} if hint.present?
16
+ options
17
+ end
18
+ end
19
+
20
+ class Index < Uchi::Field::Base::Index
21
+ class << self
22
+ def classes_for_table_cell
23
+ super + ["text-right"]
24
+ end
25
+ end
26
+ end
27
+
28
+ class Show < Uchi::Field::Base::Show
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::Text.new(**options)) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>