uchi 0.1.4 → 0.1.6

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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/uchi/application.js +2655 -24
  3. data/app/assets/javascripts/uchi.js +11 -0
  4. data/app/assets/stylesheets/uchi/application.css +982 -1311
  5. data/app/assets/tailwind/uchi.css +4 -4
  6. data/app/components/uchi/field/belongs_to/edit.html.erb +1 -1
  7. data/app/components/uchi/field/belongs_to/index.html.erb +1 -1
  8. data/app/components/uchi/field/belongs_to/show.html.erb +3 -3
  9. data/app/components/uchi/field/belongs_to.rb +38 -8
  10. data/app/components/uchi/field/boolean/edit.html.erb +1 -1
  11. data/app/components/uchi/field/date/edit.html.erb +1 -1
  12. data/app/components/uchi/field/date_time/edit.html.erb +1 -1
  13. data/app/components/uchi/field/file/edit.html.erb +1 -1
  14. data/app/components/uchi/field/file/index.html.erb +2 -1
  15. data/app/components/uchi/field/file/show.html.erb +3 -2
  16. data/app/components/uchi/field/has_and_belongs_to_many/show.html.erb +5 -3
  17. data/app/components/uchi/field/has_many/show.html.erb +5 -3
  18. data/app/components/uchi/field/id/index.html.erb +1 -1
  19. data/app/components/uchi/field/id/show.html.erb +1 -1
  20. data/app/components/uchi/field/image/edit.html.erb +1 -1
  21. data/app/components/uchi/field/image/index.html.erb +2 -1
  22. data/app/components/uchi/field/image/show.html.erb +2 -1
  23. data/app/components/uchi/field/number/edit.html.erb +1 -1
  24. data/app/components/uchi/field/string/edit.html.erb +1 -1
  25. data/app/components/uchi/field/text/edit.html.erb +1 -1
  26. data/app/components/{flowbite → uchi/flowbite}/breadcrumb.rb +5 -5
  27. data/app/components/{flowbite → uchi/flowbite}/breadcrumb_home.rb +2 -2
  28. data/app/components/{flowbite → uchi/flowbite}/breadcrumb_item/current.rb +3 -3
  29. data/app/components/{flowbite → uchi/flowbite}/breadcrumb_item/first.rb +4 -4
  30. data/app/components/{flowbite → uchi/flowbite}/breadcrumb_item.rb +4 -4
  31. data/app/components/{flowbite → uchi/flowbite}/breadcrumb_separator.rb +7 -5
  32. data/app/components/uchi/flowbite/button/outline.rb +37 -0
  33. data/app/components/uchi/flowbite/button/pill.rb +40 -0
  34. data/app/components/uchi/flowbite/button.rb +93 -0
  35. data/app/components/uchi/flowbite/card/card.html.erb +7 -0
  36. data/app/components/uchi/flowbite/card/title.rb +37 -0
  37. data/app/components/uchi/flowbite/card.rb +84 -0
  38. data/app/components/{flowbite → uchi/flowbite}/input/checkbox.rb +7 -7
  39. data/app/components/{flowbite → uchi/flowbite}/input/date.rb +1 -1
  40. data/app/components/{flowbite → uchi/flowbite}/input/date_time.rb +1 -1
  41. data/app/components/{flowbite → uchi/flowbite}/input/email.rb +1 -1
  42. data/app/components/{flowbite → uchi/flowbite}/input/field.rb +11 -10
  43. data/app/components/uchi/flowbite/input/file.rb +30 -0
  44. data/app/components/{flowbite → uchi/flowbite}/input/hint.rb +6 -5
  45. data/app/components/{flowbite → uchi/flowbite}/input/label.rb +8 -7
  46. data/app/components/{flowbite → uchi/flowbite}/input/number.rb +1 -1
  47. data/app/components/{flowbite → uchi/flowbite}/input/password.rb +1 -1
  48. data/app/components/{flowbite → uchi/flowbite}/input/phone.rb +1 -1
  49. data/app/components/{flowbite → uchi/flowbite}/input/radio_button.rb +7 -7
  50. data/app/components/{flowbite → uchi/flowbite}/input/select.rb +16 -7
  51. data/app/components/uchi/flowbite/input/textarea.rb +42 -0
  52. data/app/components/{flowbite → uchi/flowbite}/input/url.rb +1 -1
  53. data/app/components/uchi/flowbite/input/validation_error.rb +39 -0
  54. data/app/components/uchi/flowbite/input_field/checkbox.html.erb +9 -0
  55. data/app/components/{flowbite → uchi/flowbite}/input_field/checkbox.rb +10 -6
  56. data/app/components/{flowbite → uchi/flowbite}/input_field/date.rb +2 -2
  57. data/app/components/{flowbite → uchi/flowbite}/input_field/date_time.rb +2 -2
  58. data/app/components/{flowbite → uchi/flowbite}/input_field/email.rb +2 -2
  59. data/app/components/{flowbite → uchi/flowbite}/input_field/file.rb +2 -2
  60. data/app/components/uchi/flowbite/input_field/input_field.html.erb +9 -0
  61. data/app/components/{flowbite → uchi/flowbite}/input_field/number.rb +2 -2
  62. data/app/components/{flowbite → uchi/flowbite}/input_field/password.rb +2 -2
  63. data/app/components/{flowbite → uchi/flowbite}/input_field/phone.rb +2 -2
  64. data/app/components/uchi/flowbite/input_field/radio_button.html.erb +9 -0
  65. data/app/components/{flowbite → uchi/flowbite}/input_field/radio_button.rb +13 -9
  66. data/app/components/{flowbite → uchi/flowbite}/input_field/select.rb +7 -3
  67. data/app/components/{flowbite → uchi/flowbite}/input_field/text.rb +1 -1
  68. data/app/components/{flowbite → uchi/flowbite}/input_field/textarea.rb +2 -2
  69. data/app/components/{flowbite → uchi/flowbite}/input_field/url.rb +2 -2
  70. data/app/components/{flowbite → uchi/flowbite}/input_field.rb +19 -5
  71. data/app/components/uchi/flowbite/link.rb +41 -0
  72. data/app/components/{flowbite → uchi/flowbite}/style.rb +1 -1
  73. data/app/components/{flowbite → uchi/flowbite}/toast/icon.rb +1 -1
  74. data/app/components/uchi/flowbite/toast/toast.html.erb +40 -0
  75. data/app/components/{flowbite → uchi/flowbite}/toast.rb +2 -2
  76. data/app/components/uchi/ui/actions/dropdown/dropdown.html.erb +59 -0
  77. data/app/components/uchi/ui/actions/dropdown.rb +32 -0
  78. data/app/components/uchi/ui/breadcrumb/breadcrumb.html.erb +4 -4
  79. data/app/components/uchi/ui/form/input/collection_checkboxes.html.erb +2 -1
  80. data/app/components/uchi/ui/form/input/collection_checkboxes.rb +12 -17
  81. data/app/components/uchi/ui/frame/frame.html.erb +6 -1
  82. data/app/components/uchi/ui/index/records_table/records_table.html.erb +109 -18
  83. data/app/components/uchi/ui/index/records_table/search_form/search_form.html.erb +21 -5
  84. data/app/components/uchi/ui/navigation/navigation.html.erb +9 -0
  85. data/app/components/uchi/ui/navigation.rb +37 -0
  86. data/app/components/uchi/ui/no_value/no_value.html.erb +1 -0
  87. data/app/components/uchi/ui/no_value.rb +8 -0
  88. data/app/components/uchi/ui/page_header/page_header.html.erb +8 -3
  89. data/app/components/uchi/ui/pagination/current_link.html.erb +1 -1
  90. data/app/components/uchi/ui/pagination/gap.html.erb +7 -1
  91. data/app/components/uchi/ui/pagination/link.html.erb +1 -1
  92. data/app/components/uchi/ui/pagination/next_link.html.erb +18 -3
  93. data/app/components/uchi/ui/pagination/pagination.html.erb +3 -1
  94. data/app/components/uchi/ui/pagination/previous_link.html.erb +18 -3
  95. data/app/components/uchi/ui/pagination.rb +1 -1
  96. data/app/components/uchi/ui/show/attribute_fields/attribute_fields.html.erb +4 -3
  97. data/app/components/uchi/ui/spinner/spinner.html.erb +17 -3
  98. data/app/controllers/uchi/actions/executions_controller.rb +107 -0
  99. data/app/controllers/uchi/repository_controller.rb +21 -1
  100. data/app/views/layouts/uchi/_flash_messages.html.erb +1 -1
  101. data/app/views/layouts/uchi/application.html.erb +10 -16
  102. data/app/views/uchi/navigation/_main.html.erb +9 -0
  103. data/app/views/uchi/repository/edit.html.erb +5 -5
  104. data/app/views/uchi/repository/index.html.erb +15 -6
  105. data/app/views/uchi/repository/new.html.erb +5 -5
  106. data/app/views/uchi/repository/show.html.erb +16 -5
  107. data/lib/generators/uchi/controller/templates/controller.rb.tt +0 -5
  108. data/lib/generators/uchi/scaffold/scaffold_generator.rb +15 -0
  109. data/lib/uchi/action.rb +84 -0
  110. data/lib/uchi/action_response.rb +108 -0
  111. data/lib/uchi/repository/translate.rb +117 -19
  112. data/lib/uchi/repository.rb +25 -0
  113. data/lib/uchi/version.rb +1 -1
  114. data/lib/uchi.rb +2 -0
  115. metadata +63 -50
  116. data/app/components/flowbite/button/outline.rb +0 -22
  117. data/app/components/flowbite/button/pill.rb +0 -40
  118. data/app/components/flowbite/button.rb +0 -92
  119. data/app/components/flowbite/card.rb +0 -45
  120. data/app/components/flowbite/input/file.rb +0 -30
  121. data/app/components/flowbite/input/textarea.rb +0 -42
  122. data/app/components/flowbite/input/validation_error.rb +0 -11
  123. data/app/components/flowbite/input_field/checkbox.html.erb +0 -14
  124. data/app/components/flowbite/input_field/input_field.html.erb +0 -8
  125. data/app/components/flowbite/input_field/radio_button.html.erb +0 -14
  126. data/app/components/flowbite/link.rb +0 -21
  127. data/app/components/flowbite/toast/toast.html.erb +0 -11
  128. /data/app/components/{flowbite → uchi/flowbite}/toast/icon.html.erb +0 -0
