oversee 0.1.1 → 0.3.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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +53 -14
  3. data/app/assets/config/oversee_manifest.js +2 -1
  4. data/app/components/oversee/{application_component.rb → base.rb} +1 -1
  5. data/app/components/oversee/card.rb +13 -0
  6. data/app/components/oversee/dashboard/filters.rb +58 -0
  7. data/app/components/oversee/dashboard/header.rb +57 -0
  8. data/app/components/oversee/dashboard/index.rb +36 -0
  9. data/app/components/oversee/dashboard/javascript.rb +9 -0
  10. data/app/components/oversee/dashboard/pagination.rb +23 -0
  11. data/app/components/oversee/dashboard/sidebar.rb +90 -0
  12. data/app/components/oversee/dashboard/tailwind.rb +62 -0
  13. data/app/components/oversee/field/display.rb +38 -0
  14. data/app/components/oversee/field/form.rb +30 -0
  15. data/app/components/oversee/field/input/belongs_to.rb +22 -0
  16. data/app/components/oversee/field/input/boolean.rb +13 -0
  17. data/app/components/oversee/field/input/datetime.rb +10 -0
  18. data/app/components/oversee/field/input/integer.rb +10 -0
  19. data/app/components/oversee/field/input/rich_text.rb +18 -0
  20. data/app/components/oversee/field/input/string.rb +10 -0
  21. data/app/components/oversee/field/input.rb +34 -0
  22. data/app/components/oversee/field/label.rb +25 -0
  23. data/app/components/oversee/field/value/belongs_to.rb +21 -0
  24. data/app/components/oversee/field/value/boolean.rb +50 -0
  25. data/app/components/oversee/field/value/datetime.rb +9 -0
  26. data/app/components/oversee/field/value/enum.rb +9 -0
  27. data/app/components/oversee/field/value/integer.rb +9 -0
  28. data/app/components/oversee/field/value/rich_text.rb +9 -0
  29. data/app/components/oversee/field/value/string.rb +20 -0
  30. data/app/components/oversee/field/value/text.rb +9 -0
  31. data/app/components/oversee/field/value.rb +40 -0
  32. data/app/components/oversee/layout/application.rb +46 -0
  33. data/app/components/oversee/resources/base.rb +4 -0
  34. data/app/components/oversee/resources/errors.rb +41 -0
  35. data/app/components/oversee/resources/form.rb +49 -0
  36. data/app/components/oversee/resources/index.rb +45 -0
  37. data/app/components/oversee/resources/new.rb +21 -0
  38. data/app/components/oversee/resources/show.rb +207 -0
  39. data/app/components/oversee/resources/table.rb +115 -0
  40. data/app/components/oversee/table/body.rb +13 -0
  41. data/app/components/oversee/table/data.rb +5 -0
  42. data/app/components/oversee/table/head.rb +9 -0
  43. data/app/components/oversee/table/row.rb +9 -0
  44. data/app/components/oversee/table.rb +21 -0
  45. data/app/controllers/oversee/dashboard_controller.rb +4 -0
  46. data/app/controllers/oversee/resources/fields_controller.rb +20 -36
  47. data/app/controllers/oversee/resources_controller.rb +65 -26
  48. data/app/javascript/oversee/application.js +3 -0
  49. data/app/javascript/oversee/controllers/index.js +8 -0
  50. data/app/javascript/oversee/controllers/reveal_controller.js +7 -0
  51. data/app/javascript/oversee/controllers/sidebar/state_controller.js +21 -0
  52. data/app/models/oversee/resource.rb +62 -0
  53. data/app/service/oversee/filter.rb +39 -0
  54. data/app/service/oversee/search.rb +34 -0
  55. data/app/views/oversee/base.rb +5 -0
  56. data/config/importmap.rb +14 -0
  57. data/config/locales/en.yml +4 -0
  58. data/config/locales/oversee.en.yml +4 -0
  59. data/config/routes.rb +17 -11
  60. data/lib/generators/oversee/install_generator.rb +7 -0
  61. data/lib/oversee/configuration.rb +3 -0
  62. data/lib/oversee/engine.rb +19 -0
  63. data/lib/oversee/version.rb +1 -1
  64. data/lib/oversee.rb +26 -2
  65. data/lib/tasks/oversee_tasks.rake +6 -4
  66. metadata +95 -48
  67. data/app/assets/stylesheets/oversee/application.css +0 -15
  68. data/app/components/oversee/card_component.rb +0 -15
  69. data/app/components/oversee/dashboard/sidebar_component.rb +0 -127
  70. data/app/components/oversee/field_component.rb +0 -39
  71. data/app/components/oversee/field_label_component.rb +0 -155
  72. data/app/components/oversee/fields/display_row_component.rb +0 -58
  73. data/app/components/oversee/fields/input/boolean_component.rb +0 -26
  74. data/app/components/oversee/fields/input/datetime_component.rb +0 -27
  75. data/app/components/oversee/fields/input/integer_component.rb +0 -26
  76. data/app/components/oversee/fields/input/string_component.rb +0 -26
  77. data/app/components/oversee/fields/value/boolean_component.rb +0 -44
  78. data/app/components/oversee/fields/value/datetime_component.rb +0 -16
  79. data/app/components/oversee/fields/value/enum_component.rb +0 -16
  80. data/app/components/oversee/fields/value/integer_component.rb +0 -16
  81. data/app/components/oversee/fields/value/string_component.rb +0 -23
  82. data/app/components/oversee/fields/value/text_component.rb +0 -16
  83. data/app/helpers/oversee/application_helper.rb +0 -5
  84. data/app/jobs/oversee/application_job.rb +0 -4
  85. data/app/mailers/oversee/application_mailer.rb +0 -6
  86. data/app/oversee/cards.rb +0 -2
  87. data/app/oversee/resource.rb +0 -10
  88. data/app/views/layouts/oversee/application.html.erb +0 -26
  89. data/app/views/oversee/application/_javascript.html.erb +0 -17
  90. data/app/views/oversee/dashboard/show.html.erb +0 -18
  91. data/app/views/oversee/resources/_form.html.erb +0 -10
  92. data/app/views/oversee/resources/_input_field.html.erb +0 -20
  93. data/app/views/oversee/resources/edit.html.erb +0 -30
  94. data/app/views/oversee/resources/index.html.erb +0 -59
  95. data/app/views/oversee/resources/input_field.html.erb +0 -1
  96. data/app/views/oversee/resources/new.html.erb +0 -28
  97. data/app/views/oversee/resources/show.html.erb +0 -51
  98. data/app/views/oversee/resources/update.turbo_stream.erb +0 -3
  99. data/app/views/shared/_sidebar.html.erb +0 -32
