Skip to content

Commit

Permalink
Merge pull request #14 from bleu-fi/refactor-dashboard-base
Browse files Browse the repository at this point in the history
refactor dashboard base
  • Loading branch information
ribeirojose authored Apr 29, 2024
2 parents 571f4fe + 0f0b247 commit a1e451f
Show file tree
Hide file tree
Showing 24 changed files with 15,830 additions and 33,370 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
inherit_from: .rubocop_todo.yml

require:
- rubocop-packaging
- rubocop-performance
Expand Down
18 changes: 18 additions & 0 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2024-04-28 23:55:55 UTC using RuboCop version 1.60.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

# Offense count: 1
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength:
Max: 128

# Offense count: 3
# Configuration parameters: Max, CountAsOne.
RSpec/ExampleLength:
Exclude:
- 'spec/lib/dashboards/base_spec.rb'
2 changes: 1 addition & 1 deletion bleuprint.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ require_relative "lib/bleuprint/version"

Gem::Specification.new do |spec|
spec.name = "bleuprint"
spec.version = "0.1.14"
spec.version = "0.1.15"
spec.authors = ["José Ribeiro", "João Victor Assis"]
spec.email = ["jose@bleu.studio", "joao@bleu.studio"]

Expand Down
226 changes: 109 additions & 117 deletions lib/bleuprint/dashboards/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,181 +4,173 @@
require "bleuprint/services/base"
require "rails"
require "active_support/core_ext"

module Bleuprint
module Dashboards
DashboardContext = Struct.new(:pagination, :sorting, :filters, keyword_init: true)

class Base < Bleuprint::Services::Base # rubocop:disable Metrics/ClassLength
ATTRIBUTE_TYPES = {}.freeze
ACTIONS = {}.freeze
COLLECTION_ATTRIBUTES = [].freeze
class Base < Bleuprint::Services::Base
COLUMN_TYPE_MAPPING = {
string: Bleuprint::Field::Text,
text: Bleuprint::Field::Text,
date: Bleuprint::Field::Date,
datetime: Bleuprint::Field::Datetime,
boolean: Bleuprint::Field::Boolean
}.freeze

attr_reader :context

def initialize(pagination, sorting, filters)
def initialize(pagination={}, sorting={}, filters={})
super()
validate_inheritance
@context = DashboardContext.new(pagination:, sorting:, filters:)
validate_required_constants
end

def call!(*, **)
self.class.call!(*, context: @context, **)
def call!
{
columns:,
filters:,
search:
}
end

def self.call!(...)
{ columns: columns(...), filters: filters(...), search: search(...) }
def columns
self.class::ATTRIBUTE_TYPES.slice(*self.class::COLLECTION_ATTRIBUTES).map do |field_name, field_class|
field_class.new(field_name, self.class, resource_class.new).as_json
end + [{ id: "actions", type: "actions", accessorKey: "actions", actions: actions_json(resource: nil) }]
end

def self.actions_json(*, resource: nil, **)
self::ACTIONS.filter_map do |key, value|
next unless value.is_a?(Bleuprint::Field::Deferred)

# TODO: add this to the test
unless Bleuprint::Field::Action.const_defined?(value.deferred_class.name)
raise "Deferred class #{value.deferred_class.name} not found."
end
def filters
self.class::COLLECTION_FILTERS.filter_map do |field_name, filter_proc|
field = find_field(field_name)
next unless field&.filterable?

value.new(key, self, resource || resource_class.new).as_json
{
title: field.label,
value: field.name,
options: field.selectable_options(context) || filter_proc&.call(nil, {})
}
end
end

def self.filters(*, **)
return unless defined?(self::COLLECTION_FILTERS)
def search
return {} unless self.class.const_defined?(:SEARCH_FILTER)

self::COLLECTION_FILTERS.map do |field_name, _filter_proc|
field = self::ATTRIBUTE_TYPES[field_name].new(field_name, self)
next unless field
next unless field.filterable?
field = find_field(self.class::SEARCH_FILTER[:name])
{
key: field.name,
placeholder: field.placeholder
}
end

filter_options = {}
filter_options[:options] = field.selectable_options if field.respond_to?(:selectable_options)
filter_options[:options] = field.filterable_options if field.respond_to?(:filterable_options)
def actions_json(resource: nil)
return [] unless self.class.const_defined?(:ACTIONS)

{
title: field.label,
value: field.name
}.merge(filter_options)
end
resource ||= resource_class.new

self.class::ACTIONS.filter_map do |key, action|
next unless action.is_a?(Bleuprint::Field::Deferred)
unless Bleuprint::Field::Action.const_defined?(action.deferred_class.name)
raise "Deferred class #{action.deferred_class.name} not found."
end

action.new(key, self.class, resource, { context: }).as_json
end.compact
end

def self.search(*, **)
return unless defined?(self::SEARCH_FILTER)
def apply_filters_and_sorting(scope)
scope = apply_filters(scope, context.filters)
scope = apply_sorting(scope, context.sorting)
scope = apply_pagination(scope, context.pagination)
total = scope.total_count

{
key: self::SEARCH_FILTER[:name]
scope: apply_field_serialization(scope),
total:,
per_page: context.pagination.fetch(:per_page, 10),
page: context.pagination.fetch(:page, 0).to_i
}
end

def self.resource_class
class_name = name.split("::").last
class_name.singularize.constantize # rubocop:disable Sorbet/ConstantsFromStrings
end
def show_page_attributes(resource)
return {} if self.class::ATTRIBUTE_TYPES.blank?

def self.base_attribute_types
resource_class.columns.to_h do |col|
[col.name.to_sym, determine_column_type(col)]
self.class::ATTRIBUTE_TYPES.slice(*self.class::SHOW_PAGE_ATTRIBUTES).map do |field_name, field_class|
field_class.new(field_name, self, resource).as_json
end
end