@@ -0,0 +1,9 @@
1
+ <nav
2
+ class="
3
+ w-64 h-full px-3 py-4 overflow-y-auto shrink-0
4
+ bg-neutral-secondary-medium
5
+ "
6
+ aria-label="Main"
7
+ >
8
+ <%= render Uchi::Ui::Navigation.new %>
9
+ </nav>
@@ -1,12 +1,12 @@
1
- <%- content_for(:page_title) { @repository.translate.title(:edit, record: @record) } %>
1
+ <%- content_for(:page_title) { @repository.translate.title_for_edit(@record) } %>
2
2
 
3
3
  <%= render(
4
4
  Uchi::Ui::PageHeader.new(
5
5
  description: @repository.translate.description(:edit),
6
- title: @repository.translate.title(:edit, record: @record),
6
+ title: @repository.translate.title_for_edit(@record),
7
7
  )) do |header| %>
8
8
  <% header.with_breadcrumb(items: [
9
- {href: @repository.routes.root_path, label: Rails.application.name.titlecase },
9
+ {href: @repository.routes.root_path, label: @repository.translate.breadcrumb_label_for_root },
10
10
  {href: @repository.routes.path_for(:index), label: @repository.translate.breadcrumb_label(:index) },
11
11
  {href: @repository.routes.path_for(:show, id: @record.id), label: @repository.translate.breadcrumb_label(:show, record: @record) },
12
12
  {href: @repository.routes.path_for(:edit, id: @record.id), label: @repository.translate.breadcrumb_label(:edit, record: @record) }
@@ -14,7 +14,7 @@
14
14
  <% end %>
15
15
 
16
16
  <div class="mx-4 md:mx-0">
17
- <%= render(Flowbite::Card.new) do %>
17
+ <%= render(Uchi::Flowbite::Card.new) do %>
18
18
  <%= form_with model: @record, url: @repository.routes.path_for(:update, id: @record.id) do |form| %>
19
19
  <div class="space-y-6">
20
20
  <% @repository.fields_for_edit.each do |field| %>
@@ -31,7 +31,7 @@
31
31
  <% end %>
32
32
 
33
33
  <%= render(Uchi::Ui::Form::Footer.new) do |footer| %>
34
- <%= footer.with_action { render(Flowbite::Button.new) { @repository.translate.submit_button } } %>
34
+ <%= footer.with_action { render(Uchi::Flowbite::Button.new) { @repository.translate.submit_button } } %>
35
35
  <%= footer.with_action { link_to(@repository.translate.link_to_cancel, path_for_cancel(default: @repository.routes.path_for(:show, id: @record.id))) } %>
36
36
  <% end %>
37
37
  </div>
@@ -1,15 +1,16 @@
1
- <%- content_for(:page_title) { @repository.translate.title(:index) } %>
1
+ <%- content_for(:page_title) { @repository.translate.title_for_index } %>
2
2
 
3
3
  <%= render(
4
4
  Uchi::Ui::PageHeader.new(
5
5
  description: @repository.translate.description(:index),
6
- title: @repository.translate.title(:index),
6
+ title: @repository.translate.title_for_index,
7
7
  )) do |header| %>
8
8
  <% header.with_action do %>
9
- <%= link_to(@repository.translate.link_to_new, @repository.routes.path_for(:new), class: Flowbite::Button.classes) %>
9
+ <%= link_to(@repository.translate.link_to_new, @repository.routes.path_for(:new), class: Uchi::Flowbite::Button.classes) %>
10
10
  <% end %>
11
+
11
12
  <% header.with_breadcrumb(items: [
12
- {href: @repository.routes.root_path, label: Rails.application.name.titlecase },
13
+ {href: @repository.routes.root_path, label: @repository.translate.breadcrumb_label_for_root },
13
14
  {href: @repository.routes.path_for(:index), label: @repository.translate.breadcrumb_label(:index) }
14
15
  ]) %>
15
16
  <% end %>
@@ -19,12 +20,19 @@
19
20
  repository: @repository,
20
21
  scope: (scoped? ? scope_params : nil),
21
22
  )) 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
