uchi 0.1.3 → 0.1.5

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 (283) 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 +5 -0
  62. data/app/components/uchi/field/belongs_to.rb +112 -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 +368 -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 +189 -97
  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/CHANGELOG.md +0 -29
  193. data/docs/fields.md +0 -82
  194. data/docs/repositories.md +0 -63
  195. data/package.json +0 -31
  196. data/sig/uchi.rbs +0 -4
  197. data/test/components/uchi/field/belongs_to_test.rb +0 -134
  198. data/test/components/uchi/field/blank_test.rb +0 -119
  199. data/test/components/uchi/field/boolean_test.rb +0 -163
  200. data/test/components/uchi/field/date_test.rb +0 -163
  201. data/test/components/uchi/field/date_time_test.rb +0 -152
  202. data/test/components/uchi/field/has_and_belongs_to_many_test.rb +0 -144
  203. data/test/components/uchi/field/has_many_test.rb +0 -138
  204. data/test/components/uchi/field/id_test.rb +0 -113
  205. data/test/components/uchi/field/number_test.rb +0 -163
  206. data/test/components/uchi/field/string_test.rb +0 -159
  207. data/test/components/uchi/field/text_test.rb +0 -160
  208. data/test/components/uchi/ui/form/input/collection_checkboxes_test.rb +0 -171
  209. data/test/controllers/uchi/authors_controller_test.rb +0 -120
  210. data/test/controllers/uchi/repository_controller_test.rb +0 -93
  211. data/test/controllers/uchi/scoped_repository_controller_test.rb +0 -73
  212. data/test/dummy/Rakefile +0 -6
  213. data/test/dummy/app/assets/images/.keep +0 -0
  214. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  215. data/test/dummy/app/controllers/application_controller.rb +0 -4
  216. data/test/dummy/app/controllers/concerns/.keep +0 -0
  217. data/test/dummy/app/controllers/uchi/authors_controller.rb +0 -7
  218. data/test/dummy/app/controllers/uchi/books_controller.rb +0 -7
  219. data/test/dummy/app/controllers/uchi/titles_controller.rb +0 -7
  220. data/test/dummy/app/helpers/application_helper.rb +0 -2
  221. data/test/dummy/app/jobs/application_job.rb +0 -7
  222. data/test/dummy/app/mailers/application_mailer.rb +0 -4
  223. data/test/dummy/app/models/application_record.rb +0 -3
  224. data/test/dummy/app/models/author.rb +0 -5
  225. data/test/dummy/app/models/book.rb +0 -4
  226. data/test/dummy/app/models/concerns/.keep +0 -0
  227. data/test/dummy/app/models/title.rb +0 -3
  228. data/test/dummy/app/uchi/repositories/author.rb +0 -20
  229. data/test/dummy/app/uchi/repositories/book.rb +0 -16
  230. data/test/dummy/app/uchi/repositories/title.rb +0 -17
  231. data/test/dummy/app/views/layouts/application.html.erb +0 -27
  232. data/test/dummy/app/views/layouts/mailer.html.erb +0 -13
  233. data/test/dummy/app/views/layouts/mailer.text.erb +0 -1
  234. data/test/dummy/app/views/pwa/manifest.json.erb +0 -22
  235. data/test/dummy/app/views/pwa/service-worker.js +0 -26
  236. data/test/dummy/bin/dev +0 -2
  237. data/test/dummy/bin/rails +0 -4
  238. data/test/dummy/bin/rake +0 -4
  239. data/test/dummy/bin/setup +0 -34
  240. data/test/dummy/config/application.rb +0 -29
  241. data/test/dummy/config/boot.rb +0 -5
  242. data/test/dummy/config/cable.yml +0 -10
  243. data/test/dummy/config/database.yml +0 -32
  244. data/test/dummy/config/environment.rb +0 -5
  245. data/test/dummy/config/environments/development.rb +0 -69
  246. data/test/dummy/config/environments/production.rb +0 -89
  247. data/test/dummy/config/environments/test.rb +0 -53
  248. data/test/dummy/config/initializers/content_security_policy.rb +0 -25
  249. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -8
  250. data/test/dummy/config/initializers/inflections.rb +0 -16
  251. data/test/dummy/config/locales/da.yml +0 -52
  252. data/test/dummy/config/locales/en.yml +0 -31
  253. data/test/dummy/config/puma.rb +0 -38
  254. data/test/dummy/config/routes.rb +0 -9
  255. data/test/dummy/config/storage.yml +0 -34
  256. data/test/dummy/config.ru +0 -6
  257. data/test/dummy/db/migrate/20251002183635_create_authors.rb +0 -11
  258. data/test/dummy/db/migrate/20251005131726_create_books.rb +0 -9
  259. data/test/dummy/db/migrate/20251005131811_create_titles.rb +0 -11
  260. data/test/dummy/db/migrate/20251031140958_add_author_books_join_table.rb +0 -9
  261. data/test/dummy/db/schema.rb +0 -44
  262. data/test/dummy/log/.keep +0 -0
  263. data/test/dummy/public/400.html +0 -114
  264. data/test/dummy/public/404.html +0 -114
  265. data/test/dummy/public/406-unsupported-browser.html +0 -114
  266. data/test/dummy/public/422.html +0 -114
  267. data/test/dummy/public/500.html +0 -114
  268. data/test/dummy/public/icon.png +0 -0
  269. data/test/dummy/public/icon.svg +0 -3
  270. data/test/dummy/storage/.keep +0 -0
  271. data/test/dummy/test/fixtures/authors.yml +0 -11
  272. data/test/dummy/test/models/author_test.rb +0 -7
  273. data/test/dummy/tmp/.keep +0 -0
  274. data/test/dummy/tmp/pids/.keep +0 -0
  275. data/test/dummy/tmp/storage/.keep +0 -0
  276. data/test/test_helper.rb +0 -15
  277. data/test/uchi/field_test.rb +0 -77
  278. data/test/uchi/i18n_test.rb +0 -18
  279. data/test/uchi/repository/routes_test.rb +0 -49
  280. data/test/uchi/repository/translate_test.rb +0 -272
  281. data/test/uchi/repository_test.rb +0 -137
  282. data/test/uchi/sort_order_test.rb +0 -47
  283. data/test/uchi_test.rb +0 -7
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class BelongsTo < Field
6
+ DEFAULT_COLLECTION_QUERY = ->(query) { query }.freeze
7
+
8
+ class Edit < Uchi::Field::Base::Edit
9
+ def associated_repository
10
+ model = reflection.klass
11
+ repository_class = Uchi::Repository.for_model(model)
12
+ repository_class.new
13
+ end
14
+
15
+ def attribute_name
16
+ reflection.foreign_key
17
+ end
18
+
19
+ def collection
20
+ query = associated_repository.find_all
21
+ field.collection_query.call(query)
22
+ end
23
+
24
+ private
25
+
26
+ def collection_for_select
27
+ repository = associated_repository
28
+ items = []
29
+ items << ["", nil] if optional?
30
+ items + collection.map do |item|
31
+ [repository.title(item), item.id]
32
+ end
33
+ end
34
+
35
+ # Returns true if the association is optional.
36
+ def optional?
37
+ reflection.options[:optional] == true
38
+ end
39
+
40
+ def options
41
+ options = {
42
+ attribute: attribute_name,
43
+ collection: collection_for_select,
44
+ form: form,
45
+ label: {content: label}
46
+ }
47
+ options[:hint] = {content: hint} if hint.present?
48
+ options
49
+ end
50
+
51
+ def reflection
52
+ @reflection ||= record.class.reflect_on_association(field.name)
53
+ end
54
+ end
55
+
56
+ class Index < Uchi::Field::Base::Index
57
+ end
58
+
59
+ class Show < Uchi::Field::Base::Show
60
+ def associated_record
61
+ field.value(record)
62
+ end
63
+
64
+ def associated_repository
65
+ reflection = record.class.reflect_on_association(field.name)
66
+ model = reflection.klass
67
+ repository_class = Uchi::Repository.for_model(model)
68
+ repository_class.new
69
+ end
70
+ end
71
+
72
+ def initialize(name)
73
+ super
74
+ @collection_query = DEFAULT_COLLECTION_QUERY
75
+ end
76
+
77
+ # Sets or gets a custom query for filtering the collection of associated records.
78
+ #
79
+ # When called with an argument, sets the query and returns self for chaining.
80
+ # When called without arguments, returns the current query.
81
+ #
82
+ # @param query_proc [Proc, Symbol] A callable that receives an ActiveRecord query
83
+ # and returns a modified query.
84
+ # @return [self, Proc] Returns self for method chaining when setting,
85
+ # or the query proc when getting
86
+ #
87
+ # @example Setting
88
+ # Field::BelongsTo.new(:company).collection_query(->(query) {
89
+ # query.where(active: true)
90
+ # })
91
+ #
92
+ # @example Getting
93
+ # field.collection_query # => #<Proc...>
94
+ def collection_query(query_proc = Configuration::Unset)
95
+ return @collection_query if query_proc == Configuration::Unset
96
+
97
+ @collection_query = query_proc
98
+ self
99
+ end
100
+
101
+ def group_as(_action)
102
+ :attributes
103
+ end
104
+
105
+ def param_key
106
+ # TODO: This is too naive. We need to match this to the actual foreign
107
+ # key of the model.
108
+ :"#{name}_id"
109
+ end
110
+ end
111
+ end
112
+ end
@@ -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