Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ gem 'sentry-rails'

gem 'web-console'

# Search
gem 'ransack'

group :development, :test do
gem 'brakeman', require: false
gem 'debug', platforms: %i[mri windows], require: 'debug/prelude'
Expand Down
7 changes: 6 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.10)
rack (3.1.12)
rack-protection (4.1.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
Expand Down Expand Up @@ -355,6 +355,10 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
ransack (4.3.0)
activerecord (>= 6.1.5)
activesupport (>= 6.1.5)
i18n
rbs (3.8.1)
logger
rdoc (6.12.0)
Expand Down Expand Up @@ -568,6 +572,7 @@ DEPENDENCIES
rails (~> 7.2.0)
rails-controller-testing
rails-mermaid_erd
ransack
redis (>= 4.0.1)
rolify
rspec-rails
Expand Down
62 changes: 15 additions & 47 deletions app/controllers/admin/users_controller.rb
Original file line number Diff line number Diff line change
@@ -1,51 +1,19 @@
module Admin
class UsersController < BaseController
before_action :set_user, only: %i[show edit update destroy]

def index
@pagy, @users = pagy(User.order(created_at: :desc))
end

def show; end

def new
@user = User.new
end

def edit; end

def create
@user = User.new(user_params)

if @user.save
redirect_to admin_users_path(@user), notice: 'User was successfully created.'
else
render :new
end
end

def update
if @user.update(user_params)
redirect_to admin_users_path(@user), notice: 'User was successfully updated.'
else
render :edit
end
end

def destroy
@user.destroy!
redirect_to admin_users_url, notice: 'User was successfully destroyed.'
end

private

def set_user
@user = User.find(params[:id])
authorize(@user)
end

def user_params
params.require(:user).permit(policy(@user).permitted_attributes)
end
include Crudable

crud_to class: User,
collection_variable: :@users,
object_variable: :@user,
collection_path: :admmin_users_path,
object_path: :admin_user_path,
searchable: true,
modal_form: false,
# collection_includes:,
flash_messages: {
created: I18n.t('common.create.success', model: :user),
updated: I18n.t('common.update.success', model: :user),
deleted: I18n.t('common.delete.success', model: :user)
}
end
end
11 changes: 11 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ def context_view_path(*strs)
File.join([controller_path].push(*strs))
end

def current_user
@current_user ||= super.tap do |user|
if user.present?
ActiveRecord::Associations::Preloader.new(
records: [user],
associations: :roles
).call
end
end
end

private

def not_authorized
Expand Down
209 changes: 209 additions & 0 deletions app/controllers/concerns/crudable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# Idea for this module from Mr Ben, https://github.com/tranquangvu
# Copyright (c) 2022 "Golden Owl Solutions", MIT License
# https://github.com/GoldenowlConsultingCompany

module Crudable
extend ActiveSupport::Concern

included do
before_action :prepare_collection, only: %i[index]
before_action :prepare_new_object, only: %i[new create]
before_action :prepare_object, only: %i[show edit update destroy]

assign_resource_class_accessors
end

module ClassMethods
attr_accessor :resource_class,
:resource_collection_variable,
:resource_object_variable,
:resource_query_variable,
:resource_pagy_variable,
:resource_route_namespaces,
:resource_collection_path,
:resource_object_path,
:resource_modal_form,
:resource_collection_includes,
:resource_object_includes,
:resource_flash_messages,
:resource_searchable

RESOURCE_ACTIONS = %i[index show new create edit update destroy].freeze

def crud_to(options = {})
options.symbolize_keys!
options.assert_valid_keys(
:class,
:searchable,
:collection_variable,
:object_variable,
:route_namespaces,
:collection_path,
:object_path,
:modal_form,
:collection_includes,
:object_includes,
:flash_messages,
:only_actions,
:except_actions
)

only_actions = options.delete(:only_actions)
only_actions = only_actions.present? ? only_actions.map(&:to_sym) : RESOURCE_ACTIONS
except_actions = options.delete(:except_actions)
except_actions = except_actions.present? ? except_actions.map(&:to_sym) : []
effect_actions = only_actions - except_actions

assign_resource_class_accessors(options)
check_define_resource_actions(effect_actions)
format_request_resource_actions(effect_actions)
end

private

def assign_resource_class_accessors(options = {}) # rubocop:disable Metrics/AbcSize
self.resource_class = options.fetch(:class, (name.split('::').last.sub(/Controller$/, '').singularize.constantize rescue nil)) # rubocop:disable Style/RescueModifier
self.resource_collection_variable = options.fetch(:collection_variable, ("@#{resource_class.name.underscore.pluralize}" rescue :collection)).to_sym # rubocop:disable Style/RescueModifier
self.resource_object_variable = options.fetch(:object_variable, ("@#{resource_class.name.underscore}" rescue :object)).to_sym # rubocop:disable Style/RescueModifier
self.resource_searchable = options.fetch(:searchable, true)
self.resource_pagy_variable = :@pagy
self.resource_query_variable = :@q

self.resource_route_namespaces = options.fetch(:route_namespaces, name.underscore.split('/')[0..-2]).map(&:to_sym)
self.resource_collection_path = options.fetch(:collection_path, nil)
self.resource_object_path = options.fetch(:object_path, nil)

self.resource_modal_form = options.fetch(:modal_form, false)
self.resource_collection_includes = options.fetch(:collection_includes, [])
self.resource_object_includes = options.fetch(:object_includes, [])
self.resource_flash_messages = options.fetch(:flash_messages, {})
end

def check_define_resource_actions(actions)
(RESOURCE_ACTIONS - actions).each do |action|
undef_method(action)
end
end

def format_request_resource_actions(actions)
effected_actions = actions & %i[new edit]
only_turbo_stream_for(*effected_actions) if resource_modal_form
end
end

def index
pagy, collection = pagy(instance_variable_get(self.class.resource_collection_variable))
instance_variable_set(self.class.resource_pagy_variable, pagy)
instance_variable_set(self.class.resource_collection_variable, collection)
block_given? ? yield : render(:index)
end

def show
render(:show)
end

def new
render(:new)
end

def create
object = instance_variable_get(self.class.resource_object_variable)
object.assign_attributes(resource_permitted_params)

created = object.save

return yield(created) if block_given?

if created
set_flash_message(:notice, :created)
redirect_to resource_after_create_or_update_path
else
template = self.class.resource_modal_form ? :reform : :new
render(template, status: :unprocessable_entity)
end
end

def edit
render(:edit)
end

def update
object = instance_variable_get(self.class.resource_object_variable)
updated = object.update(resource_permitted_params)

return yield(updated) if block_given?

if updated
set_flash_message(:notice, :updated)
redirect_to resource_after_create_or_update_path
else
template = self.class.resource_modal_form ? :reform : :edit
render(template, status: :unprocessable_entity)
end
end

def destroy
object = instance_variable_get(self.class.resource_object_variable)
object.destroy!

return yield(object) if block_given?

set_flash_message(:notice, :deleted)
redirect_to(resource_after_destroy_path)
end

private

def set_flash_message(type, key, flash_now: false)
message = self.class.resource_flash_messages.fetch(key, nil)
(flash_now ? flash.now : flash)[type] = message if message
end

def resource_object_path
object = instance_variable_get(self.class.resource_object_variable)
custom_path_method = self.class.resource_object_path
custom_path_method ? send(custom_path_method, object) : polymorphic_path([*self.class.resource_route_namespaces, object])
end

def resource_collection_path
custom_path_method = self.class.resource_collection_path
custom_path_method ? send(custom_path_method) : polymorphic_path([*self.class.resource_route_namespaces, self.class.resource_class])
end

def resource_after_create_or_update_path
self.class.resource_modal_form ? resource_collection_path : resource_object_path
end

def resource_after_destroy_path
resource_collection_path
end

def resource_permitted_params
raise NotImplementedError, "You must define `resource_permitted_params` as instance method in #{self.class.name} class"
end

def resource_base_scope
policy_scope(self.class.resource_class)
end

def prepare_collection
q = resource_base_scope.ransack(params[:q])
collection = q.result(distinct: true).includes(self.class.resource_collection_includes)

instance_variable_set(self.class.resource_query_variable, q)
instance_variable_set(self.class.resource_collection_variable, collection)
authorize(collection)
end

def prepare_new_object
object = self.class.resource_class.new
instance_variable_set(self.class.resource_object_variable, object)
authorize(object)
end

def prepare_object
object = resource_base_scope.includes(self.class.resource_object_includes).find(params[:id])
instance_variable_set(self.class.resource_object_variable, object)
authorize(object)
end
end
30 changes: 30 additions & 0 deletions app/frontend/controllers/shared/sidebar_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
static targets = ['menu'];

connect() {
// Check if we're on a large screen
if (window.innerWidth >= 1024) {
// Initialize from cookie
const isCollapsed = document.cookie.includes('sidebar_collapsed=true');
if (isCollapsed) {
document.documentElement.classList.add('drawer-mini');
}
}
}

toggleCollapse(event) {
event.preventDefault();

const isCollapsed = document.documentElement.classList.contains('drawer-mini');

if (isCollapsed) {
document.cookie = 'sidebar_collapsed=false; path=/';
document.documentElement.classList.remove('drawer-mini');
} else {
document.cookie = 'sidebar_collapsed=true; path=/';
document.documentElement.classList.add('drawer-mini');
}
}
}
6 changes: 3 additions & 3 deletions app/frontend/entrypoints/admin.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import '../stylesheets/admin/index.scss';
import '../controllers/admin';
import '../controllers/shared';
import '@/stylesheets/admin/index.scss';
import '@/controllers/admin';
import '@/controllers/shared';
import '@hotwired/turbo-rails';
4 changes: 2 additions & 2 deletions app/frontend/entrypoints/application.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import '~/stylesheets/application/index.scss';
import '../controllers/application';
import '../controllers/shared';
import '@/controllers/application';
import '@/controllers/shared';
import '@hotwired/turbo-rails';
Loading
Loading