+ <%# Styles from https://flowbite.com/docs/components/tables/#default-table %>
24
+ <div
25
+ class="
26
+ relative overflow-x-auto bg-neutral-primary-soft rounded-base border
27
+ border-default shadow-xs
28
+ "
29
+ >
23
30
  <% if @repository.searchable? %>
24
31
  <div class="px-3 py-3">
25
32
  <%= render(Uchi::Ui::Index::RecordsTable::SearchForm.new(params: params, repository: @repository)) %>
26
33
  </div>
27
34
  <% end %>
35
+
28
36
  <% if @records.any? %>
29
37
  <%= render(Uchi::Ui::Index::RecordsTable.new(
30
38
  columns: @columns,
@@ -35,11 +43,12 @@
35
43
  scope: (scoped? ? scope_params : nil),
36
44
  )) %>
37
45
  <% else %>
38
- <div class="p-6 text-center text-gray-500 dark:text-gray-400">
46
+ <div class="p-6 text-center text-body-subtle">
39
47
  <%= @repository.translate.no_records_found %>
40
48
  </div>
41
49
  <% end %>
42
50
  </div>
51
+
43
52
  <% if @records.any? %>
44
53
  <div class="my-4 flex justify-center">
45
54
  <%= render(Uchi::Ui::Pagination.new(paginator: @paginator)) %>