def self.columns(*, context: nil, **)
columns = self::ATTRIBUTE_TYPES.slice(*self::COLLECTION_ATTRIBUTES).filter_map do |k, v|
field = v.new(k, self, resource_class.new, context:)
{
accessorKey: field.name,
title: field.label,
type: field.type,
hide: field.hidden?,
field_options: { **field.options }
}
end

columns << { id: "actions", type: "actions", actions: actions_json(*) }
def resource_class
self.class.resource_class
end

def self.show_page_attributes(resource)
self::ATTRIBUTE_TYPES.slice(*self::SHOW_PAGE_ATTRIBUTES).filter_map do |k, v|
field = v.new(k, self, resource)
{
accessorKey: field.name,
title: field.label,
type: field.type,
value: field.value,
field_options: { **field.options }
}
def apply_field_serialization(scope)
scope.map do |resource|
self.class::ATTRIBUTE_TYPES.slice(*self.class::COLLECTION_ATTRIBUTES).to_h do |field_name, field_class|
field = field_class.new(field_name, self.class, resource)
[field.name.to_sym, field.value]
end.merge(actions: actions_json(resource:))
end
end

def show_page_attributes(resource)
self.class.show_page_attributes(resource)
end
private

COLUMN_TYPE_MAPPING = {
string: Bleuprint::Field::Text,
text: Bleuprint::Field::Text,
date: Bleuprint::Field::Date,
datetime: Bleuprint::Field::Datetime,
boolean: Bleuprint::Field::Boolean
}.freeze
def validate_inheritance
return if is_a?(Bleuprint::Dashboards::Base)

def self.determine_column_type(column)
COLUMN_TYPE_MAPPING[column.type] || Bleuprint::Field::Text
raise "#{self.class.name} must inherit from Bleuprint::Dashboards::Base"
end

def self.add_collection_filter(name, &block)
collection_filters[name] = block
def validate_required_constants
# ACTIONS is not required yet
required_constants = %i[ATTRIBUTE_TYPES COLLECTION_ATTRIBUTES COLLECTION_FILTERS]
missing_constants = required_constants.reject { |constant| self.class.const_defined?(constant) }
raise "Missing required constants: #{missing_constants.join(', ')}" if missing_constants.any?
end

def self.apply_filters(scope, params)
return scope if params.blank?

self::COLLECTION_FILTERS.each do |filter_name, filter_proc|
scope = filter_proc.call(scope, params) if params[filter_name].present?
end
def find_field(field_name)
self.class::ATTRIBUTE_TYPES[field_name]&.new(field_name, self.class, resource_class.new)
end

return scope unless defined?(self::SEARCH_FILTER)
def apply_filters(scope, filters)
return scope if filters.blank?

scope = self::SEARCH_FILTER[:filter].call(scope, params) if params[self::SEARCH_FILTER[:name]].present?
scope
self.class::COLLECTION_FILTERS.reduce(scope) do |current_scope, (filter_name, filter_proc)|
filters[filter_name].present? ? filter_proc.call(current_scope, filters) : current_scope
end
end

def self.apply_sorting(scope, sorting)
def apply_sorting(scope, sorting)
return scope if sorting.blank?

sort_column = sorting.fetch(:sort_column, "created_at")
sort_direction = sorting.fetch(:sort_direction, "desc")
scope.order("#{sort_column} #{sort_direction}")
scope.order("#{sorting[:sort_column] || 'created_at'} #{sorting[:sort_direction] || 'desc'}")
end

def self.apply_pagination(scope, pagination)
def apply_pagination(scope, pagination)
return scope if pagination.blank?

per_page = pagination.fetch(:per_page, 10)
page = pagination.fetch(:page, 0).to_i + 1
scope.page(page).per(per_page)
scope.page(pagination[:page].to_i + 1).per(pagination[:per_page] || 10)
end

def self.apply_field_serialization(scope)
scope.map do |resource|
attrs = self::ATTRIBUTE_TYPES.slice(*self::COLLECTION_ATTRIBUTES).filter_map do |field_name, field_type|
field = field_type.new(field_name, self, resource)
[field.name, field.value]
end.to_h

attrs[:actions] = actions_json(resource:)
class << self
def call!
new.call!
end

attrs
def apply_filters_and_sorting(scope, filters, sorting, pagination)
new(pagination, sorting, filters).apply_filters_and_sorting(
scope
)
end
end

def self.apply_filters_and_sorting(scope, filters={}, sorting={}, pagination={})
scope = apply_filters(scope, filters)
scope = apply_sorting(scope, sorting)
scope = apply_pagination(scope, pagination)
total = scope.total_count
def apply_field_serialization(scope)
new.apply_field_serialization(scope)
end

{
scope: apply_field_serialization(scope),
total:,
per_page: pagination.fetch(:per_page, 10),
page: pagination.fetch(:page, 0).to_i
}
def resource_class
name.split("::").last.singularize.delete_suffix("Dashboard").constantize # rubocop:disable Sorbet/ConstantsFromStrings
end
end
end
end
Expand Down
19 changes: 19 additions & 0 deletions lib/bleuprint/field/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ def hidden?
false
end
end

def selectable_options(_context=nil)
[]
end

def placeholder
nil
end

def as_json
{
accessorKey: name,
title: label,
type:,
value:,
hide: hidden?,
field_options: options
}
end
end
end
end
2 changes: 1 addition & 1 deletion lib/bleuprint/field/select.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def value
end&.first || resource.send(attribute)
end

def selectable_options
def selectable_options(_context=nil)
values =
if options.key?(:collection)
options.fetch(:collection)
Expand Down
File renamed without changes.
Loading

0 comments on commit a1e451f

Please sign in to comment.