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,368 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Repository
5
+ class Translate
6
+ attr_reader :repository
7
+
8
+ # Returns the breadcrumb label for the given page.
9
+ #
10
+ # Example translation key:
11
+ # uchi.repository.author.breadcrumb.edit.label
12
+ def breadcrumb_label(page, record: nil)
13
+ return breadcrumb_label_for_index if page.to_sym == :index
14
+
15
+ default = {
16
+ show: title_for_record(record)
17
+ }[page.to_sym].presence
18
+ default ||= translate("common.#{page}", default: page.to_s.capitalize)
19
+
20
+ translate(
21
+ "label",
22
+ default: default,
23
+ model: singular_name,
24
+ record: title_for_record(record),
25
+ scope: i18n_scope("breadcrumb.#{page}")
26
+ )
27
+ end
28
+
29
+ # Returns the breadcrumb label for the index page.
30
+ #
31
+ # Returns the first of the following that is present:
32
+ # 1. Translation from "uchi.repository.author.breadcrumb.index.label"
33
+ # 2. Translation from "uchi.repository.author.index.title"
34
+ # 3. plural name of the model
35
+ # 4. Translation from "common.index"
36
+ # 5. Capitalized page name ("Index")
37
+ def breadcrumb_label_for_index
38
+ first_present_value(
39
+ translate(i18n_scope("breadcrumb.index.label"), default: nil),
40
+ translate(i18n_scope("index.title"), default: nil),
41
+ plural_name,
42
+ translate("common.index", default: nil),
43
+ "Index"
44
+ )
45
+ end
46
+
47
+ # Returns the label for the root breadcrumb item. Defaults to the
48
+ # application name.
49
+ #
50
+ # To customize this provide a translation for the key:
51
+ # `uchi.breadcrumb.root.label`
52
+ def breadcrumb_label_for_root
53
+ first_present_value(
54
+ translate("breadcrumb.root.label", default: nil),
55
+ Rails.application.name.titlecase
56
+ )
57
+ end
58
+
59
+ # Returns a description for the given page, or nil if none is found.
60
+ # This description is intended to provide additional context for the page
61
+ # being shown.
62
+ def description(page, record: nil)
63
+ translate(
64
+ "description",
65
+ default: nil,
66
+ model: singular_name,
67
+ record: record,
68
+ scope: i18n_scope(page)
69
+ )
70
+ end
71
+
72
+ # Returns a title for a dialog with the given name, e.g. :destroy.
73
+ # The title may include interpolation keys such as %{record}.
74
+ #
75
+ # Example translation key:
76
+ # uchi.repository.author.dialog.destroy.title
77
+ #
78
+ # Example default value:
79
+ # Are you sure you want to delete %{record}?
80
+ #
81
+ # Note that the default value itself is also looked up in the "common"
82
+ # scope, so that it can be shared across repositories.
83
+ #
84
+ # Example fallback translation key:
85
+ # common.dialog.destroy.title
86
+ #
87
+ # Example fallback default value:
88
+ # Are you sure you want to delete this record?
89
+ def destroy_dialog_title(record = nil)
90
+ translate(
91
+ "dialog.destroy.title",
92
+ default: translate(
93
+ "common.dialog.destroy.title",
94
+ default: "Are you sure?",
95
+ record: repository.title(record)
96
+ ),
97
+ record: repository.title(record),
98
+ scope: "uchi.repository.#{i18n_key}"
99
+ )
100
+ end
101
+
102
+ def failed_destroy
103
+ translate(
104
+ "destroy.failure",
105
+ default: translate(
106
+ "destroy.failure",
107
+ default: "The record could not be deleted",
108
+ scope: "common"
109
+ ),
110
+ scope: "uchi.repository.#{i18n_key}"
111
+ )
112
+ end
113
+
114
+ def initialize(repository:)
115
+ @repository = repository
116
+ end
117
+
118
+ # Returns the label for the given field.
119
+ def field_label(field)
120
+ translate(
121
+ "label",
122
+ default: model.human_attribute_name(field.name),
123
+ scope: i18n_scope("field.#{field.name}")
124
+ )
125
+ end
126
+
127
+ # Returns the hint for the given field, or nil if none is found.
128
+ def field_hint(field)
129
+ translate(
130
+ "hint",
131
+ default: nil,
132
+ scope: i18n_scope("field.#{field.name}")
133
+ )
134
+ end
135
+
136
+ def link_to_cancel
137
+ translate("common.cancel", default: "Cancel")
138
+ end
139
+
140
+ def link_to_destroy(record)
141
+ first_present_value(
142
+ translate(
143
+ "link_to_destroy",
144
+ default: nil,
145
+ model: singular_name,
146
+ record: repository.title(record),
147
+ scope: i18n_scope("button")
148
+ ),
149
+ translate(
150
+ "common.destroy",
151
+ default: nil,
152
+ model: singular_name,
153
+ record: repository.title(record)
154
+ ),
155
+ "Delete"
156
+ )
157
+ end
158
+
159
+ def link_to_edit(record)
160
+ first_present_value(
161
+ translate(
162
+ "link_to_edit",
163
+ default: nil,
164
+ model: singular_name,
165
+ record: repository.title(record),
166
+ scope: i18n_scope("button")
167
+ ),
168
+ translate(
169
+ "common.edit",
170
+ default: nil,
171
+ model: singular_name,
172
+ record: repository.title(record)
173
+ ),
174
+ "Edit"
175
+ )
176
+ end
177
+
178
+ # Returns the text for the "new" action link.
179
+ #
180
+ # Returns the first of the following translations that is present:
181
+ # 1. Translation from "uchi.repository.[name].button.link_to_new"
182
+ # 2. Translation from "uchi.common.new" with interpolation key %{model}
183
+ # 3. Default string "New %{model}"
184
+ def link_to_new
185
+ translate(
186
+ "link_to_new",
187
+ default: translate("common.new", default: "New %{model}"),
188
+ model: singular_name,
189
+ scope: i18n_scope("button")
190
+ )
191
+ end
192
+
193
+ def loading_message
194
+ translate("loading", default: "Loading...", scope: "uchi.repository.common")
195
+ end
196
+
197
+ # Returns the label for the navigation link to this repository's index
198
+ # page.
199
+ #
200
+ # Returns the first of the following that is present:
201
+ # 1. Translation from "uchi.repository.[name].navigation.label"
202
+ # 2. Translation from "uchi.repository.[name].index.title"
203
+ # 3. plural name of the model
204
+ def navigation_label
205
+ first_present_value(
206
+ translate(i18n_scope("navigation.label"), default: nil),
207
+ translate(i18n_scope("index.title"), default: nil),
208
+ plural_name
209
+ )
210
+ end
211
+
212
+ # Returns the localized, human-readable plural name of the model this
213
+ # repository manages.
214
+ def plural_name
215
+ ::I18n.translate(
216
+ "uchi.repository.#{i18n_key}.model",
217
+ count: 2,
218
+ default: model.model_name.plural.humanize
219
+ )
220
+ end
221
+
222
+ def no_records_found
223
+ translate("no_records_found", default: "No records found", scope: "uchi.common")
224
+ end
225
+
226
+ def search_label
227
+ translate("common.search", default: "Search")
228
+ end
229
+
230
+ def search_button
231
+ translate("common.search", default: "Search")
232
+ end
233
+
234
+ # Returns the label for a generic submit button
235
+ def submit_button
236
+ translate("common.save", default: "Save")
237
+ end
238
+
239
+ def successful_create
240
+ translate(
241
+ "create.success",
242
+ default: translate(
243
+ "create.success",
244
+ default: "Your changes have been saved",
245
+ scope: "common"
246
+ ),
247
+ scope: "uchi.repository.#{i18n_key}"
248
+ )
249
+ end
250
+
251
+ def successful_destroy
252
+ translate(
253
+ "destroy.success",
254
+ default: translate(
255
+ "destroy.success",
256
+ default: "The record has been deleted",
257
+ scope: "common"
258
+ ),
259
+ scope: "uchi.repository.#{i18n_key}"
260
+ )
261
+ end
262
+
263
+ def successful_update
264
+ translate(
265
+ "update.success",
266
+ default: translate(
267
+ "update.success",
268
+ default: "Your changes have been saved",
269
+ scope: "common"
270
+ ),
271
+ scope: "uchi.repository.#{i18n_key}"
272
+ )
273
+ end
274
+
275
+ def title_for_edit(record)
276
+ first_present_value(
277
+ translate(
278
+ "title",
279
+ default: nil,
280
+ model: singular_name,
281
+ record: repository.title(record),
282
+ scope: i18n_scope(:edit)
283
+ ),
284
+ link_to_edit(record)
285
+ )
286
+ end
287
+
288
+ def title_for_index
289
+ translate(
290
+ "title",
291
+ default: plural_name,
292
+ model: singular_name,
293
+ scope: i18n_scope(:index)
294
+ )
295
+ end
296
+
297
+ def title_for_show(record)
298
+ return repository.title(record) if record
299
+
300
+ translate(
301
+ "title",
302
+ default: plural_name,
303
+ model: singular_name,
304
+ scope: i18n_scope(:show)
305
+ )
306
+ end
307
+
308
+ # Returns the title for the "new" page.
309
+ #
310
+ # Returns the first of the following translations that is present:
311
+ # 1. Translation from "uchi.repository.[name].new.title"
312
+ # 2. Translation from "uchi.repository.[name].button.link_to_new"
313
+ # 3. Translation from "uchi.common.new" with interpolation key %{model}
314
+ # 4. Default string "New %{model}"
315
+ def title_for_new
316
+ first_present_value(
317
+ translate(i18n_scope("new.title"), default: nil),
318
+ link_to_new
319
+ )
320
+ end
321
+
322
+ private
323
+
324
+ # Returns the first translation that yields a present value by looking
325
+ # through each key in keys in order.
326
+ def first_present_value(*values)
327
+ values.find(&:presence)
328
+ end
329
+
330
+ # Returns the segment of the i18n key specific to this repository.
331
+ def i18n_key
332
+ @repository.model.model_name.i18n_key
333
+ end
334
+
335
+ def i18n_scope(section = nil)
336
+ [
337
+ "uchi.repository",
338
+ i18n_key,
339
+ section
340
+ ].compact.join(".")
341
+ end
342
+
343
+ def model
344
+ @model ||= repository.model
345
+ end
346
+
347
+ # Returns the localized, human-readable singular name of the model this
348
+ # repository manages.
349
+ def singular_name
350
+ ::I18n.translate(
351
+ "uchi.repository.#{i18n_key}.model",
352
+ count: 1,
353
+ default: model.model_name.human(count: 1)
354
+ )
355
+ end
356
+
357
+ def title_for_record(record)
358
+ return nil unless record
359
+
360
+ repository.title(record)
361
+ end
362
+
363
+ def translate(key, **options)
364
+ Uchi::I18n.translate(key, **options)
365
+ end
366
+ end
367
+ end
368
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "repository/routes"
4
+
5
+ module Uchi
6
+ class Repository
7
+ class << self
8
+ # Returns all defined Uchi::Repository classes
9
+ def all
10
+ Uchi::Repositories.constants.map { |const_name|
11
+ Uchi::Repositories.const_get(const_name)
12
+ }
13
+ end
14
+
15
+ # Returns the repository for the given model, or nil if none is found.
16
+ def for_model(model)
17
+ all.find { |repository| repository.model.to_s == model.to_s }
18
+ end
19
+
20
+ # Returns the model class this repository manages.
21
+ def model
22
+ @model ||= name.demodulize.constantize
23
+ end
24
+ end
25
+
26
+ # Returns a new, unsaved instance of the model this repository manages.
27
+ def build(attributes = {})
28
+ model.new(attributes)
29
+ end
30
+
31
+ # Returns the "name" of the controller that handles requests for this
32
+ # repository. Note that this is different from the controllers class name
33
+ # and is intended for generating URLs.
34
+ def controller_name
35
+ model_param_key.pluralize
36
+ end
37
+
38
+ def default_sort_order
39
+ SortOrder.new(:id, :asc)
40
+ end
41
+
42
+ # Returns an array of fields to show on the edit page.
43
+ def fields_for_edit
44
+ fields.select { |field| field.on.include?(:edit) }.each { |field| field.repository = self }
45
+ end
46
+
47
+ # Returns an array of fields to show on the index page.
48
+ def fields_for_index
49
+ fields.select { |field| field.on.include?(:index) }.each { |field| field.repository = self }
50
+ end
51
+
52
+ # Returns an array of fields to show on the show page.
53
+ def fields_for_show
54
+ fields.select { |field| field.on.include?(:show) }.each { |field| field.repository = self }
55
+ end
56
+
57
+ def find_all(search: nil, scope: model.all, sort_order: default_sort_order)
58
+ scope ||= model.all
59
+ query = scope.includes(includes)
60
+ query = apply_search(query, search)
61
+ apply_sort_order(query, sort_order)
62
+ end
63
+
64
+ def find(id)
65
+ model.find(id)
66
+ end
67
+
68
+ # Returns the list of associations to include when querying for records.
69
+ #
70
+ # See
71
+ # https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-includes
72
+ # for further details.
73
+ def includes
74
+ []
75
+ end
76
+
77
+ def model
78
+ self.class.model
79
+ end
80
+
81
+ def model_param_key
82
+ model.model_name.param_key
83
+ end
84
+
85
+ # Returns an instance of Uchi::Repository::Routes for this repository,
86
+ # which can be used to generate paths and URLs.
87
+ #
88
+ # @return [Uchi::Repository::Routes]
89
+ def routes
90
+ @routes ||= Routes.new(self)
91
+ end
92
+
93
+ # Returns true if this repository has at least one searchable field.
94
+ #
95
+ # @return [Boolean]
96
+ def searchable?
97
+ searchable_fields.any?
98
+ end
99
+
100
+ # Returns the title to show for a given record. By default, this method
101
+ # returns the value of the first of the following methods that exist:
102
+ #
103
+ # 1. `name`
104
+ # 2. `title`
105
+ # 3. `to_s`
106
+ #
107
+ # You can override this method in your repository subclass to provide
108
+ # custom logic.
109
+ def title(record)
110
+ return nil unless record
111
+
112
+ [:name, :title, :to_s].each do |method|
113
+ if record.respond_to?(method)
114
+ return record.public_send(method)
115
+ end
116
+ end
117
+ end
118
+
119
+ # Provides access to translation helpers specific to this repository.
120
+ def translate
121
+ @translate ||= Translate.new(repository: self)
122
+ end
123
+
124
+ private
125
+
126
+ def apply_search(query, search)
127
+ return query unless search.present?
128
+ return query if searchable_fields.empty?
129
+
130
+ search = search.strip
131
+ conditions = searchable_fields.map { |field|
132
+ arel_field = model.arel_table[field.name]
133
+ Arel::Nodes::NamedFunction.new(
134
+ "CAST",
135
+ [arel_field.as(Arel::Nodes::SqlLiteral.new("VARCHAR"))]
136
+ ).matches("%#{search}%")
137
+ }
138
+ query.where(conditions.inject(:or))
139
+ end
140
+
141
+ def apply_sort_order(query, sort_order)
142
+ field_to_sort_by = fields.find { |field| field.name == sort_order.name }
143
+ return query unless field_to_sort_by
144
+
145
+ if field_to_sort_by.sortable.respond_to?(:call)
146
+ field_to_sort_by.sortable.call(query, sort_order.direction)
147
+ else
148
+ sort_order.apply(query)
149
+ end
150
+ end
151
+
152
+ def searchable_fields
153
+ @searchable_fields ||= fields.select { |field| field.searchable? }
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,35 @@
1
+ module Uchi
2
+ class SortOrder
3
+ attr_reader :name, :direction
4
+
5
+ class << self
6
+ def from_params(params)
7
+ sort_params = params[:sort] || {}
8
+ return nil if sort_params.empty?
9
+
10
+ by = sort_params[:by]
11
+ return by unless by
12
+
13
+ direction = sort_params[:direction] || :asc
14
+ new(by, direction)
15
+ end
16
+ end
17
+
18
+ def ascending?
19
+ direction == :asc
20
+ end
21
+
22
+ def apply(query)
23
+ query.order(name => direction)
24
+ end
25
+
26
+ def descending?
27
+ direction == :desc
28
+ end
29
+
30
+ def initialize(name, direction)
31
+ @name = name.to_sym
32
+ @direction = direction.to_sym
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ VERSION = "0.1.5"
5
+ end
data/lib/uchi.rb ADDED
@@ -0,0 +1,18 @@
1
+ gem "turbo-rails"
2
+ require "turbo-rails"
3
+
4
+ gem "view_component"
5
+ require "view_component"
6
+
7
+ require "uchi/version"
8
+ require "uchi/engine"
9
+
10
+ require "uchi/field"
11
+ require "uchi/i18n"
12
+ require "uchi/repository"
13
+ require "uchi/sort_order"
14
+ require "uchi/repository/translate"
15
+
16
+ module Uchi
17
+ # Your code goes here...
18
+ end
data/uchi.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/uchi/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "uchi"
7
+ spec.version = Uchi::VERSION
8
+ spec.authors = ["Jakob Skjerning"]
9
+ spec.email = ["jakob@substancelab.com"]
10
+
11
+ spec.summary = "Build usable and extensible admin panels for your Ruby on Rails application in minutes."
12
+ spec.description = "Level up your scaffolds with a modern admin backend framework, designed for Rails developers who demand both beauty, functionality, and extensibility. Uchi provides a set of components and conventions for creating user interfaces that are both powerful and easy to use."
13
+ spec.homepage = "https://www.uchiadmin.com/"
14
+ spec.required_ruby_version = ">= 3.2.0"
15
+
16
+ spec.metadata["changelog_uri"] = "https://github.com/substancelab/uchi/blob/main/CHANGELOG.md"
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/substancelab/uchi"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = \
23
+ Dir["app/**/*"] +
24
+ Dir["lib/**/*"] +
25
+ [
26
+ "README.md",
27
+ "uchi.gemspec"
28
+ ]
29
+
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_dependency "rails", ">= 7.2"
33
+ spec.add_dependency "turbo-rails"
34
+ spec.add_dependency "view_component", ">= 4.0"
35
+ end