@@ -1,18 +1,18 @@
1
- <%- content_for(:page_title) { @repository.translate.title(:new) } %>
1
+ <%- content_for(:page_title) { @repository.translate.title_for_new } %>
2
2
 
3
3
  <%= render(
4
4
  Uchi::Ui::PageHeader.new(
5
5
  description: @repository.translate.description(:new),
6
- title: @repository.translate.title(:new),
6
+ title: @repository.translate.title_for_new,
7
7
  )) do |header| %>
8
8
  <% header.with_breadcrumb(items: [
9
- {href: @repository.routes.root_path, label: Rails.application.name.titlecase },
9
+ {href: @repository.routes.root_path, label: @repository.translate.breadcrumb_label_for_root },
10
10
  {href: @repository.routes.path_for(:index), label: @repository.translate.breadcrumb_label(:index) },
11
11
  {href: @repository.routes.path_for(:new), label: @repository.translate.breadcrumb_label(:new) }
12
12
  ]) %>
13
13
  <% end %>
14
14
 
15
- <%= render(Flowbite::Card.new) do %>
15
+ <%= render(Uchi::Flowbite::Card.new) do %>
16
16
  <%= form_with model: @record, url: @repository.routes.path_for(:create) do |form| %>
17
17
  <div class="space-y-6">
18
18
  <% @repository.fields_for_edit.each do |field| %>
@@ -29,7 +29,7 @@
29
29
  <% end %>
30
30
 
31
31
  <%= render(Uchi::Ui::Form::Footer.new) do |footer| %>
