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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/uchi/application.js +2655 -24
- data/app/assets/javascripts/uchi.js +11 -0
- data/app/assets/stylesheets/uchi/application.css +982 -1311
- data/app/assets/tailwind/uchi.css +4 -4
- data/app/components/uchi/field/belongs_to/edit.html.erb +1 -1
- data/app/components/uchi/field/belongs_to/index.html.erb +1 -1
- data/app/components/uchi/field/belongs_to/show.html.erb +3 -3
- data/app/components/uchi/field/belongs_to.rb +38 -8
- data/app/components/uchi/field/boolean/edit.html.erb +1 -1
- data/app/components/uchi/field/date/edit.html.erb +1 -1
- data/app/components/uchi/field/date_time/edit.html.erb +1 -1
- data/app/components/uchi/field/file/edit.html.erb +1 -1
- data/app/components/uchi/field/file/index.html.erb +2 -1
- data/app/components/uchi/field/file/show.html.erb +3 -2
- data/app/components/uchi/field/has_and_belongs_to_many/show.html.erb +5 -3
- data/app/components/uchi/field/has_many/show.html.erb +5 -3
- data/app/components/uchi/field/id/index.html.erb +1 -1
- data/app/components/uchi/field/id/show.html.erb +1 -1
- data/app/components/uchi/field/image/edit.html.erb +1 -1
- data/app/components/uchi/field/image/index.html.erb +2 -1
- data/app/components/uchi/field/image/show.html.erb +2 -1
- data/app/components/uchi/field/number/edit.html.erb +1 -1
- data/app/components/uchi/field/string/edit.html.erb +1 -1
- data/app/components/uchi/field/text/edit.html.erb +1 -1
- data/app/components/{flowbite → uchi/flowbite}/breadcrumb.rb +5 -5
- data/app/components/{flowbite → uchi/flowbite}/breadcrumb_home.rb +2 -2
- data/app/components/{flowbite → uchi/flowbite}/breadcrumb_item/current.rb +3 -3
- data/app/components/{flowbite → uchi/flowbite}/breadcrumb_item/first.rb +4 -4
- data/app/components/{flowbite → uchi/flowbite}/breadcrumb_item.rb +4 -4
- data/app/components/{flowbite → uchi/flowbite}/breadcrumb_separator.rb +7 -5
- data/app/components/uchi/flowbite/button/outline.rb +37 -0
- data/app/components/uchi/flowbite/button/pill.rb +40 -0
- data/app/components/uchi/flowbite/button.rb +93 -0
- data/app/components/uchi/flowbite/card/card.html.erb +7 -0
- data/app/components/uchi/flowbite/card/title.rb +37 -0
- data/app/components/uchi/flowbite/card.rb +84 -0
- data/app/components/{flowbite → uchi/flowbite}/input/checkbox.rb +7 -7
- data/app/components/{flowbite → uchi/flowbite}/input/date.rb +1 -1
- data/app/components/{flowbite → uchi/flowbite}/input/date_time.rb +1 -1
- data/app/components/{flowbite → uchi/flowbite}/input/email.rb +1 -1
- data/app/components/{flowbite → uchi/flowbite}/input/field.rb +11 -10
- data/app/components/uchi/flowbite/input/file.rb +30 -0
- data/app/components/{flowbite → uchi/flowbite}/input/hint.rb +6 -5
- data/app/components/{flowbite → uchi/flowbite}/input/label.rb +8 -7
- data/app/components/{flowbite → uchi/flowbite}/input/number.rb +1 -1
- data/app/components/{flowbite → uchi/flowbite}/input/password.rb +1 -1
- data/app/components/{flowbite → uchi/flowbite}/input/phone.rb +1 -1
- data/app/components/{flowbite → uchi/flowbite}/input/radio_button.rb +7 -7
- data/app/components/{flowbite → uchi/flowbite}/input/select.rb +16 -7
- data/app/components/uchi/flowbite/input/textarea.rb +42 -0
- data/app/components/{flowbite → uchi/flowbite}/input/url.rb +1 -1
- data/app/components/uchi/flowbite/input/validation_error.rb +39 -0
- data/app/components/uchi/flowbite/input_field/checkbox.html.erb +9 -0
- data/app/components/{flowbite → uchi/flowbite}/input_field/checkbox.rb +10 -6
- data/app/components/{flowbite → uchi/flowbite}/input_field/date.rb +2 -2
- data/app/components/{flowbite → uchi/flowbite}/input_field/date_time.rb +2 -2
- data/app/components/{flowbite → uchi/flowbite}/input_field/email.rb +2 -2
- data/app/components/{flowbite → uchi/flowbite}/input_field/file.rb +2 -2
- data/app/components/uchi/flowbite/input_field/input_field.html.erb +9 -0
- data/app/components/{flowbite → uchi/flowbite}/input_field/number.rb +2 -2
- data/app/components/{flowbite → uchi/flowbite}/input_field/password.rb +2 -2
- data/app/components/{flowbite → uchi/flowbite}/input_field/phone.rb +2 -2
- data/app/components/uchi/flowbite/input_field/radio_button.html.erb +9 -0
- data/app/components/{flowbite → uchi/flowbite}/input_field/radio_button.rb +13 -9
- data/app/components/{flowbite → uchi/flowbite}/input_field/select.rb +7 -3
- data/app/components/{flowbite → uchi/flowbite}/input_field/text.rb +1 -1
- data/app/components/{flowbite → uchi/flowbite}/input_field/textarea.rb +2 -2
- data/app/components/{flowbite → uchi/flowbite}/input_field/url.rb +2 -2
- data/app/components/{flowbite → uchi/flowbite}/input_field.rb +19 -5
- data/app/components/uchi/flowbite/link.rb +41 -0
- data/app/components/{flowbite → uchi/flowbite}/style.rb +1 -1
- data/app/components/{flowbite → uchi/flowbite}/toast/icon.rb +1 -1
- data/app/components/uchi/flowbite/toast/toast.html.erb +40 -0
- data/app/components/{flowbite → uchi/flowbite}/toast.rb +2 -2
- data/app/components/uchi/ui/actions/dropdown/dropdown.html.erb +59 -0
- data/app/components/uchi/ui/actions/dropdown.rb +32 -0
- data/app/components/uchi/ui/breadcrumb/breadcrumb.html.erb +4 -4
- data/app/components/uchi/ui/form/input/collection_checkboxes.html.erb +2 -1
- data/app/components/uchi/ui/form/input/collection_checkboxes.rb +12 -17
- data/app/components/uchi/ui/frame/frame.html.erb +6 -1
- data/app/components/uchi/ui/index/records_table/records_table.html.erb +109 -18
- data/app/components/uchi/ui/index/records_table/search_form/search_form.html.erb +21 -5
- data/app/components/uchi/ui/navigation/navigation.html.erb +9 -0
- data/app/components/uchi/ui/navigation.rb +37 -0
- data/app/components/uchi/ui/no_value/no_value.html.erb +1 -0
- data/app/components/uchi/ui/no_value.rb +8 -0
- data/app/components/uchi/ui/page_header/page_header.html.erb +8 -3
- data/app/components/uchi/ui/pagination/current_link.html.erb +1 -1
- data/app/components/uchi/ui/pagination/gap.html.erb +7 -1
- data/app/components/uchi/ui/pagination/link.html.erb +1 -1
- data/app/components/uchi/ui/pagination/next_link.html.erb +18 -3
- data/app/components/uchi/ui/pagination/pagination.html.erb +3 -1
- data/app/components/uchi/ui/pagination/previous_link.html.erb +18 -3
- data/app/components/uchi/ui/pagination.rb +1 -1
- data/app/components/uchi/ui/show/attribute_fields/attribute_fields.html.erb +4 -3
- data/app/components/uchi/ui/spinner/spinner.html.erb +17 -3
- data/app/controllers/uchi/actions/executions_controller.rb +107 -0
- data/app/controllers/uchi/repository_controller.rb +21 -1
- data/app/views/layouts/uchi/_flash_messages.html.erb +1 -1
- data/app/views/layouts/uchi/application.html.erb +10 -16
- data/app/views/uchi/navigation/_main.html.erb +9 -0
- data/app/views/uchi/repository/edit.html.erb +5 -5
- data/app/views/uchi/repository/index.html.erb +15 -6
- data/app/views/uchi/repository/new.html.erb +5 -5
- data/app/views/uchi/repository/show.html.erb +16 -5
- data/lib/generators/uchi/controller/templates/controller.rb.tt +0 -5
- data/lib/generators/uchi/scaffold/scaffold_generator.rb +15 -0
- data/lib/uchi/action.rb +84 -0
- data/lib/uchi/action_response.rb +108 -0
- data/lib/uchi/repository/translate.rb +117 -19
- data/lib/uchi/repository.rb +25 -0
- data/lib/uchi/version.rb +1 -1
- data/lib/uchi.rb +2 -0
- metadata +63 -50
- data/app/components/flowbite/button/outline.rb +0 -22
- data/app/components/flowbite/button/pill.rb +0 -40
- data/app/components/flowbite/button.rb +0 -92
- data/app/components/flowbite/card.rb +0 -45
- data/app/components/flowbite/input/file.rb +0 -30
- data/app/components/flowbite/input/textarea.rb +0 -42
- data/app/components/flowbite/input/validation_error.rb +0 -11
- data/app/components/flowbite/input_field/checkbox.html.erb +0 -14
- data/app/components/flowbite/input_field/input_field.html.erb +0 -8
- data/app/components/flowbite/input_field/radio_button.html.erb +0 -14
- data/app/components/flowbite/link.rb +0 -21
- data/app/components/flowbite/toast/toast.html.erb +0 -11
- /data/app/components/{flowbite → uchi/flowbite}/toast/icon.html.erb +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
<%- content_for(:page_title) { @repository.translate.
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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-
|
|
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.
|
|
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.
|
|
6
|
+
title: @repository.translate.title_for_new,
|
|
7
7
|
)) do |header| %>
|
|
8
8
|
<% header.with_breadcrumb(items: [
|
|
9
|
-
{href: @repository.routes.root_path, label:
|
|
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.
|
|
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.
|
|
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: :
|
|
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:
|
|
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 %>
|
|
@@ -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
|
data/lib/uchi/action.rb
ADDED
|
@@ -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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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}",
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
|
data/lib/uchi/repository.rb
CHANGED
|
@@ -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