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,115 @@
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
+
12
+ @oversee_resource = Oversee::Resource.new(resource_class: @resource_class)
13
+ end
14
+
15
+ def view_template
16
+ turbo_frame_tag dom_id(@resource_class, :table), target: "_top" do
17
+ render Oversee::Table.new do |table|
18
+ table.head do |head|
19
+ head.row do
20
+ th(scope: "col", class: "px-4 py-3 text-left text-xs text-gray-900 uppercase")
21
+ # Attributes
22
+ @resource_class.columns_hash.each do |key, metadata|
23
+ next if @oversee_resource.foreign_keys.include?(key.to_s)
24
+ th(scope: "col", class: "text-left text-xs text-gray-900 uppercase whitespace-nowrap hover:bg-gray-50 transition relative") do
25
+ a(
26
+ href:
27
+ (
28
+ helpers.resources_path(
29
+ resource: @params[:resource],
30
+ sort_attribute: key,
31
+ sort_direction:
32
+ @params[:sort_direction] == "asc" ? :desc : :asc
33
+ )
34
+ ),
35
+ target: "_top",
36
+ class: "px-4 py-3 flex items-center justify-between gap-2 w-full h-full hover:text-gray-900 transition-colors"
37
+ ) do
38
+ render Oversee::Field::Label.new(key: key, datatype: metadata.sql_type_metadata.type)
39
+ render Phlex::Icons::Iconoir::ArrowSeparateVertical.new(class: "size-3 text-gray-500", stroke_width: 2.5)
40
+ end
41
+ end
42
+ end
43
+
44
+ # Associations
45
+ resource_associations.each do |association|
46
+ th(scope: "col", class: "text-left text-xs text-gray-900 uppercase whitespace-nowrap hover:bg-gray-50 transition relative") do
47
+ span(
48
+ class: "px-4 py-3 flex items-center justify-between gap-2 w-full h-full hover:text-gray-900 transition-colors"
49
+ ) do
50
+ render Oversee::Field::Label.new(key: association.name, datatype: nil)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ table.body do |body|
58
+ @resources.each do |resource|
59
+
60
+ body.row do |row|
61
+ td do
62
+ div(class: "flex space-x-2 mx-4") do
63
+ a(
64
+ href:
65
+ (
66
+ helpers.resource_path(
67
+ resource.id,
68
+ resource_class_name: @params[:resource_class_name]
69
+ )
70
+ ),
71
+ data: { turbo_stream: true },
72
+ class: "size-6 bg-gray-100 inline-flex items-center justify-center hover:bg-gray-200 group"
73
+ ) { render Phlex::Icons::Iconoir::Eye.new(class: "size-4 text-gray-500") }
74
+ end
75
+ end
76
+
77
+ @resource_class.columns_hash.each do |key, metadata|
78
+ next if @oversee_resource.foreign_keys.include?(key.to_s)
79
+ row.data do
80
+ div(class: "max-w-96") do
81
+ render Oversee::Field::Value.new(datatype: metadata.sql_type_metadata.type, value: resource.send(key), key: key)
82
+ end
83
+ end
84
+ end
85
+
86
+ resource_associations.each do |association|
87
+ foreign_id = resource.send(association.foreign_key)
88
+ resource_class_name = association.class_name
89
+
90
+ path = !!foreign_id ? helpers.resource_path(id: foreign_id, resource_class_name:) : helpers.resources_path(resource_class_name: resource_class_name)
91
+
92
+ row.data do
93
+ div(class: "max-w-96") do
94
+ a(
95
+ href: path,
96
+ 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
97
+ span { foreign_id.presence || "N/A" }
98
+ render Phlex::Icons::Iconoir::ArrowRight.new(class: "size-3")
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def resource_associations
113
+ @resource_class.reflect_on_all_associations.select { |association| association.macro == :belongs_to }
114
+ end
115
+ end
@@ -0,0 +1,13 @@
1
+ class Oversee::Table::Body < Phlex::HTML
2
+ def view_template(&)
3
+ tbody(class: "divide-y divide-gray-200", &)
4
+ end
5
+
6
+ def row(&)
7
+ render Oversee::Table::Row.new(&)
8
+ end
9
+
10
+ def data(&)
11
+ render Oversee::Table::Data.new(&)
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ class Oversee::Table::Data < Phlex::HTML
2
+ def view_template(&)
3
+ td(class: "whitespace-nowrap p-4 text-sm text-gray-500", &)
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class Oversee::Table::Head < Phlex::HTML
2
+ def view_template(&)
3
+ thead(class: "bg-white", &)
4
+ end
5
+
6
+ def row(&)
7
+ render Oversee::Table::Row.new(&)
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class Oversee::Table::Row < Phlex::HTML
2
+ def view_template(&)
3
+ tr(class: "divide-x divide-gray-200") { yield }
4
+ end
5
+
6
+ def data(&)
7
+ render Oversee::Table::Data.new(&)
8
+ end
9
+ 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
@@ -1,5 +1,9 @@
1
1
  module Oversee
2
2
  class DashboardController < BaseController
3
+ def index
4
+ render Oversee::Dashboard::Index.new, layout: false
5
+ end
6
+
3
7
  def show
4
8
  fetch_metrics
5
9
 
@@ -1,44 +1,28 @@
1
- module Oversee
2
- module Resources
3
- class FieldsController < BaseController
4
- before_action :set_resource_class
1
+ class Oversee::Resources::FieldsController < Oversee::ResourcesController
2
+ # Renders the display field for a resource
3
+ def show
4
+ end
5
5
 
6
- def update
7
- @resource_class = params[:resource_class].constantize
8
- @resource = @resource_class.find(params[:id])
9
- # @datatype = @resource_class.columns_hash[@key.to_s].type
6
+ # Renders the input field for a resource
7
+ def input
8
+ set_resource
10
9
 
11
- respond_to do |format|
12
- if @resource.update(resource_params)
13
- format.html { redirect_to resource_path(@resource, resource: @resource_class) }
14
- format.turbo_stream
15
- else
16
- end
17
- end
18
- end
19
-
20
- # Non-standard actions
21
- def edit
22
- @resource = @resource_class.find(params[:id])
23
- @key = params[:key].to_sym
24
- @value = @resource.send(@key)
25
- @datatype = @resource.class.columns_hash[@key.to_s].type
26
- puts "---"
27
- puts "key: #{@key}"
28
- puts "value: #{@value}"
29
- puts "datatype: #{@datatype}"
30
- puts "---"
10
+ key = params[:key].to_sym
11
+ value = params[:value] || @resource.send(key)
12
+ datatype = params[:datatype] || @resource.class.columns_hash[key.to_s].type
31
13
 
32
- end
14
+ # puts "---" * 30
15
+ # puts "key: #{key}"
16
+ # puts "value: #{value}"
17
+ # puts "datatype: #{datatype}"
18
+ # puts "---" * 30
33
19
 
34
- private
35
-
36
- def set_resource_class
37
- @resource_class = params[:resource_class].constantize
38
- end
20
+ field_dom_id = dom_id(@resource, key)
21
+ field = Oversee::Field::Form.new(resource: @resource, datatype:, key:, value:)
39
22
 
40
- def resource_params
41
- params.require(:resource).permit!
23
+ respond_to do |format|
24
+ format.turbo_stream do
25
+ render turbo_stream: turbo_stream.replace(field_dom_id, field)
42
26
  end
43
27
  end
44
28
  end
@@ -2,51 +2,67 @@ module Oversee
2
2
  class ResourcesController < BaseController
3
3
  include ActionView::RecordIdentifier
4
4
 
5
- before_action :set_resource_class, except: [:create, :update]
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]
7
+ before_action :set_resources, only: %i[index]
7
8
 