32
- <%= footer.with_action { render(Flowbite::Button.new) { @repository.translate.submit_button } } %>
32
+ <%= footer.with_action { render(Uchi::Flowbite::Button.new) { @repository.translate.submit_button } } %>
33
33
  <%= footer.with_action { link_to(@repository.translate.link_to_cancel, path_for_cancel(default: @repository.routes.path_for(:index))) } %>
34
34
  <% end %>
35
35
  </div>
@@ -1,17 +1,27 @@
1
- <%- content_for(:page_title) { @repository.translate.title(:show, record: @record) } %>
1
+ <%- content_for(:page_title) { @repository.translate.title_for_show(@record) } %>
2
2
 
3
3
  <%= render(
4
4
  Uchi::Ui::PageHeader.new(
5
5
  description: @repository.translate.description(:show, record: @record),
6
- title: @repository.translate.title(:show, record: @record),
6
+ title: @repository.translate.title_for_show(@record),
7
7
  )
8
8
  ) do |header| %>
9
+ <% if @repository.actions.any? %>
10
+ <% header.with_action do %>
11
+ <%= render(Uchi::Ui::Actions::Dropdown.new(
12
+ actions: @repository.actions,
13
+ record: @record,
14
+ repository: @repository
15
+ )) %>
16
+ <% end %>
17
+ <% end %>
18
+
9
19
  <% header.with_action do %>
10
20
  <%= button_to(
11
21
  @repository.translate.link_to_destroy(@record),
12
22
  @repository.routes.path_for(:destroy, id: @record.id),
13
23
  method: :delete,
14
- class: Flowbite::Button.classes(style: :red),
24
+ class: Uchi::Flowbite::Button.classes(style: :danger),
15
25
  data: {
16
26
  :"turbo-confirm" => @repository.translate.destroy_dialog_title(@record)
17
27
  }
@@ -19,11 +29,11 @@
19
29
  <% end %>
20
30
 
21
31
  <% 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) %>
32
+ <%= link_to(@repository.translate.link_to_edit(@record), @repository.routes.path_for(:edit, id: @record.id), class: Uchi::Flowbite::Button.classes) %>
23
33
  <% end %>
24
34
 
25
35
  <% header.with_breadcrumb(items: [
26
- {href: @repository.routes.root_path, label: Rails.application.name.titlecase },
36
+ {href: @repository.routes.root_path, label: @repository.translate.breadcrumb_label_for_root },
27
37
  {href: @repository.routes.path_for(:index), label: @repository.translate.breadcrumb_label(:index) },
28
38
  {href: @repository.routes.path_for(:show, id: @record.id), label: @repository.translate.breadcrumb_label(:show, record: @record) }
29
39
  ]) %>
@@ -36,6 +46,7 @@
36
46
  )) %>
37
47
 
38
48
  <% association_fields = @repository.fields_for_show.select{ |field| field.group_as(:show) == :associations } %>
49
+
39
50
  <% association_fields.each do |field| %>
40
51
  <%= render(field.show_component(record: @record, repository: @repository)) %>
41
52
  <% end %>
@@ -2,10 +2,5 @@
2
2
 
3
3
  module Uchi
4
4
  class <%= name.pluralize %>Controller < Uchi::RepositoryController
5
- private
6
-
7
- def repository_class
8
- Uchi::Repositories::<%= class_name %>
9
- end
10
5
  end
