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,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ module Ui
5
+ class Pagination
6
+ class NextLink < Item
7
+ # Returns true if there is a next page
8
+ def next?
9
+ paginator.next.present?
10
+ end
11
+
12
+ def url
13
+ if next?
14
+ page_url(:next)
15
+ else
16
+ page_url(:last)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ <nav aria-label="<%= aria_label %>">
2
+ <ul class="flex items-center -space-x-px h-8 text-sm">
3
+ <%= render(Uchi::Ui::Pagination::PreviousLink.new(paginator: @paginator)) %>
4
+ <% pages.each do |page_number| %>
5
+ <% if page_number.nil? %>
6
+ <%= render(Uchi::Ui::Pagination::Gap.new(page_number: page_number, paginator: @paginator)) %>
7
+ <% elsif page_number == @paginator.page %>
8
+ <%= render(Uchi::Ui::Pagination::CurrentLink.new(page_number: page_number, paginator: @paginator)) %>
9
+ <% else %>
10
+ <%= render(Uchi::Ui::Pagination::Link.new(page_number: page_number, paginator: @paginator)) %>
11
+ <% end %>
12
+ <% end %>
13
+ <%= render(Uchi::Ui::Pagination::NextLink.new(paginator: @paginator)) %>
14
+ </ul>
15
+ </nav>
@@ -0,0 +1,8 @@
1
+ <li>
2
+ <%= link_to(url, class: "flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white") do %>
3
+ <span class="sr-only">Previous</span>
4
+ <svg class="w-3 h-3 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
5
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4"/>
6
+ </svg>
7
+ <% end %>
8
+ </li>
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ module Ui
5
+ class Pagination
6
+ class PreviousLink < Item
7
+ # Returns true if there is a previous page
8
+ def previous?
9
+ paginator.page > 1
10
+ end
11
+
12
+ def url
13
+ if previous?
14
+ page_url(:previous)
15
+ else
16
+ page_url(:first)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ module Ui
5
+ class Pagination < ViewComponent::Base
6
+ attr_reader :paginator
7
+
8
+ def aria_label
9
+ "Page navigation"
10
+ end
11
+
12
+ # @param paginator [Pagy::Pagy] The Pagy object
13
+ def initialize(paginator:)
14
+ super()
15
+ @paginator = paginator
16
+ end
17
+
18
+ # Returns the URL for a given page. page can be a number, of :first,
19
+ # :last, :previous, :next, :current.
20
+ def page_url(page)
21
+ paginator.page_url(page)
22
+ end
23
+
24
+ # Returns an array of page numbers to display in the pagination component
25
+ def pages
26
+ pages = []
27
+ pages += [1]
28
+ pages += (paginator.page - 2..paginator.page + 2).to_a
29
+ pages += [paginator.last]
30
+ pages = pages.select { |page| page.between?(1, paginator.last) }.uniq.sort
31
+
32
+ result = []
33
+ pages.each_with_index do |page, index|
34
+ result << page
35
+ next_page = pages[index + 1]
36
+ if index < pages.size - 1 && next_page - page > 1
37
+ result << nil # Add a gap
38
+ end
39
+ end
40
+ result
41
+ end
42
+
43
+ def render?
44
+ paginator.present?
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,14 @@
1
+ <%= render(Uchi::Ui::Frame.new) do %>
2
+ <dl class="divide-y divide-gray-200 dark:divide-white/5">
3
+ <% attribute_fields.each do |field| %>
4
+ <div class="px-4 py-4 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
5
+ <dt class="font-semibold text-gray-900 dark:text-white">
6
+ <%= repository.translate.field_label(field) %>
7
+ </dt>
8
+ <dd class="font-light text-gray-500 dark:text-gray-400 col-span-3">
9
+ <%= render(field.show_component(record: record, repository: repository)) %>
10
+ </dd>
11
+ </div>
12
+ <% end %>
13
+ </dl>
14
+ <% end %>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ module Ui
5
+ module Show
6
+ class AttributeFields < ViewComponent::Base
7
+ attr_reader :attribute_fields, :record, :repository
8
+
9
+ def initialize(attribute_fields:, record:, repository:)
10
+ super()
11
+ @attribute_fields = attribute_fields
12
+ @record = record
13
+ @repository = repository
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ <div role="status">
2
+ <svg aria-hidden="true" class="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
4
+ <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
5
+ </svg>
6
+ <span class="sr-only"><%= message %></span>
7
+ </div>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ module Ui
5
+ # https://flowbite.com/docs/components/spinner/
6
+ class Spinner < ViewComponent::Base
7
+ attr_reader :message
8
+
9
+ def initialize(message: "Loading...")
10
+ super()
11
+ @message = message
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ module Uchi
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Controller < ActionController::Base
5
+ layout "uchi/application"
6
+
7
+ protected
8
+
9
+ helper_method def uchi_user
10
+ current_user
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uchi/pagination/controller"
4
+
5
+ module Uchi
6
+ class RepositoryController < Uchi::ApplicationController
7
+ include Uchi::Pagination::Controller
8
+
9
+ before_action :set_repository
10
+
11
+ def create
12
+ @record = build_record
13
+ if @record.save
14
+ flash[:success] = @repository.translate.successful_create
15
+ redirect_to(@repository.routes.path_for(:show, id: @record.id), status: :see_other)
16
+ else
17
+ render :new, status: :unprocessable_entity
18
+ end
19
+ end
20
+
21
+ def destroy
22
+ @record = find_record
23
+ if @record.destroy
24
+ flash[:success] = @repository.translate.successful_destroy
25
+ redirect_to(@repository.routes.path_for(:index), status: :see_other)
26
+ else
27
+ flash[:alert] = @repository.translate.failed_destroy
28
+ redirect_to(@repository.routes.path_for(:show, id: @record.id), status: :see_other)
29
+ end
30
+ end
31
+
32
+ def edit
33
+ @record = find_record
34
+ end
35
+
36
+ def index
37
+ if params[:scope]
38
+ # Handle being shown inline in another record's show view
39
+ parent_repository = Uchi::Repository.for_model(params[:scope][:model])&.new
40
+ parent_record = parent_repository.find(params[:scope][:id])
41
+ field_name = params[:scope][:field]
42
+ inverse_of = params[:scope][:inverse_of]&.to_sym
43
+
44
+ @columns = @repository.fields_for_index
45
+ @columns = @columns.reject { |field| field.name == inverse_of } if inverse_of
46
+ @records = find_all_records_from_association(name: field_name, parent_record: parent_record)
47
+ @paginator, @records = paginate(@records, records_per_page: scoped_records_per_page)
48
+ else
49
+ # Handle the normal case
50
+ @columns = @repository.fields_for_index
51
+ @records = find_all_records
52
+ @paginator, @records = paginate(@records, records_per_page: index_records_per_page)
53
+ end
54
+ end
55
+
56
+ def new
57
+ @record = build_record
58
+ end
59
+
60
+ def show
61
+ @record = find_record
62
+ end
63
+
64
+ def update
65
+ @record = find_record
66
+ if @record.update(record_params)
67
+ flash[:success] = @repository.translate.successful_update
68
+ redirect_to(@repository.routes.path_for(:show, id: @record.id, uniq: rand), status: :see_other)
69
+ else
70
+ render :edit, status: :unprocessable_entity
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def build_record
77
+ @repository.build(record_params)
78
+ end
79
+
80
+ # Returns the path to use for the cancel link
81
+ helper_method def path_for_cancel(default:)
82
+ return default unless scoped?
83
+
84
+ parent_model_name = scope[:model]
85
+ parent_repository = Uchi::Repository.for_model(parent_model_name)&.new
86
+ raise NameError, "No repository found for scoped model #{parent_model_name}" unless parent_repository
87
+
88
+ parent_model_id = scope[:id]
89
+ parent_repository.routes.path_for(:show, id: parent_model_id)
90
+ end
91
+
92
+ helper_method def current_sort_order
93
+ @current_sort_order ||= SortOrder.from_params(params) || @repository.default_sort_order
94
+ end
95
+
96
+ def find_all_records(scope: nil)
97
+ @repository
98
+ .find_all(
99
+ scope: scope,
100
+ search: params[:query],
101
+ sort_order: current_sort_order
102
+ )
103
+ end
104
+
105
+ def find_all_records_from_association(name:, parent_record:)
106
+ association = parent_record.class.reflect_on_association(name.to_sym)
107
+ raise NameError, "No association named #{name} on #{parent_record.class}" unless association
108
+
109
+ source_repository = Uchi::Repository.for_model(association.active_record)&.new
110
+ raise NameError, "No repository found for scoped model #{association.active_record}" unless source_repository
111
+
112
+ associated_repository = Uchi::Repository.for_model(association.klass)&.new
113
+ raise NameError, "No repository found for associated model #{association.klass}" unless associated_repository
114
+
115
+ field = source_repository.fields.find { |f| f.name == name.to_sym }
116
+ raise NameError, "No field named #{name} on #{source_repository.model}" unless field
117
+
118
+ scope = parent_record.association(name.to_sym).scope
119
+ find_all_records(scope: scope)
120
+ end
121
+
122
+ def find_record
123
+ @record = @repository.find(params[:id])
124
+ end
125
+
126
+ # Returns the number of records per page to show in index views
127
+ def index_records_per_page
128
+ 25
129
+ end
130
+
131
+ def record_params
132
+ permitted_params = @repository.fields_for_edit.map(&:permitted_param)
133
+ (params[@repository.model_param_key] || ActionController::Parameters.new)
134
+ .permit(*permitted_params)
135
+ end
136
+
137
+ def repository_class
138
+ raise NotImplementedError, "Subclasses must implement repository_class"
139
+ end
140
+
141
+ helper_method def scope_params
142
+ if scoped?
143
+ scope.permit(:field, :id, :inverse_of, :model)
144
+ else
145
+ ActionController::Parameters.new
146
+ end
147
+ end
148
+
149
+ # Returns the scope that we're currently operating within, if any.
150
+ def scope
151
+ params[:scope]
152
+ end
153
+
154
+ helper_method def scoped?
155
+ scope.present?
156
+ end
157
+
158
+ def scoped_records_per_page
159
+ 5
160
+ end
161
+
162
+ def set_repository
163
+ @repository = repository_class.new
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,17 @@
1
+ module Uchi
2
+ module ApplicationHelper
3
+ # Maps Rails flash types to Flowbite Toast styles
4
+ def flash_style(flash_type)
5
+ case flash_type.to_sym
6
+ when :success
7
+ :success
8
+ when :alert, :error
9
+ :danger
10
+ when :warning
11
+ :warning
12
+ else
13
+ :default
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ module Uchi
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Uchi
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ <% if flash.any? %>
2
+ <div class="fixed bottom-4 right-4 z-50 space-y-4">
3
+ <% flash.to_hash.each do |type, message| %>
4
+ <%= render Flowbite::Toast.new(
5
+ message: message,
6
+ style: flash_style(type),
7
+ ) %>
8
+ <% end %>
9
+ </div>
10
+ <% end %>
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= content_for?(:page_title) ? yield(:page_title) : "Uchi" %></title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+
9
+ <%= javascript_include_tag "uchi/application", "data-turbo-track" => "reload" %>
10
+ <%= stylesheet_link_tag "uchi/application", media: "all", "data-turbo-track": "reload" %>
11
+ </head>
12
+ <body class="antialiased bg-gray-50 dark:bg-gray-900 p-2">
13
+ <div class="md:flex">
14
+ <nav class="w-64 h-full px-3 py-4 overflow-y-auto shrink-0 bg-gray-50 dark:bg-gray-800" aria-label="Main">
15
+ <ul class="space-y-2 font-medium">
16
+ <% Uchi::Repository.all.map { |repository| repository.new }.sort_by { |repository| repository.translate.plural_name.downcase }.each do |repository| %>
17
+ <li>
18
+ <%= link_to(repository.routes.path_for(:index), :class => "flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group") do %>
19
+ <span class="ms-3"><%= repository.translate.plural_name.titlecase %></span>
20
+ <% end %>
21
+ </li>
22
+ <% end %>
23
+ </ul>
24
+ </nav>
25
+
26
+ <main class="py-6 overflow-x-hidden grow md:px-6">
27
+ <%= yield %>
28
+ </main>
29
+ </div>
30
+
31
+ <%= render "layouts/uchi/flash_messages" %>
32
+ </body>
33
+ </html>
@@ -0,0 +1,40 @@
1
+ <%- content_for(:page_title) { @repository.translate.title(:edit, record: @record) } %>
2
+
3
+ <%= render(
4
+ Uchi::Ui::PageHeader.new(
5
+ description: @repository.translate.description(:edit),
6
+ title: @repository.translate.title(:edit, record: @record),
7
+ )) do |header| %>
8
+ <% header.with_breadcrumb(items: [
9
+ {href: @repository.routes.root_path, label: Rails.application.name.titlecase },
10
+ {href: @repository.routes.path_for(:index), label: @repository.translate.breadcrumb_label(:index) },
11
+ {href: @repository.routes.path_for(:show, id: @record.id), label: @repository.translate.breadcrumb_label(:show, record: @record) },
12
+ {href: @repository.routes.path_for(:edit, id: @record.id), label: @repository.translate.breadcrumb_label(:edit, record: @record) }
13
+ ]) %>
14
+ <% end %>
15
+
16
+ <div class="mx-4 md:mx-0">
17
+ <%= render(Flowbite::Card.new) do %>
18
+ <%= form_with model: @record, url: @repository.routes.path_for(:update, id: @record.id) do |form| %>
19
+ <div class="space-y-6">
20
+ <% @repository.fields_for_edit.each do |field| %>
21
+ <div>
22
+ <%= render(
23
+ field.edit_component(
24
+ form: form,
25
+ repository: @repository,
26
+ label: @repository.translate.field_label(field),
27
+ hint: @repository.translate.field_hint(field)
28
+ )
29
+ ) %>
30
+ </div>
31
+ <% end %>
32
+
33
+ <%= render(Uchi::Ui::Form::Footer.new) do |footer| %>
34
+ <%= footer.with_action { render(Flowbite::Button.new) { @repository.translate.submit_button } } %>
35
+ <%= footer.with_action { link_to(@repository.translate.link_to_cancel, path_for_cancel(default: @repository.routes.path_for(:show, id: @record.id))) } %>
36
+ <% end %>
37
+ </div>
38
+ <% end %>
39
+ <% end %>
40
+ </div>
@@ -0,0 +1,49 @@
1
+ <%- content_for(:page_title) { @repository.translate.title(:index) } %>
2
+
3
+ <%= render(
4
+ Uchi::Ui::PageHeader.new(
5
+ description: @repository.translate.description(:index),
6
+ title: @repository.translate.title(:index),
7
+ )) do |header| %>
8
+ <% header.with_action do %>
9
+ <%= link_to(@repository.translate.link_to_new, @repository.routes.path_for(:new), class: Flowbite::Button.classes) %>
10
+ <% end %>
11
+ <% header.with_breadcrumb(items: [
12
+ {href: @repository.routes.root_path, label: Rails.application.name.titlecase },
13
+ {href: @repository.routes.path_for(:index), label: @repository.translate.breadcrumb_label(:index) }
14
+ ]) %>
15
+ <% end %>
16
+
17
+ <div class="mx-0">
18
+ <%= render(Uchi::Ui::Index::TurboFrame.new(
19
+ repository: @repository,
20
+ scope: (scoped? ? scope_params : nil),
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
+ <% if @repository.searchable? %>
24
+ <div class="px-3 py-3">
25
+ <%= render(Uchi::Ui::Index::RecordsTable::SearchForm.new(params: params, repository: @repository)) %>
26
+ </div>
27
+ <% end %>
28
+ <% if @records.any? %>
29
+ <%= render(Uchi::Ui::Index::RecordsTable.new(
30
+ columns: @columns,
31
+ sort_order: current_sort_order,
32
+ query: params[:query],
33
+ records: @records,
34
+ repository: @repository,
35
+ scope: (scoped? ? scope_params : nil),
36
+ )) %>
37
+ <% else %>
38
+ <div class="p-6 text-center text-gray-500 dark:text-gray-400">
39
+ <%= @repository.translate.no_records_found %>
40
+ </div>
41
+ <% end %>
42
+ </div>
43
+ <% if @records.any? %>
44
+ <div class="my-4 flex justify-center">
45
+ <%= render(Uchi::Ui::Pagination.new(paginator: @paginator)) %>
46
+ </div>
47
+ <% end %>
48
+ <% end %>
49
+ </div>
@@ -0,0 +1,37 @@
1
+ <%- content_for(:page_title) { @repository.translate.title(:new) } %>
2
+
3
+ <%= render(
4
+ Uchi::Ui::PageHeader.new(
5
+ description: @repository.translate.description(:new),
6
+ title: @repository.translate.title(:new),
7
+ )) do |header| %>
8
+ <% header.with_breadcrumb(items: [
9
+ {href: @repository.routes.root_path, label: Rails.application.name.titlecase },
10
+ {href: @repository.routes.path_for(:index), label: @repository.translate.breadcrumb_label(:index) },
11
+ {href: @repository.routes.path_for(:new), label: @repository.translate.breadcrumb_label(:new) }
12
+ ]) %>
13
+ <% end %>
14
+
15
+ <%= render(Flowbite::Card.new) do %>
16
+ <%= form_with model: @record, url: @repository.routes.path_for(:create) do |form| %>
17
+ <div class="space-y-6">
18
+ <% @repository.fields_for_edit.each do |field| %>
19
+ <div>
20
+ <%= render(
21
+ field.edit_component(
22
+ form: form,
23
+ repository: @repository,
24
+ label: @repository.translate.field_label(field),
25
+ hint: @repository.translate.field_hint(field)
26
+ )
27
+ ) %>
28
+ </div>
29
+ <% end %>
30
+
31
+ <%= render(Uchi::Ui::Form::Footer.new) do |footer| %>
32
+ <%= footer.with_action { render(Flowbite::Button.new) { @repository.translate.submit_button } } %>
33
+ <%= footer.with_action { link_to(@repository.translate.link_to_cancel, path_for_cancel(default: @repository.routes.path_for(:index))) } %>
34
+ <% end %>
35
+ </div>
36
+ <% end %>
37
+ <% end %>
@@ -0,0 +1,41 @@
1
+ <%- content_for(:page_title) { @repository.translate.title(:show, record: @record) } %>
2
+
3
+ <%= render(
4
+ Uchi::Ui::PageHeader.new(
5
+ description: @repository.translate.description(:show, record: @record),
6
+ title: @repository.translate.title(:show, record: @record),
7
+ )
8
+ ) do |header| %>
9
+ <% header.with_action do %>
10
+ <%= button_to(
11
+ @repository.translate.link_to_destroy(@record),
12
+ @repository.routes.path_for(:destroy, id: @record.id),
13
+ method: :delete,
14
+ class: Flowbite::Button.classes(style: :red),
15
+ data: {
16
+ :"turbo-confirm" => @repository.translate.destroy_dialog_title(@record)
17
+ }
18
+ ) %>
19
+ <% end %>
20
+
21
+ <% header.with_action do %>
22
+ <%= link_to(@repository.translate.link_to_edit(@record), @repository.routes.path_for(:edit, id: @record.id), class: Flowbite::Button.classes) %>
23
+ <% end %>
24
+
25
+ <% header.with_breadcrumb(items: [
26
+ {href: @repository.routes.root_path, label: Rails.application.name.titlecase },
27
+ {href: @repository.routes.path_for(:index), label: @repository.translate.breadcrumb_label(:index) },
28
+ {href: @repository.routes.path_for(:show, id: @record.id), label: @repository.translate.breadcrumb_label(:show, record: @record) }
29
+ ]) %>
30
+ <% end %>
31
+
32
+ <%= render(Uchi::Ui::Show::AttributeFields.new(
33
+ attribute_fields: @repository.fields_for_show.select { |field| field.group_as(:show) == :attributes },
34
+ record: @record,
35
+ repository: @repository
36
+ )) %>
37
+
38
+ <% association_fields = @repository.fields_for_show.select{ |field| field.group_as(:show) == :associations } %>
39
+ <% association_fields.each do |field| %>
40
+ <%= render(field.show_component(record: @record, repository: @repository)) %>
41
+ <% end %>
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class ControllerGenerator < Rails::Generators::NamedBase
5
+ source_root File.expand_path("templates", __dir__)
6
+
7
+ def create_controller_file
8
+ destination = File.join("app/controllers/uchi")
9
+ template "controller.rb", File.join(destination, "#{plural_file_name}_controller.rb")
10
+ end
11
+
12
+ def add_route
13
+ route "resources :#{plural_file_name}", namespace: :uchi
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class <%= name.pluralize %>Controller < Uchi::RepositoryController
5
+ private
6
+
7
+ def repository_class
8
+ Uchi::Repositories::<%= class_name %>
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class InstallGenerator < Rails::Generators::Base
5
+ def create_namespace_in_routes
6
+ route "namespace :uchi do\nend"
7
+ end
8
+
9
+ def mount_engine
10
+ route "mount Uchi::Engine, at: \"/uchi\""
11
+ end
12
+ end
13
+ end