oversee 0.1.0 → 0.2.0
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/README.md +53 -14
- data/app/components/oversee/{application_component.rb → base.rb} +1 -1
- data/app/components/oversee/card.rb +13 -0
- data/app/components/oversee/dashboard/filters.rb +54 -0
- data/app/components/oversee/dashboard/header.rb +41 -0
- data/app/components/oversee/dashboard/index.rb +24 -0
- data/app/components/oversee/dashboard/javascript.rb +9 -0
- data/app/components/oversee/dashboard/pagination.rb +23 -0
- data/app/components/oversee/dashboard/sidebar.rb +53 -0
- data/app/components/oversee/dashboard/tailwind.rb +33 -0
- data/app/components/oversee/field/display.rb +29 -0
- data/app/components/oversee/field/form.rb +49 -0
- data/app/components/oversee/field/input/boolean.rb +23 -0
- data/app/components/oversee/field/input/datetime.rb +20 -0
- data/app/components/oversee/field/input/integer.rb +20 -0
- data/app/components/oversee/field/input/string.rb +20 -0
- data/app/components/oversee/field/input.rb +26 -0
- data/app/components/oversee/field/label.rb +24 -0
- data/app/components/oversee/field/value/boolean.rb +50 -0
- data/app/components/oversee/field/value/datetime.rb +9 -0
- data/app/components/oversee/field/value/enum.rb +9 -0
- data/app/components/oversee/field/value/integer.rb +9 -0
- data/app/components/oversee/field/value/string.rb +20 -0
- data/app/components/oversee/field/value/text.rb +9 -0
- data/app/components/oversee/field/value.rb +34 -0
- data/app/components/oversee/resources/base.rb +4 -0
- data/app/components/oversee/resources/errors.rb +41 -0
- data/app/components/oversee/resources/form.rb +49 -0
- data/app/components/oversee/resources/index.rb +26 -0
- data/app/components/oversee/resources/new.rb +17 -0
- data/app/components/oversee/resources/show.rb +126 -0
- data/app/components/oversee/resources/table.rb +109 -0
- data/app/components/oversee/table/body.rb +13 -0
- data/app/components/oversee/table/data.rb +5 -0
- data/app/components/oversee/table/head.rb +9 -0
- data/app/components/oversee/table/row.rb +9 -0
- data/app/components/oversee/table.rb +21 -0
- data/app/controllers/oversee/dashboard_controller.rb +4 -0
- data/app/controllers/oversee/resources_controller.rb +79 -21
- data/app/models/oversee/resource.rb +36 -0
- data/app/oversee/filter.rb +39 -0
- data/app/oversee/resource.rb +10 -0
- data/app/oversee/search.rb +34 -0
- data/app/views/layouts/oversee/application.html.erb +6 -6
- data/config/locales/en.yml +4 -0
- data/config/locales/oversee.en.yml +4 -0
- data/config/routes.rb +12 -6
- data/lib/oversee/configuration.rb +3 -0
- data/lib/oversee/version.rb +1 -1
- data/lib/oversee.rb +19 -2
- metadata +73 -47
- data/app/assets/config/oversee_manifest.js +0 -1
- data/app/assets/stylesheets/oversee/application.css +0 -15
- data/app/components/oversee/card_component.rb +0 -15
- data/app/components/oversee/dashboard/sidebar_component.rb +0 -127
- data/app/components/oversee/field_component.rb +0 -36
- data/app/components/oversee/field_label_component.rb +0 -155
- data/app/components/oversee/fields/display_row_component.rb +0 -58
- data/app/components/oversee/fields/input/datetime_component.rb +0 -27
- data/app/components/oversee/fields/input/integer_component.rb +0 -26
- data/app/components/oversee/fields/input/string_component.rb +0 -26
- data/app/components/oversee/fields/value/boolean_component.rb +0 -44
- data/app/components/oversee/fields/value/datetime_component.rb +0 -16
- data/app/components/oversee/fields/value/enum_component.rb +0 -16
- data/app/components/oversee/fields/value/integer_component.rb +0 -16
- data/app/components/oversee/fields/value/string_component.rb +0 -23
- data/app/components/oversee/fields/value/text_component.rb +0 -16
- data/app/helpers/oversee/application_helper.rb +0 -5
- data/app/mailers/oversee/application_mailer.rb +0 -6
- data/app/views/oversee/application/_javascript.html.erb +0 -17
- data/app/views/oversee/dashboard/show.html.erb +0 -18
- data/app/views/oversee/resources/_form.html.erb +0 -10
- data/app/views/oversee/resources/_input_field.html.erb +0 -20
- data/app/views/oversee/resources/edit.html.erb +0 -31
- data/app/views/oversee/resources/index.html.erb +0 -59
- data/app/views/oversee/resources/input_field.html.erb +0 -1
- data/app/views/oversee/resources/input_field.turbo_stream.erb +0 -3
- data/app/views/oversee/resources/show.html.erb +0 -51
- data/app/views/oversee/resources/update.turbo_stream.erb +0 -3
- data/app/views/shared/_sidebar.html.erb +0 -32
@@ -0,0 +1,20 @@
|
|
1
|
+
class Oversee::Field::Value::String < Phlex::HTML
|
2
|
+
def initialize(key: nil, value: nil, kind: :value)
|
3
|
+
@key = key
|
4
|
+
@value = value
|
5
|
+
@kind = kind
|
6
|
+
end
|
7
|
+
|
8
|
+
def view_template
|
9
|
+
return p(class: "text-gray-400 text-xs uppercase") { "Empty" } if @value == ""
|
10
|
+
return p(class: "text-gray-400 text-xs uppercase") { "Redacted" } if sensitive?
|
11
|
+
|
12
|
+
p(class: "truncate") { @value }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def sensitive?
|
18
|
+
@key&.downcase&.include?("password") || @key&.downcase&.include?("token")
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Oversee::Field::Value < Oversee::Base
|
2
|
+
MAP = {
|
3
|
+
string: Oversee::Field::Value::String,
|
4
|
+
boolean: Oversee::Field::Value::Boolean,
|
5
|
+
integer: Oversee::Field::Value::Integer,
|
6
|
+
datetime: Oversee::Field::Value::Datetime,
|
7
|
+
text: Oversee::Field::Value::Text,
|
8
|
+
enum: Oversee::Field::Value::Enum
|
9
|
+
}
|
10
|
+
|
11
|
+
def initialize(key: nil, value: nil, datatype: :string, **options)
|
12
|
+
@key = key
|
13
|
+
@value = value
|
14
|
+
@datatype = datatype
|
15
|
+
@options = options
|
16
|
+
|
17
|
+
@class = @options[:class]
|
18
|
+
end
|
19
|
+
|
20
|
+
def view_template
|
21
|
+
return p(class: "text-gray-400 text-xs"){ "—" } if @value.nil?
|
22
|
+
render component_class.new(key: @key, value: @value)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def component_class
|
28
|
+
MAP[@datatype.to_sym] || Oversee::Field::Value::String
|
29
|
+
end
|
30
|
+
|
31
|
+
def for_table?
|
32
|
+
@options[:for_table] || false
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Oversee::Resources::Errors < Oversee::Base
|
4
|
+
def initialize(resource: nil, errors: nil, content: nil, **options)
|
5
|
+
@resource = resource
|
6
|
+
@errors = errors
|
7
|
+
@content = content
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def view_template
|
12
|
+
if @content || @resource&.errors&.any?
|
13
|
+
div(class: container_class) do
|
14
|
+
div(class: "inline-flex items-center gap-1.5 bg-red-600 py-1 px-2 text-xs text-white") do
|
15
|
+
# render Phlex::TablerIcons::ExclamationCircle.new(class: "size-5", stroke_width: 1.75)
|
16
|
+
h3(class: "text-sm font-medium") { plain @content || "Something went wrong!" }
|
17
|
+
end
|
18
|
+
if displayed_errors.present?
|
19
|
+
div(class: "mt-4 border-l-2 border-red-600 p-2") do
|
20
|
+
ul(class: "space-y-1 text-sm text-gray-700") do
|
21
|
+
displayed_errors.each do |message|
|
22
|
+
li { "— #{message}" }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def container_class
|
34
|
+
@options[:container_class] || "bg-white mb-4"
|
35
|
+
end
|
36
|
+
|
37
|
+
def displayed_errors
|
38
|
+
@errors || @resource&.errors&.full_messages
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Oversee::Resources::Form < Oversee::Base
|
4
|
+
include Phlex::Rails::Helpers::FormWith
|
5
|
+
def initialize(resource:)
|
6
|
+
@resource = resource
|
7
|
+
end
|
8
|
+
|
9
|
+
def view_template
|
10
|
+
div(id: dom_id(@resource)) do
|
11
|
+
render Oversee::Resources::Errors.new(resource: @resource)
|
12
|
+
form_with model: @resource,
|
13
|
+
scope: :resource,
|
14
|
+
url: helpers.create_resource_path(resource_class_name:),
|
15
|
+
scope: :resource do |f|
|
16
|
+
@resource.class.columns_hash.each do |key, metadata|
|
17
|
+
if [@resource.class.primary_key, "created_at", "updated_at"].include?(key)
|
18
|
+
next
|
19
|
+
end
|
20
|
+
div(class: "py-2") do
|
21
|
+
render Oversee::Field::Label.new(
|
22
|
+
key: key,
|
23
|
+
datatype: metadata.sql_type_metadata.type
|
24
|
+
)
|
25
|
+
div(class: "mt-2") do
|
26
|
+
render Oversee::Field::Input.new(
|
27
|
+
datatype: metadata.sql_type_metadata.type,
|
28
|
+
key: key
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
hr(class: "-mx-8 my-8")
|
34
|
+
div(class: "flex justify-end mt-8") do
|
35
|
+
plain f.submit "Save",
|
36
|
+
class:
|
37
|
+
"bg-gray-900 px-6 py-2 rounded-full text-white font-medium text-sm hover:bg-gray-700 cursor-pointer"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def resource_class_name
|
47
|
+
@resource.class.to_s
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Oversee::Resources::Index < Oversee::Base
|
4
|
+
def initialize(resources:, resource_class:, pagy:, params:)
|
5
|
+
@resources = resources
|
6
|
+
@resource_class = resource_class
|
7
|
+
@pagy = pagy
|
8
|
+
@params = params
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def view_template
|
13
|
+
render Oversee::Dashboard::Header.new(title: @resource_class.to_s, subtitle: "Index") do
|
14
|
+
a(href: helpers.new_resource_path(@params[:resource_class_name]), class: "inline-flex items-center justify-center size-8 rounded-full bg-emerald-100 text-emerald-500 hover:bg-emerald-200 text-sm font-medium") { render Phlex::Icons::Iconoir::Plus.new(class: "size-4 text-emerald-500") }
|
15
|
+
end
|
16
|
+
render Oversee::Dashboard::Filters.new(params: @params)
|
17
|
+
render Oversee::Resources::Table.new(resource_class: @resource_class, resources: @resources, params: @params)
|
18
|
+
render Oversee::Dashboard::Pagination.new(pagy: @pagy, params: @params)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def resource_associations
|
24
|
+
@resource_class.reflect_on_all_associations.select { |association| association.macro == :belongs_to }
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Oversee::Resources::New < Oversee::Base
|
4
|
+
def initialize(resource:, resource_class:, params:)
|
5
|
+
@resource = resource
|
6
|
+
@resource_class = resource_class
|
7
|
+
@params = params
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
render Oversee::Dashboard::Header.new(title: @resource_class.to_s, subtitle: "Creating new record")
|
12
|
+
|
13
|
+
div(class: "p-8") do
|
14
|
+
render Oversee::Resources::Form.new(resource: @resource)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Oversee::Resources::Show < Oversee::Base
|
4
|
+
include Phlex::Rails::Helpers::ButtonTo
|
5
|
+
include Phlex::Rails::Helpers::TurboFrameTag
|
6
|
+
|
7
|
+
def initialize(resource:, resource_class:, resource_associations:, params:)
|
8
|
+
@resource = resource
|
9
|
+
@resource_class = resource_class
|
10
|
+
@resource_associations = resource_associations
|
11
|
+
@params = params
|
12
|
+
@oversee_resource = Oversee::Resource.new(resource_class: @resource_class, resource: @resource)
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def view_template
|
17
|
+
render Oversee::Dashboard::Header.new(title: @resource_class.to_s, subtitle: "##{@resource.id}", return_path: helpers.resources_path(resource_class_name: @params[:resource_class_name])) do
|
18
|
+
button_to(helpers.resource_path(resource_class_name: @params[:resource_class_name]), method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "size-8 inline-flex items-center justify-center rounded-full text-rose-500 bg-rose-50 hover:bg-rose-100 text-sm font-medium transition-colors") { render Phlex::Icons::Iconoir::Trash.new(class: "size-4") }
|
19
|
+
end
|
20
|
+
|
21
|
+
div(class: "p-8") do
|
22
|
+
@resource_class.columns_hash.each do |key, metadata|
|
23
|
+
div(class: "py-4") do
|
24
|
+
div(class: "space-y-2") do
|
25
|
+
render Oversee::Field::Label.new(key: key, datatype: metadata.sql_type_metadata.type)
|
26
|
+
div(id: dom_id(@resource, :"#{key}_row"), class: "flex items-center gap-2 mt-4") do
|
27
|
+
render Oversee::Field::Display.new(resource: @resource, key: key, datatype: metadata.sql_type_metadata.type)
|
28
|
+
# div(id: dom_id(@resource, :"#{key}_actions")) do
|
29
|
+
# div(class: "bg-white text-gray-400 size-9 aspect-square inline-flex items-center justify-center"){ render Phlex::Icons::Iconoir::Copy.new(class: "size-5", stroke_width: 1.75) }
|
30
|
+
# end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# BELONGS_TO Associations
|
37
|
+
if !!belongs_to_associations.length
|
38
|
+
belongs_to_associations.each do |association|
|
39
|
+
div(class: "py-6") do
|
40
|
+
div(class: "space-y-4") do
|
41
|
+
div(class:"flex items-center gap-2") do
|
42
|
+
render Oversee::Field::Label.new(key: association[:name].to_s.titleize, datatype: :data)
|
43
|
+
a(href: helpers.resources_path(resource_class_name: association[:class_name].to_s), class: "hover:text-blue-500") { render Phlex::Icons::Iconoir::ArrowUpRight.new(class: "size-3") }
|
44
|
+
end
|
45
|
+
|
46
|
+
div(class: "flex items-center gap-2 flex-wrap") do
|
47
|
+
foreign_key_value = @resource[association[:foreign_key]]
|
48
|
+
path = !!foreign_key_value ? helpers.resource_path(id: foreign_key_value, resource_class_name: association[:class_name]) : helpers.resources_path(resource_class_name: association[:class_name])
|
49
|
+
|
50
|
+
a(
|
51
|
+
href: path,
|
52
|
+
class:
|
53
|
+
"inline-flex items-center gap-2 text-xs border border-transparent bg-gray-100 hover:bg-gray-200 px-3 py-1.5"
|
54
|
+
) do
|
55
|
+
plain "#{association[:class_name]} | #{foreign_key_value}"
|
56
|
+
span { render Phlex::Icons::Iconoir::ArrowUpRight.new(class: "size-3") }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# HAS_MANY Associations
|
65
|
+
if !!has_many_associations.length
|
66
|
+
has_many_associations.each do |association|
|
67
|
+
associated_resources = @resource.send(association[:name])
|
68
|
+
associated_resource_class = association[:class_name].constantize
|
69
|
+
|
70
|
+
div(class: "py-6") do
|
71
|
+
div(class: "space-y-4") do
|
72
|
+
div(class:"flex items-center gap-2") do
|
73
|
+
render Oversee::Field::Label.new(key: association[:name].to_s.titleize, datatype: :data)
|
74
|
+
a(href: helpers.resources_path(resource_class_name: association[:class_name]), class: "hover:text-blue-500") { render Phlex::Icons::Iconoir::ArrowUpRight.new(class: "size-3") }
|
75
|
+
end
|
76
|
+
|
77
|
+
div(class: "bg-gray-50 p-2") do
|
78
|
+
if associated_resources.present?
|
79
|
+
div(class: "bg-white") do
|
80
|
+
# turbo_frame_tag(
|
81
|
+
# dom_id(associated_resource_class, :table),
|
82
|
+
# src: helpers.resources_table_path(
|
83
|
+
# resource_class_name: association[:class_name],
|
84
|
+
# filters: { eq: { association[:foreign_key] => [@resource.id] } }
|
85
|
+
# ),
|
86
|
+
# loading: :lazy,
|
87
|
+
# data: { turbo_stream: true }
|
88
|
+
# ) do
|
89
|
+
# div(class: "h-20 flex items-center justify-center") { render Phlex::Icons::Iconoir::DatabaseSearch.new(class: "animate-pulse size-6 text-gray-600") }
|
90
|
+
# end
|
91
|
+
|
92
|
+
#
|
93
|
+
render Oversee::Resources::Table.new(
|
94
|
+
resources: associated_resources,
|
95
|
+
resource_class: associated_resource_class,
|
96
|
+
params: @params
|
97
|
+
)
|
98
|
+
end
|
99
|
+
else
|
100
|
+
p(class: "bg-gray-50 p-2 pr-4 flex gap-2 items-center text-xs") {
|
101
|
+
render Phlex::Icons::Iconoir::DatabaseSearch.new(class: "size-3")
|
102
|
+
plain "No #{association[:name].to_s.titleize.downcase} found"
|
103
|
+
}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def belongs_to_associations
|
116
|
+
@oversee_resource.associations[:belongs_to]
|
117
|
+
end
|
118
|
+
|
119
|
+
def has_many_associations
|
120
|
+
@oversee_resource.associations[:has_many]
|
121
|
+
end
|
122
|
+
|
123
|
+
def has_associations?
|
124
|
+
@resource_associations.present?
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Oversee::Resources::Table < Oversee::Base
|
4
|
+
include Phlex::Rails::Helpers::TurboFrameTag
|
5
|
+
|
6
|
+
def initialize(resources:, resource_class:, params:, **options)
|
7
|
+
@resources = resources
|
8
|
+
@resource_class = resource_class || resources.first.class
|
9
|
+
@params = params
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def view_template
|
14
|
+
turbo_frame_tag dom_id(@resource_class, :table), target: :_top do
|
15
|
+
render Oversee::Table.new do |table|
|
16
|
+
table.head do |head|
|
17
|
+
head.row do
|
18
|
+
th(scope: "col", class: "px-4 py-3 text-left text-xs text-gray-900 uppercase")
|
19
|
+
# Attributes
|
20
|
+
@resource_class.columns_hash.each do |key, metadata|
|
21
|
+
th(scope: "col", class: "text-left text-xs text-gray-900 uppercase whitespace-nowrap hover:bg-gray-50 transition relative") do
|
22
|
+
a(
|
23
|
+
href:
|
24
|
+
(
|
25
|
+
helpers.resources_path(
|
26
|
+
resource: @params[:resource],
|
27
|
+
sort_attribute: key,
|
28
|
+
sort_direction:
|
29
|
+
@params[:sort_direction] == "asc" ? :desc : :asc
|
30
|
+
)
|
31
|
+
),
|
32
|
+
class: "px-4 py-3 flex items-center justify-between gap-2 w-full h-full hover:text-gray-900 transition-colors"
|
33
|
+
) do
|
34
|
+
render Oversee::Field::Label.new(key: key, datatype: metadata.sql_type_metadata.type)
|
35
|
+
render Phlex::Icons::Iconoir::ArrowSeparateVertical.new(class: "size-3 text-gray-500", stroke_width: 2.5)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Associations
|
41
|
+
resource_associations.each do |association|
|
42
|
+
th(scope: "col", class: "text-left text-xs text-gray-900 uppercase whitespace-nowrap hover:bg-gray-50 transition relative") do
|
43
|
+
span(
|
44
|
+
class: "px-4 py-3 flex items-center justify-between gap-2 w-full h-full hover:text-gray-900 transition-colors"
|
45
|
+
) do
|
46
|
+
render Oversee::Field::Label.new(key: association.name, datatype: nil)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
table.body do |body|
|
54
|
+
@resources.each do |resource|
|
55
|
+
body.row do |row|
|
56
|
+
td do
|
57
|
+
div(class: "flex space-x-2 mx-4") do
|
58
|
+
a(
|
59
|
+
href:
|
60
|
+
(
|
61
|
+
helpers.resource_path(
|
62
|
+
resource.id,
|
63
|
+
resource_class_name: @params[:resource_class_name]
|
64
|
+
)
|
65
|
+
),
|
66
|
+
data: { turbo_stream: true },
|
67
|
+
class: "size-6 bg-gray-100 inline-flex items-center justify-center hover:bg-gray-200 group"
|
68
|
+
) { render Phlex::Icons::Iconoir::Eye.new(class: "size-4 text-gray-500") }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
@resource_class.columns_hash.each do |key, metadata|
|
73
|
+
row.data do
|
74
|
+
div(class: "max-w-96") do
|
75
|
+
render Oversee::Field::Value.new(datatype: metadata.sql_type_metadata.type, value: resource.send(key), key: key)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
resource_associations.each do |association|
|
81
|
+
foreign_id = resource.send(association.foreign_key)
|
82
|
+
resource_class_name = association.class_name
|
83
|
+
|
84
|
+
path = !!foreign_id ? helpers.resource_path(id: foreign_id, resource_class_name:) : helpers.resources_path(resource_class_name: resource_class_name)
|
85
|
+
|
86
|
+
row.data do
|
87
|
+
div(class: "max-w-96") do
|
88
|
+
a(
|
89
|
+
href: path,
|
90
|
+
class:"px-2 py-1 bg-gray-100 text-gray-500 text-sm flex items-center justify-between gap-2 hover:bg-gray-200 hover:text-gray-900 min-w-20") do
|
91
|
+
span { foreign_id.presence || "N/A" }
|
92
|
+
render Phlex::Icons::Iconoir::ArrowRight.new(class: "size-3")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def resource_associations
|
107
|
+
@resource_class.reflect_on_all_associations.select { |association| association.macro == :belongs_to }
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Oversee::Table < Phlex::HTML
|
2
|
+
def view_template(&)
|
3
|
+
div(class: "overflow-x-hidden") do
|
4
|
+
div(class: "-mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8") do
|
5
|
+
div(class: "inline-block min-w-full align-middle sm:px-6 lg:px-8") do
|
6
|
+
table(class: "min-w-full divide-y divide-gray-200") do
|
7
|
+
yield
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def head(&)
|
15
|
+
render Oversee::Table::Head.new(&)
|
16
|
+
end
|
17
|
+
|
18
|
+
def body(&)
|
19
|
+
render Oversee::Table::Body.new(&)
|
20
|
+
end
|
21
|
+
end
|
@@ -2,34 +2,67 @@ module Oversee
|
|
2
2
|
class ResourcesController < BaseController
|
3
3
|
include ActionView::RecordIdentifier
|
4
4
|
|
5
|
-
before_action :set_resource_class
|
6
|
-
before_action :set_resource, only: %i[show edit destroy]
|
5
|
+
before_action :set_resource_class
|
6
|
+
before_action :set_resource, only: %i[show edit update destroy input_field]
|
7
|
+
before_action :set_resources, only: %i[index table]
|
7
8
|
|
8
9
|
def index
|
9
|
-
|
10
|
+
@pagy, @resources = pagy(@resources, limit: params[:per_page] || Oversee.configuration.per_page)
|
11
|
+
|
12
|
+
render Oversee::Resources::Index.new(
|
13
|
+
resources: @resources,
|
14
|
+
resource_class: @resource_class,
|
15
|
+
pagy: @pagy,
|
16
|
+
params: params
|
17
|
+
)
|
18
|
+
end
|
10
19
|
|
11
|
-
|
12
|
-
@
|
20
|
+
def new
|
21
|
+
@resource = @resource_class.new
|
22
|
+
render Oversee::Resources::New.new(
|
23
|
+
resource: @resource,
|
24
|
+
resource_class: @resource_class,
|
25
|
+
params: params
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def create
|
30
|
+
@resource = @resource_class.new(resource_params)
|
31
|
+
|
32
|
+
respond_to do |format|
|
33
|
+
if @resource.update(resource_params)
|
34
|
+
format.html { redirect_to resource_path(@resource.id, resource_class_name: @resource_class) }
|
35
|
+
format.turbo_stream { redirect_to resource_path(@resource.id, resource_class_name: @resource_class), status: :see_other }
|
36
|
+
else
|
37
|
+
format.html { render :new }
|
38
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace(dom_id(@resource), Oversee::Resources::Form.new(resource: @resource)) }
|
39
|
+
end
|
40
|
+
end
|
13
41
|
end
|
14
42
|
|
15
43
|
def show
|
16
|
-
|
44
|
+
render Oversee::Resources::Show.new(
|
45
|
+
resource: @resource,
|
46
|
+
resource_class: @resource_class,
|
47
|
+
resource_associations: resource_associations,
|
48
|
+
params: params
|
49
|
+
)
|
17
50
|
end
|
18
51
|
|
19
52
|
def edit
|
20
53
|
end
|
21
54
|
|
22
55
|
def update
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
@key = params[:resource][:oversee_key]
|
27
|
-
@datatype = params[:resource][:oversee_datatype]
|
56
|
+
key = params[:oversee_key]
|
57
|
+
datatype = params[:oversee_datatype]
|
28
58
|
|
29
59
|
respond_to do |format|
|
30
60
|
if @resource.update(resource_params)
|
31
61
|
format.html { redirect_to resource_path(@resource.id, resource: @resource_class) }
|
32
|
-
format.turbo_stream
|
62
|
+
format.turbo_stream do
|
63
|
+
component = Oversee::Field::Display.new(resource: @resource, datatype:, key:, value: @resource.send(key))
|
64
|
+
render turbo_stream: turbo_stream.replace(dom_id(@resource, key), component)
|
65
|
+
end
|
33
66
|
else
|
34
67
|
end
|
35
68
|
end
|
@@ -44,27 +77,52 @@ module Oversee
|
|
44
77
|
def input_field
|
45
78
|
set_resource
|
46
79
|
|
47
|
-
|
48
|
-
|
49
|
-
|
80
|
+
key = params[:key].to_sym
|
81
|
+
value = @resource.send(key)
|
82
|
+
datatype = @resource.class.columns_hash[key.to_s].type
|
83
|
+
|
84
|
+
field_dom_id = dom_id(@resource, key)
|
85
|
+
field = Oversee::Field::Form.new(resource: @resource, datatype:, key:, value:)
|
50
86
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
87
|
+
respond_to do |format|
|
88
|
+
format.turbo_stream do
|
89
|
+
render turbo_stream: turbo_stream.replace(field_dom_id, field)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def table
|
95
|
+
component_id = dom_id(@resource_class, :table)
|
96
|
+
component = Oversee::Resources::Table.new(resource_class: @resource_class, resources: @resources, params: params)
|
97
|
+
|
98
|
+
respond_to do |format|
|
99
|
+
format.turbo_stream do
|
100
|
+
render turbo_stream: turbo_stream.replace(component_id, component)
|
101
|
+
end
|
102
|
+
format.html do
|
103
|
+
render component, layout: false
|
104
|
+
end
|
105
|
+
end
|
56
106
|
end
|
57
107
|
|
58
108
|
private
|
59
109
|
|
60
110
|
def set_resource_class
|
61
|
-
@resource_class = params[:
|
111
|
+
@resource_class = params[:resource_class_name].constantize
|
62
112
|
end
|
63
113
|
|
64
114
|
def set_resource
|
65
115
|
@resource = @resource_class.find(params[:id])
|
66
116
|
end
|
67
117
|
|
118
|
+
def set_resources
|
119
|
+
set_sorting_rules
|
120
|
+
|
121
|
+
@resources = @resource_class.order(@sort_attribute.to_sym => sort_direction)
|
122
|
+
@resources = Filter.new(collection: @resources, params:).apply
|
123
|
+
@resources = Search.new(collection: @resources, resource_class: @resource_class, query: params[:query]).call
|
124
|
+
end
|
125
|
+
|
68
126
|
def resource_associations
|
69
127
|
@resource_associations ||= @resource_class.reflect_on_all_associations
|
70
128
|
end
|