8
9
  def index
9
- set_sorting_rules
10
-
11
- @resources = @resource_class.order(@sort_attribute.to_sym => sort_direction)
12
- @pagy, @resources = pagy(@resources)
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
+ ), layout: false
13
18
  end
14
19
 
15
20
  def new
16
21
  @resource = @resource_class.new
22
+ render Oversee::Resources::New.new(
23
+ resource: @resource,
24
+ resource_class: @resource_class,
25
+ params: params
26
+ ), layout: false
17
27
  end
18
28
 
19
29
  def create
20
- @resource_class = params[:resource_class].constantize
21
30
  @resource = @resource_class.new(resource_params)
22
31
 
23
32
  respond_to do |format|
24
33
  if @resource.update(resource_params)
25
- format.html { redirect_to resource_path(@resource.id, resource: @resource_class) }
26
- format.turbo_stream
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 }
27
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)) }
28
39
  end
29
40
  end
30
41
  end
31
42
 
32
43
  def show
33
- resource_associations
44
+ render Oversee::Resources::Show.new(
45
+ resource: @resource,
46
+ resource_class: @resource_class,
47
+ resource_associations: resource_associations,
48
+ params: params
49
+ ), layout: false
34
50
  end
35
51
 
36
52
  def edit
37
53
  end