@@ -0,0 +1,25 @@
1
+ class Oversee::Field::Label < Oversee::Base
2
+ ICON_MAP = {
3
+ string: Phlex::Icons::Iconoir::Text,
4
+ text: Phlex::Icons::Iconoir::TextSquare,
5
+ rich_text: Phlex::Icons::Iconoir::TextSquare,
6
+ integer: Phlex::Icons::Iconoir::Number0Square,
7
+ datetime: Phlex::Icons::Iconoir::Calendar,
8
+ boolean: Phlex::Icons::Iconoir::SwitchOn,
9
+ data: Phlex::Icons::Iconoir::Page,
10
+ }
11
+
12
+ def initialize(datatype: :string, key: nil)
13
+ @datatype = datatype
14
+ @key = key
15
+ end
16
+
17
+ def view_template
18
+ div(id: "#{@key}_label", class:"inline-flex items-center space-x-2") do
19
+ div(class: "size-5 bg-gray-100 inline-flex items-center justify-center") do
20
+ render ICON_MAP[@datatype] ? ICON_MAP[@datatype].new(class: "size-3") : ICON_MAP[:data].new(class: "size-3")
21
+ end
22
+ label(class: "uppercase text-xs text-gray-700 font-medium block cursor-auto") { @key.to_s.humanize(keep_id_suffix: true) }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ class Oversee::Field::Value::BelongsTo < Phlex::HTML
2
+ def initialize(key: nil, value: nil, **options)
3
+ @key = key
4
+ @value = value
5
+ @options = options
6
+ end
7
+
8
+ def view_template
9
+ p(title: @value, class:"inline-flex items-center gap-1") do
10
+ if display_key?
11
+ span { @key.humanize }
12
+ span {"|"}
13
+ end
14
+ span { @value }
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def display_key? = !!@options[:display_key]
21
+ end
@@ -0,0 +1,50 @@
1
+ class Oversee::Field::Value::Boolean < Phlex::HTML
2
+ def initialize(key: nil, value: nil, kind: :value)
3
+ @value = value
4
+ end
5
+
6
+ def view_template
7
+ @value ? check_icon : x_icon
8
+ end
9
+
10
+ private
11
+
12
+ def check_icon
13
+ svg(
14
+ stroke_width: "2",
15
+ viewbox: "0 0 24 24",
16
+ fill: "none",
17
+ xmlns: "http://www.w3.org/2000/svg",
18
+ color: "currentColor",
19
+ class: "size-4 text-emerald-500"
20
+ ) do |s|
21
+ s.path(
22
+ d: "M5 13L9 17L19 7",
23
+ stroke: "currentColor",
24
+ stroke_width: "2",
25
+ stroke_linecap: "round",
26
+ stroke_linejoin: "round"
27
+ )
28
+ end
29
+ end
30
+
31
+ def x_icon
32
+ svg(
33
+ stroke_width: "1.5",
34
+ viewbox: "0 0 24 24",
35
+ fill: "none",
36
+ xmlns: "http://www.w3.org/2000/svg",
37
+ color: "currentColor",
38
+ class: "size-4 text-rose-500"
39
+ ) do |s|
40
+ s.path(
41
+ d:
42
+ "M6.75827 17.2426L12.0009 12M17.2435 6.75736L12.0009 12M12.0009 12L6.75827 6.75736M12.0009 12L17.2435 17.2426",
43
+ stroke: "currentColor",
44
+ stroke_width: "1.5",
45
+ stroke_linecap: "round",
46
+ stroke_linejoin: "round"
47
+ )
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,9 @@
1
+ class Oversee::Field::Value::Datetime < Phlex::HTML
2
+ def initialize(key: nil, value: nil, kind: :value)
3
+ @value = value
4
+ end
5
+
6
+ def view_template
7
+ time(title: @value.to_s) { @value&.to_fs(:long) || "N/A" }
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class Oversee::Field::Value::Enum < Phlex::HTML
2
+ def initialize(key: nil, value: nil, kind: :value)
3
+ @value = value
4
+ end
5
+
6
+ def view_template
7
+ p(class:"inline-flex px-2 py-1 text-red-500") { @value.to_s }
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class Oversee::Field::Value::Integer < Phlex::HTML
2
+ def initialize(key: nil, value: nil, kind: :value)
3
+ @value = value
4
+ end
5
+
6
+ def view_template
7
+ p { @value }
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class Oversee::Field::Value::RichText < Phlex::HTML
2
+ def initialize(datatype: :string, key: nil, value: nil, kind: :value)
3
+ @value = value
4
+ end
5
+
6
+ def view_template
7
+ p(class: "truncate") { @value }
8
+ end
9
+ end
@@ -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,9 @@
1
+ class Oversee::Field::Value::Text < Phlex::HTML
2
+ def initialize(datatype: :string, key: nil, value: nil, kind: :value)
3
+ @value = value
4
+ end
5
+
6
+ def view_template
7
+ p(class: "truncate") { @value }
8
+ end
9
+ end
@@ -0,0 +1,40 @@
1
+ class Oversee::Field::Value < Oversee::Base
2
+ MAP = {
3
+ string: Oversee::Field::Value::String,
4
+ belongs_to: Oversee::Field::Value::BelongsTo,
5
+ boolean: Oversee::Field::Value::Boolean,
6
+ datetime: Oversee::Field::Value::Datetime,
7
+ enum: Oversee::Field::Value::Enum,
8
+ integer: Oversee::Field::Value::Integer,
9
+ rich_text: Oversee::Field::Value::RichText,
10
+ text: Oversee::Field::Value::Text,
11
+ }
12
+
13
+ attr_reader :key
14
+ attr_reader :value
15
+ attr_reader :datatype
16
+
17
+ def initialize(key: nil, value: nil, datatype: :string, **options)
18
+ @key = key
19
+ @value = value
20
+ @datatype = datatype
21
+ @options = options
22
+
23
+ @class = @options[:class]
24
+ end
25
+
26
+ def view_template
27
+ return p(class: "text-gray-400 text-xs"){ "—" } if value.nil?
28
+ render component_class.new(key:, value:, **@options)
29
+ end
30
+
31
+ private
32
+
33
+ def component_class
34
+ MAP[datatype.to_sym] || Oversee::Field::Value::String
35
+ end
36
+
37
+ def for_table?
38
+ @options[:for_table] || false
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Oversee::Layout::Application < Oversee::Base
4
+ extend Phlex::Kit
5
+
6
+ include Phlex::Rails::Helpers::CSPMetaTag
7
+ include Phlex::Rails::Helpers::CSRFMetaTags
8
+ include Phlex::Rails::Helpers::StylesheetLinkTag
9
+ include Phlex::Rails::Helpers::JavascriptImportmapTags
10
+
11
+ def view_template
12
+ doctype
13
+ html do
14
+ # render Head
15
+ head do
16
+ csrf_meta_tags
17
+ csp_meta_tag
18
+
19
+ title { "Oversee | #{Oversee.application_name}" }
20
+
21
+ meta(name: "viewport", content: "width=device-width, initial-scale=1.0")
22
+ meta(name: "ROBOTS", content: "NOODP")
23
+
24
+ link(rel: "stylesheet", type: "text/css", href: "https://unpkg.com/trix@2.1.8/dist/trix.css")
25
+
26
+ render Oversee::Dashboard::Javascript.new
27
+ render Oversee::Dashboard::Tailwind.new
28
+ end
29
+
30
+ body(class: "min-h-screen bg-gray-100 p-4") do
31
+ # render Flash.new
32
+
33
+ div(class: "flex gap-4 w-full") do
34
+ div(class: "w-72 shrink-0") { render Oversee::Dashboard::Sidebar.new }
35
+ div(class: "w-full overflow-scroll") do
36
+ div(class: "bg-white rounded-lg overflow-clip p-4") do
37
+ yield
38
+ end
39
+ end
40
+ end
41
+
42
+
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Oversee::Resources::Base < Oversee::Base
4
+ 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,45 @@
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 around_template
13
+ render Oversee::Layout::Application.new { super }
14
+ end
15
+
16
+ def view_template
17
+ render Oversee::Dashboard::Header.new(title: @resource_class.to_s.pluralize) do |h|
18
+ h.left
19
+ h.right do
20
+ a(
21
+ href: helpers.new_resource_path(@params[:resource_class_name]),
22
+ target: "_top",
23
+ class: "inline-flex items-center justify-center gap-1.5 h-8 px-4 rounded-full bg-transparent text-gray-900 hover:bg-gray-100 text-sm font-medium transition group"
24
+ ) do
25
+ render Phlex::Icons::Iconoir::Plus.new(class: "size-4 text-gray-500 group-hover:text-blue-500", stroke_width: 2.5)
26
+ plain "Add new"
27
+ end
28
+ end
29
+ end
30
+ hr(class: "my-4")
31
+
32
+ render Oversee::Dashboard::Filters.new(params: @params)
33
+
34
+ hr(class: "mt-4")
35
+ render Oversee::Resources::Table.new(resource_class: @resource_class, resources: @resources, params: @params)
36
+ hr()
37
+ render Oversee::Dashboard::Pagination.new(pagy: @pagy, params: @params)
38
+ end
39
+
40
+ private
41
+
42
+ def resource_associations
43
+ @resource_class.reflect_on_all_associations.select { |association| association.macro == :belongs_to }
44
+ end
45
+ end
@@ -0,0 +1,21 @@
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 around_template
11
+ render Oversee::Layout::Application.new { super }
12
+ end
13
+
14
+ def view_template
15
+ render Oversee::Dashboard::Header.new(title: @resource_class.to_s, subtitle: "Creating new record")
16
+
17
+ hr(class: "my-4")
18
+
19
+ render Oversee::Resources::Form.new(resource: @resource)
20
+ end
21
+ end
@@ -0,0 +1,207 @@
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
+
8
+ attr_reader :resource
9
+ attr_reader :resource_class
10
+ attr_reader :resource_associations
11
+
12
+ def initialize(resource:, resource_class:, resource_associations:, params:)
13
+ @resource = resource
14
+ @resource_class = resource_class
15
+ @resource_associations = resource_associations
16
+ @params = params
17
+
18
+ @oversee_resource = Oversee::Resource.new(resource_class: @resource_class, instance: @resource)
19
+ end
20
+
21
+ def around_template
22
+ render Oversee::Layout::Application.new { super }
23
+ end
24
+
25
+ def view_template
26
+ render Oversee::Dashboard::Header.new(title: @resource_class.to_s.pluralize) do |h|
27
+ h.left do
28
+ h.separator
29
+ div(class: "inline-flex items-center gap-1 text-sm bg-gray-100 text-gray-600 h-6 px-2") do
30
+ render Phlex::Icons::Iconoir::Hashtag.new(class: "size-2.5 text-gray-500", stroke_width: 2)
31
+ span { @resource.to_param }
32
+ end
33
+ end
34
+ h.right do
35
+ details(class: "relative inline-block") do
36
+ summary(class: "cursor-pointer list-none") do
37
+ div(class: "inline-flex items-center justify-center size-8 hover:bg-indigo-50 transition") do
38
+ render Phlex::Icons::Iconoir::MoreHorizCircle.new(class: "size-4 text-gray-500")
39
+ end
40
+ end
41
+ ul(
42
+ class:
43
+ "absolute p-2 mt-2 right-0 top-full min-w-40 overflow-hidden rounded-lg bg-white border border-b-2 border-gray-200/75 divide-y text-xs text-gray-500 font-medium"
44
+ ) do
45
+ li(class: "w-full") do
46
+ button_to(helpers.resource_path(resource_class_name: @params[:resource_class_name]), method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "p-1 hover:bg-gray-50 w-full flex items-center gap-2 transition duration-100") do
47
+ div(class: "inline-flex items-center justify-center size-6 bg-gray-100") do
48
+ render Phlex::Icons::Iconoir::Trash.new(class: "size-3 text-gray-500")
49
+ end
50
+ plain "Delete"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ hr(class: "my-4")
59
+
60
+
61
+
62
+
63
+ # COLUMNS
64
+ @resource_class.columns_hash.each do |key, metadata|
65
+ next if @oversee_resource.foreign_keys.include?(key.to_s)
66
+
67
+ div(class: "py-4") do
68
+ div(class: "space-y-2") do
69
+ render Oversee::Field::Label.new(key: key, datatype: metadata.sql_type_metadata.type)
70
+ div(id: dom_id(@resource, :"#{key}_row"), class: "flex items-center gap-2 mt-4") do
71
+ render Oversee::Field::Display.new(resource:, key:, value: @resource.send(key), datatype: metadata.sql_type_metadata.type)
72
+ # div(id: dom_id(@resource, :"#{key}_actions")) do
73
+ # button(class: "bg-gray-100 hover:bg-gray-200 text-gray-400 hover:text-blue-500 size-10 aspect-square inline-flex items-center justify-center transition-colors") { render Phlex::Icons::Iconoir::Copy.new(class: "size-4") }
74
+ # end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ hr(class: "my-4")
81
+
82
+ # RICH TEXT Associations
83
+ if !!rich_text_associations.length
84
+ rich_text_associations.each do |association|
85
+
86
+ # Remove the "rich_text_" prefix from the association name
87
+ key = association[:name].to_s[10..].to_sym
88
+
89
+ div do
90
+ div(class: "space-y-4") do
91
+ div(class:"flex items-center gap-2") do
92
+ render Oversee::Field::Label.new(key: key.to_s.titleize, datatype: :rich_text)
93
+ 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") }
94
+ end
95
+
96
+ div(id: dom_id(@resource, :"#{key.to_s}_row"), class: "flex items-center gap-2 mt-4") do
97
+ render Oversee::Field::Display.new(resource:, key:, value: @resource.send(key).to_plain_text[..196], datatype: :rich_text)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ # BELONGS_TO Associations
105
+ if !!belongs_to_associations.length
106
+ belongs_to_associations.each do |association|
107
+ div(class: "py-6") do
108
+ div(class: "space-y-4") do
109
+ div(class:"flex items-center gap-2") do
110
+ render Oversee::Field::Label.new(key: association[:name].to_s.titleize, datatype: :data)
111
+ 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") }
112
+ end
113
+
114
+ foreign_key = association[:foreign_key]
115
+ foreign_key_value = @resource[association[:foreign_key]]
116
+ 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])
117
+
118
+ div(id: dom_id(@resource, :"#{foreign_key}_row"), class: "flex items-center gap-2 mt-4") do
119
+ render Oversee::Field::Display.new(resource:, key: foreign_key, value: foreign_key_value, datatype: :belongs_to, display_key: true)
120
+ div(id: dom_id(@resource, :"#{foreign_key}_actions")) do
121
+ a(href: path, class: "bg-gray-100 hover:bg-gray-200 text-gray-400 hover:text-blue-500 size-10 aspect-square inline-flex items-center justify-center transition-colors"){ render Phlex::Icons::Iconoir::ArrowUpRight.new(class: "size-4") }
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ hr(class: "my-4")
130
+
131
+ # HAS_MANY Associations
132
+ if !!has_many_associations.length
133
+ div(class: "flex flex-col gap-8") do
134
+ has_many_associations.each do |association|
135
+ associated_resources = @resource.send(association[:name])
136
+ associated_resource_class = association[:class_name].constantize
137
+
138
+ div(class: "space-y-4") do
139
+ div(class:"flex items-center gap-2") do
140
+ render Oversee::Field::Label.new(key: association[:name].to_s.titleize, datatype: :data)
141
+ 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") }
142
+ end
143
+
144
+ div(class: "bg-gray-50 p-2") do
145
+ # turbo_frame_tag(
146
+ # dom_id(associated_resource_class, :table),
147
+ # src: helpers.resources_table_path(resources_table_params(association)),
148
+ # loading: :lazy,
149
+ # data: { turbo_stream: true }
150
+ # ) do
151
+ # div(class: "h-20 flex items-center justify-center") { render Phlex::Icons::Iconoir::DatabaseSearch.new(class: "animate-pulse size-6 text-gray-600") }
152
+ # end
153
+
154
+ if associated_resources.present?
155
+ div(class: "bg-white") do
156
+ render Oversee::Resources::Table.new(
157
+ resources: associated_resources,
158
+ resource_class: associated_resource_class,
159
+ params: @params
160
+ )
161
+ end
162
+ else
163
+ p(class: "bg-gray-50 p-2 pr-4 flex gap-2 items-center text-xs") {
164
+ render Phlex::Icons::Iconoir::DatabaseSearch.new(class: "size-3")
165
+ plain "No #{association[:name].to_s.titleize.downcase} found"
166
+ }
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ private
176
+
177
+ def rich_text_associations
178
+ @oversee_resource.rich_text_associations
179
+ end
180
+
181
+ def belongs_to_associations
182
+ @oversee_resource.associations[:belongs_to]
183
+ end
184
+
185
+ def has_many_associations
186
+ @oversee_resource.associations[:has_many]
187
+ end
188
+
189
+ def has_associations?
190
+ @resource_associations.present?
191
+ end
192
+
193
+ def resources_table_params(association)
194
+ # if association[:through].nil?
195
+ # return { association[:foreign_key] => { eq: [@resource.id] } }
196
+ # else
197
+ # keys = @resource.send(association[:through]).pluck(association[:foreign_key])
198
+ # return { @resource_class.primary_key => { eq: keys } }
199
+ # end
200
+
201
+ {
202
+ resource_class_name: association[:class_name],
203
+ association_name: association[:through],
204
+ filters: { association[:foreign_key] => { eq: [@resource.id] } }
205
+ }
206
+ end
207
+ end