11
6
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/rails/resource/resource_generator"
4
+
5
+ module Uchi
6
+ class ScaffoldGenerator < Rails::Generators::ResourceGenerator # :nodoc:
7
+ remove_hook_for :resource_controller
8
+ remove_hook_for :resource_route
9
+ remove_class_option :actions
10
+
11
+ def generate_repository
12
+ generate("uchi:repository", name)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ # Base class for all Uchi actions.
5
+ #
6
+ # Actions allow you to perform custom operations on one or more records from
7
+ # a repository. Examples include publishing posts, exporting data, or sending
8
+ # notifications.
9
+ #
10
+ # To create an action, subclass this class and implement the `perform` method:
11
+ #
12
+ # class PublishPost < Uchi::Action
13
+ # def perform(records, input = {})
14
+ # records.each { |record| record.update!(published: true) }
15
+ # Uchi::ActionResponse.success("Published #{records.size} posts")
16
+ # end
17
+ # end
18
+ #
19
+ # Actions are registered on repositories via the `actions` method:
20
+ #
21
+ # class PostRepository < Uchi::Repository
22
+ # def actions
23
+ # [PublishPost.new]
24
+ # end
25
+ # end
26
+ class Action
27
+ # Returns the display name for this action.
28
+ #
29
+ # By default, this looks up the translation key
30
+ # `uchi.action.[action_key].name` and falls back to the humanized class
31
+ # name.
32
+ #
33
+ # @return [String]
34
+ def name
35
+ translate(:name, default: self.class.name.demodulize.titleize)
36
+ end
37
+
38
+ # Returns the list of fields to show in the action form.
39
+ #
40
+ # Fields are instances of Uchi::Field subclasses (e.g., Field::String,
41
+ # Field::Boolean).
42
+ #
43
+ # @return [Array<Uchi::Field>]
44
+ def fields
45
+ []
46
+ end
47
+
48
+ # Performs the action on the given records.
49
+ #
50
+ # This method must be implemented in subclasses.
51
+ #
52
+ # @param records [ActiveRecord::Relation, Array] - The records to operate on
53
+ # @param input [Hash] - Hash of field values from the action form
54
+ # @return [Uchi::ActionResponse]
55
+ def perform(records, input = {})
56
+ raise NotImplementedError, "#{self.class}#perform must be implemented"
57
+ end
58
+
59
+ # Returns true if this action requires input fields.
60
+ #
61
+ # @return [Boolean]
62
+ def requires_input?
63
+ fields.any?
64
+ end
65
+
66
+ private
67
+
68
+ # Looks up i18n key with fallback
69
+ # @param key [Symbol] - the key to look up (e.g., :name)
70
+ # @param default [String] - fallback value
71
+ # @param options [Hash] - additional i18n options (count, etc.)
72
+ # @return [String, nil]
73
+ def translate(key, default:, **options)
74
+ i18n_key = "action.#{translation_key}.#{key}"
75
+ Uchi::I18n.translate(i18n_key, default: default, **options)
76
+ end
77
+
78
+ # Returns the i18n key segment for this action
79
+ # Example: PublishPost -> "publish_post"
80
+ def translation_key
81
+ self.class.name.underscore.tr("/", ".")
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ # Represents the response from executing an action.
5
+ #
6
+ # ActionResponse uses a builder pattern for fluent chaining:
7
+ #
8
+ # ActionResponse.success("Done").redirect_to(path: "/posts")
9
+ # ActionResponse.error("Failed").message("Try again later")
10
+ # ActionResponse.success("Exported").download(file_path: path, filename: "data.csv")
11
+ class ActionResponse
12
+ attr_reader :status, :message_text, :redirect_path, :file_path, :filename, :turbo_stream_block
13
+
14
+ # Creates a successful response with an optional message.
15
+ #
16
+ # @param message [String, nil] - Success message to display
17
+ # @return [ActionResponse]
18
+ def self.success(message = nil)
19
+ new(status: :success, message: message)
20
+ end
21
+
22
+ # Creates an error response with an optional message.
23
+ #
24
+ # @param message [String, nil] - Error message to display
25
+ # @return [ActionResponse]
26
+ def self.error(message = nil)
27
+ new(status: :error, message: message)
28
+ end
29
+
30
+ def initialize(status:, message: nil)
31
+ @status = status
32
+ @message_text = message
33
+ end
34
+
35
+ # Sets the message to display.
36
+ #
37
+ # @param text [String] - The message text
38
+ # @return [ActionResponse] self for chaining
39
+ def message(text)
40
+ @message_text = text
41
+ self
42
+ end
43
+
44
+ # Sets a redirect path.
45
+ #
46
+ # @param path [String] - The path to redirect to
47
+ # @return [ActionResponse] self for chaining
48
+ def redirect_to(path:)
49
+ @redirect_path = path
50
+ self
51
+ end
52
+
53
+ # Sets a file to download.
54
+ #
55
+ # @param file_path [String] - Path to the file
56
+ # @param filename [String] - Name for the downloaded file
57
+ # @return [ActionResponse] self for chaining
58
+ def download(file_path:, filename:)
59
+ @file_path = file_path
60
+ @filename = filename
61
+ self
62
+ end
63
+
64
+ # Sets a custom Turbo Stream response.
65
+ #
66
+ # @yield [stream] Provides a Turbo Stream builder
67
+ # @return [ActionResponse] self for chaining
68
+ def turbo_stream(&block)
69
+ @turbo_stream_block = block
70
+ self
71
+ end
72
+
73
+ # Returns true if this is a success response.
74
+ #
75
+ # @return [Boolean]
76
+ def success?
77
+ status == :success
78
+ end
79
+
80
+ # Returns true if this is an error response.
81
+ #
82
+ # @return [Boolean]
83
+ def error?
84
+ status == :error
85
+ end
86
+
87
+ # Returns true if this response includes a redirect.
88
+ #
89
+ # @return [Boolean]
90
+ def redirect?
91
+ !redirect_path.nil?
92
+ end
93
+
94
+ # Returns true if this response includes a download.
95
+ #
96
+ # @return [Boolean]
97
+ def download?
98
+ !file_path.nil?
99
+ end
100
+
101
+ # Returns true if this response includes a custom Turbo Stream.
102
+ #
103
+ # @return [Boolean]
104
+ def turbo_stream?
105
+ !turbo_stream_block.nil?
106
+ end
107
+ end
108
+ end
@@ -5,6 +5,20 @@ module Uchi
5
5
  class Translate