38
54
 
39
55
  def update
40
- @resource_class = params[:resource_class].constantize
41
- set_resource
42
-
43
- @key = params[:resource][:oversee_key]
44
- @datatype = params[:resource][:oversee_datatype]
56
+ key = params[:oversee_key]
57
+ datatype = params[:oversee_datatype]
45
58
 
46
59
  respond_to do |format|
47
60
  if @resource.update(resource_params)
48
61
  format.html { redirect_to resource_path(@resource.id, resource: @resource_class) }
49
- 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
50
66
  else
51
67
  end
52
68
  end
@@ -57,19 +73,34 @@ module Oversee
57
73
  redirect_to resources_path(resource: @resource_class)
58
74
  end
59
75
 
60
- # Non-standard actions
61
- def input_field
62
- set_resource
76
+ def association
77
+ @resources = @resource_class.find(params[:id]).send(params[:association_name])
78
+ @pagy, @resources = pagy(@resources, limit: params[:per_page] || Oversee.configuration.per_page)
79
+
80
+ render Oversee::Resources::Index.new(
81
+ resources: @resources,
82
+ resource_class: @resource_class,
83
+ pagy: @pagy,
84
+ params: params
85
+ )
86
+ end
63
87
 
64
- @key = params[:key].to_sym
65
- @value = @resource.send(@key)
66
- @datatype = @resource.class.columns_hash[@key.to_s].type
88
+ def table
89
+ if params[:association_name].present?
90
+ @resources = @resource_class.find(params[:id]).send(params[:association_name])
91
+ else
92
+ set_resources
93
+ end
94
+
95
+ component_id = dom_id(@resource_class, :table)
96
+ component = Oversee::Resources::Table.new(resource_class: @resource_class, resources: @resources, params: params)
67
97
 
68
98
  respond_to do |format|
69
99
  format.turbo_stream do
70
- render turbo_stream: [
71
- turbo_stream.replace(dom_id(@resource, @key), partial: "oversee/resources/input_field", locals: { datatype: @datatype, key: @key, value: @value })
72
- ]
100
+ render turbo_stream: turbo_stream.replace(component_id, component)
101
+ end
102
+ format.html do
103
+ render component, layout: false
73
104
  end
74
105
  end
75
106
  end
@@ -77,13 +108,21 @@ module Oversee
77
108
  private
78
109
 
79
110
  def set_resource_class
80
- @resource_class = params[:resource].constantize
111
+ @resource_class = params[:resource_class_name].constantize
81
112
  end
82
113
 
83
114
  def set_resource
84
115
  @resource = @resource_class.find(params[:id])
85
116
  end
86
117
 
118
+ def set_resources
119
+ set_sorting_rules
120
+
121
+ @resources = @resource_class.order(@sort_attribute.to_sym => sort_direction)
122
+ @resources = Oversee::Filter.new(collection: @resources, params:).apply
123
+ @resources = Oversee::Search.new(collection: @resources, resource_class: @resource_class, query: params[:query]).call
124
+ end
125
+
87
126
  def resource_associations
88
127
  @resource_associations ||= @resource_class.reflect_on_all_associations
89
128
  end