6
6
  attr_reader :repository
7
7
 
8
+ # Returns the label for the actions button in the UI.
9
+ #
10
+ # Returns the first of the following that is present:
11
+ # 1. uchi.repository.author.button.actions
12
+ # 2. uchi.common.actions
13
+ # 3. "Actions"
14
+ def actions_button_label
15
+ first_present_value(
16
+ translate(i18n_scope("button.actions"), default: nil),
17
+ translate("common.actions", default: nil),
18
+ "Actions"
19
+ )
20
+ end
21
+
8
22
  # Returns the breadcrumb label for the given page.
9
23
  #
10
24
  # Example translation key:
@@ -39,11 +53,23 @@ module Uchi
39
53
  translate(i18n_scope("breadcrumb.index.label"), default: nil),
40
54
  translate(i18n_scope("index.title"), default: nil),
41
55
  plural_name,
42
- translate("common.index"),
56
+ translate("common.index", default: nil),
43
57
  "Index"
44
58
  )
45
59
  end
46
60
 
61
+ # Returns the label for the root breadcrumb item. Defaults to the
62
+ # application name.
63
+ #
64
+ # To customize this provide a translation for the key:
65
+ # `uchi.breadcrumb.root.label`
66
+ def breadcrumb_label_for_root
67
+ first_present_value(
68
+ translate("breadcrumb.root.label", default: nil),
69
+ Rails.application.name.titlecase
70
+ )
71
+ end
72
+
47
73
  # Returns a description for the given page, or nil if none is found.
48
74
  # This description is intended to provide additional context for the page
49
75
  # being shown.
@@ -126,30 +152,53 @@ module Uchi
126
152
  end
127
153
 
128
154
  def link_to_destroy(record)