@@ -0,0 +1,3 @@
1
+ import "trix";
2
+ import "@rails/actiontext";
3
+ import "controllers";
@@ -0,0 +1,8 @@
1
+ import { Application } from "@hotwired/stimulus";
2
+ const application = Application.start();
3
+
4
+ application.debug = false;
5
+ window.Stimulus = application;
6
+
7
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
8
+ eagerLoadControllersFrom("controllers", application);
@@ -0,0 +1,7 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ console.log("Hello from reveal_controller.js");
6
+ }
7
+ }
@@ -0,0 +1,21 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ const KEY_BASE = "oversee-sidebar";
4
+
5
+ export default class extends Controller {
6
+ initialize() {
7
+ this.key = `${KEY_BASE}--${this.element.id}`;
8
+ this.expanded = localStorage.getItem(this.key) === "true";
9
+ this.element.open = this.expanded;
10
+ }
11
+
12
+ toggle() {
13
+ this.expanded = !this.expanded;
14
+ this.element.open = this.expanded;
15
+ }
16
+
17
+ persist(event) {
18
+ this.expanded = event.newState === "open";
19
+ localStorage.setItem(this.key, this.expanded);
20
+ }
21
+ }
@@ -0,0 +1,62 @@
1
+ class Oversee::Resource
2
+ attr_reader :resource_class
3
+ attr_reader :resource_class_name
4
+ attr_reader :instance
5
+ attr_reader :rich_text_associations
6
+
7
+ attr_accessor :associations
8
+
9
+ def initialize(resource_class:, instance: nil)
10
+ @resource_class = resource_class
11
+ @resource_class_name = resource_class.to_s
12
+ @instance = instance
13
+ end
14
+
15
+ # Route helpers
16
+ def resources_path
17
+
18
+ end
19
+
20
+ def resource_path
21
+ end
22
+
23
+ # Columns
24
+ def columns_for_create
25
+ excluded_columns = [resource_class.primary_key, "created_at", "updated_at"]
26
+ resource_class.columns_hash.except(*excluded_columns)
27
+ end
28
+
29
+ def columns_for_show
30
+
31
+ end
32
+
33
+ # Associations
34
+ # Structured by association macro
35
+ def associations
36
+ @associations ||= begin
37
+ map = Hash.new { |hash, key| hash[key] = [] }
38
+
39
+ @resource_class.reflect_on_all_associations.each do |association|
40
+ map[association.macro] << {
41
+ name: association.name,
42
+ class_name: association.class_name,
43
+ foreign_key: association.foreign_key,
44
+ optional: association.macro == :belongs_to ? !!association.options[:optional] : true,
45
+ through: association.options[:through],
46
+ rich_text: association.name.to_s.start_with?("rich_text_"),
47
+ }
48
+ end
49
+ map
50
+ end
51
+ end
52
+
53
+ def foreign_keys
54
+ resource_class.reflections.map do |name, reflection|
55
+ reflection.foreign_key if reflection.belongs_to?
56
+ end.compact
57
+ end
58
+
59
+ def rich_text_associations
60
+ @rich_text_associations ||= associations[:has_one].select { |association| association[:rich_text] }
61
+ end
62
+ end
@@ -0,0 +1,39 @@
1
+ class Oversee::Filter
2
+ ALLOWED_OPERATORS = %w[eq in gt gte lt lte].freeze
3
+
4
+ def initialize(collection:, params: nil)
5
+ @collection = collection
6
+ @params = params
7
+ end
8
+
9
+
10
+ def apply
11
+ return @collection if filters.blank?
12
+
13
+ filters.each do |key, constraint|
14
+ operator = constraint.keys.first
15
+ next unless ALLOWED_OPERATORS.include?(operator)
16
+
17
+ value = constraint[operator]
18
+ chain(key:, operator:, value:)
19
+ end
20
+ return @collection
21
+ end
22
+
23
+ private
24
+
25
+ # Example params hash:
26
+ # filters[kind][eq][]=get => {"kind"=>{"is"=>["get"]}}
27
+ def filters
28
+ @filters ||= @params[:filters]
29
+ end
30
+
31
+ def chain(key:, operator:, value:)
32
+ @collection = case operator
33
+ when "eq"
34
+ @collection.then { _1.where(key => value) }
35
+ when "in"
36
+ @collection.then { _1.where(key => value) }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ class Oversee::Search
2
+ DEFAULT_SEARCHABLE_ATTRIBUTES = %w[name title email email_address]
3
+
4
+ def initialize(collection:, resource_class:, query: nil)
5
+ @collection = collection
6
+ @resource_class = resource_class
7
+ @query = query
8
+ end
9
+
10
+ def call
11
+ return @collection if @query.blank? || @resource_class.blank?
12
+ default_searchable_by
13
+ end
14
+
15
+ def default_searchable_attribute
16
+ DEFAULT_SEARCHABLE_ATTRIBUTES.each do |attr|
17
+ return attr if @resource_class.column_names.include?(attr)
18
+ end
19
+
20
+ return @resource_class.primary_key
21
+ end
22
+
23
+ private
24
+
25
+ def default_searchable_by
26
+ return @collection.where(default_searchable_attribute => @query) if @resource_class.primary_key == default_searchable_attribute
27
+
28
+ @collection
29
+ .where(@resource_class.arel_table[default_searchable_attribute]
30
+ .matches("%#{@query}%"))
31
+ end
32
+
33
+
34
+ end
@@ -0,0 +1,5 @@
1
+ class Oversee::Views::Base < Phlex::HTML
2
+ def around_template
3
+ render Oversee::Layout::Application.new { super }
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ # Application
2
+ pin "application", to: "oversee/application.js", preload: true
3
+
4
+ # Rich text editor
5
+ pin "trix", to: "https://unpkg.com/trix@2.1.8"
6
+ pin "@rails/actiontext", to: "https://unpkg.com/@rails/actiontext@8.0.0"
7
+
8
+ # Turbo
9
+ pin "@hotwired/turbo-rails", to: "https://unpkg.com/@hotwired/turbo-rails"
10
+
11
+ # Stimulus
12
+ pin "@hotwired/stimulus", to: "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"
13
+ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
14
+ pin_all_from Oversee::Engine.root.join("app/javascript/oversee/controllers"), under: "controllers", to: "oversee/controllers"
@@ -0,0 +1,4 @@
1
+ en:
2
+ pagy:
3
+ prev: "« Prev"
4
+ next: "Next »"
@@ -0,0 +1,4 @@
1
+ en:
2
+ pagy:
3
+ prev: "« Prev"
4
+ next: "Next »"
data/config/routes.rb CHANGED
@@ -1,16 +1,22 @@
1
1
  Oversee::Engine.routes.draw do