129
- translate(
130
- "link_to_destroy",
131
- default: "Delete",
132
- model: singular_name,
133
- record: repository.title(record),
134
- scope: i18n_scope("button")
155
+ first_present_value(
156
+ translate(
157
+ "link_to_destroy",
158
+ default: nil,
159
+ model: singular_name,
160
+ record: repository.title(record),
161
+ scope: i18n_scope("button")
162
+ ),
163
+ translate(
164
+ "common.destroy",
165
+ default: nil,
166
+ model: singular_name,
167
+ record: repository.title(record)
168
+ ),
169
+ "Delete"
135
170
  )
136
171
  end
137
172
 
138
173
  def link_to_edit(record)
139
- translate(
140
- "link_to_edit",
141
- default: "Edit",
142
- model: singular_name,
143
- record: repository.title(record),
144
- scope: i18n_scope("button")
174
+ first_present_value(
175
+ translate(
176
+ "link_to_edit",
177
+ default: nil,
178
+ model: singular_name,
179
+ record: repository.title(record),
180
+ scope: i18n_scope("button")
181
+ ),
182
+ translate(
183
+ "common.edit",
184
+ default: nil,
185
+ model: singular_name,
186
+ record: repository.title(record)
187
+ ),
188
+ "Edit"
145
189
  )
146
190
  end
147
191
 
148
192
  # Returns the text for the "new" action link.
193
+ #
194
+ # Returns the first of the following translations that is present:
195
+ # 1. Translation from "uchi.repository.[name].button.link_to_new"
196
+ # 2. Translation from "uchi.common.new" with interpolation key %{model}
197
+ # 3. Default string "New %{model}"
149
198
  def link_to_new
150
199
  translate(
151
200
  "link_to_new",
152
- default: "New %{model}", # rubocop:disable Style/FormatStringToken
201
+ default: translate("common.new", default: "New %{model}"),
153
202
  model: singular_name,
154
203
  scope: i18n_scope("button")
155
204
  )
@@ -159,6 +208,21 @@ module Uchi
159
208
  translate("loading", default: "Loading...", scope: "uchi.repository.common")
160
209
  end
161
210
 
211
+ # Returns the label for the navigation link to this repository's index
212
+ # page.
213
+ #
214
+ # Returns the first of the following that is present:
215
+ # 1. Translation from "uchi.repository.[name].navigation.label"
216
+ # 2. Translation from "uchi.repository.[name].index.title"
217
+ # 3. plural name of the model
218
+ def navigation_label
219
+ first_present_value(
220
+ translate(i18n_scope("navigation.label"), default: nil),
221
+ translate(i18n_scope("index.title"), default: nil),
222
+ plural_name
223
+ )
224
+ end
225
+
162
226
  # Returns the localized, human-readable plural name of the model this
163
227
  # repository manages.
164
228
  def plural_name
@@ -222,16 +286,50 @@ module Uchi
222
286
  )
223
287
  end
224
288
 
225
- # Returns the title for the given page.
226
- def title(page, record: nil)
227
- return repository.title(record) if record && page == :show
289
+ def title_for_edit(record)
290
+ first_present_value(
291
+ translate(
292
+ "title",
293
+ default: nil,
294
+ model: singular_name,
295
+ record: repository.title(record),
296
+ scope: i18n_scope(:edit)
297
+ ),
298
+ link_to_edit(record)
299
+ )
300
+ end
228
301
 
302
+ def title_for_index
229
303
  translate(
230
304
  "title",
231
305
  default: plural_name,
232
306
  model: singular_name,
233
- record: record,
234
- scope: i18n_scope(page)
307
+ scope: i18n_scope(:index)
308
+ )
309
+ end
310
+
311
+ def title_for_show(record)
312
+ return repository.title(record) if record
313
+
314
+ translate(
315
+ "title",
316
+ default: plural_name,
317
+ model: singular_name,
318
+ scope: i18n_scope(:show)
319
+ )
320
+ end
321
+
322
+ # Returns the title for the "new" page.
323
+ #
324
+ # Returns the first of the following translations that is present:
325
+ # 1. Translation from "uchi.repository.[name].new.title"
326
+ # 2. Translation from "uchi.repository.[name].button.link_to_new"
327
+ # 3. Translation from "uchi.common.new" with interpolation key %{model}
328
+ # 4. Default string "New %{model}"
329
+ def title_for_new
330
+ first_present_value(
331
+ translate(i18n_scope("new.title"), default: nil),
332
+ link_to_new
235
333
  )
236
334
  end
237
335
 
@@ -61,6 +61,16 @@ module Uchi
61
61
  apply_sort_order(query, sort_order)
62
62
  end
63
63
 
64
+ # Finds multiple records by their IDs. If a record is not found, it is
65
+ # ignored.
66
+ #
67
+ # @param ids [Array<Integer>] The IDs of the records to find
68
+ #
69
+ # @return [ActiveRecord::Relation] The found records
70
+ def find_many(ids)
71
+ model.where(id: ids)
72
+ end
73
+
64
74
  def find(id)
65
75
  model.find(id)
66
76
  end
@@ -74,6 +84,21 @@ module Uchi
74
84
  []
75
85
  end
76
86
 
87
+ # Returns the list of actions available for this repository.
88
+ #
89
+ # Actions are instances of Uchi::Action subclasses that can be executed
90
+ # on one or more records.
91
+ #
92
+ # Example:
93
+ # def actions
94
+ # [PublishPost.new, ExportToCsv.new]
95
+ # end
96
+ #
97
+ # @return [Array<Uchi::Action>]
98
+ def actions
99
+ []
100
+ end
101
+
77
102
  def model
78
103
  self.class.model
79
104
  end
data/lib/uchi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uchi
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.6"
5
5
  end
data/lib/uchi.rb CHANGED
@@ -7,6 +7,8 @@ require "view_component"
7
7
  require "uchi/version"
8
8
  require "uchi/engine"
9
9
 
10
+ require "uchi/action"
11
+ require "uchi/action_response"
10
12
  require "uchi/field"
11
13
  require "uchi/i18n"
12
14
  require "uchi/repository"