2
-
3
2
  # Resources
4
- scope :resources, controller: "resources" do
5
- get ":resource/", action: :index, as: :resources
6
- get ":resource/new", action: :new, as: :new_resource
7
- post ":resource_class/", action: :create, as: :create_resource
8
- get ":resource/:id", action: :show, as: :resource
9
- get ":resource/:id/edit", action: :edit, as: :edit_resource
10
- patch ":resource/:id", action: :update, as: :update_resource
11
- delete ":resource/:id", action: :destroy, as: :destroy_resource
12
- get ":resource/:id/input_field", action: :input_field, as: :input_field
3
+ scope ":resource_class_name", controller: "resources" do
4
+ get "/", action: :index, as: :resources
5
+ post "/", action: :create, as: :create_resource
6
+ get "/new", action: :new, as: :new_resource
7
+ get "/:id", action: :show, as: :resource
8
+ get "/:id/edit", action: :edit, as: :edit_resource
9
+ patch "/:id", action: :update, as: :update_resource
10
+ delete "/:id", action: :destroy, as: :destroy_resource
11
+ get "/table", action: :table, as: :resources_table
12
+
13
+ get "/:id/input_field", action: :input_field, as: :resource_input_field
14
+ get "/:id/association", action: :association, as: :resource_association
15
+
16
+
17
+ # To reach this route using Rails' route helpers, use resource_input_path(:resource_class_name, :id)
18
+ get "/:id/input", controller: "resources/fields", action: :input, as: :resource_input
13
19
  end
14
20
 
15
- root to: "dashboard#show"
21
+ root to: "dashboard#index"
16
22
  end
@@ -0,0 +1,7 @@
1
+ class Oversee::InstallGenerator < Rails::Generators::Base
2
+ source_root File.expand_path('templates', __dir__)
3
+
4
+ def add_routes
5
+ route 'mount Oversee::Engine => "/oversee"'
6
+ end
7
+ end