goodmin 0.0.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1359 -0
- data/Rakefile +56 -0
- data/app/assets/stylesheets/goodmin/application.css +75 -0
- data/app/controllers/goodmin/application_controller.rb +43 -0
- data/app/controllers/goodmin/resource_controller.rb +235 -0
- data/app/helpers/goodmin/application_helper.rb +45 -0
- data/app/javascript/goodmin/application.js +8 -0
- data/app/javascript/goodmin/controllers/batch_actions_controller.js +101 -0
- data/app/javascript/goodmin/controllers/datetimepicker_controller.js +24 -0
- data/app/javascript/goodmin/controllers/navigation_controller.js +30 -0
- data/app/jobs/goodmin/application_job.rb +4 -0
- data/app/mailers/goodmin/application_mailer.rb +6 -0
- data/app/models/goodmin/application_record.rb +5 -0
- data/app/views/goodmin/application/welcome.html.erb +17 -0
- data/app/views/goodmin/fields/association/_form.html.erb +19 -0
- data/app/views/goodmin/fields/association/_index.html.erb +6 -0
- data/app/views/goodmin/fields/association/_show.html.erb +6 -0
- data/app/views/goodmin/fields/boolean/_form.html.erb +7 -0
- data/app/views/goodmin/fields/boolean/_index.html.erb +1 -0
- data/app/views/goodmin/fields/boolean/_show.html.erb +1 -0
- data/app/views/goodmin/fields/date/_form.html.erb +6 -0
- data/app/views/goodmin/fields/date/_index.html.erb +1 -0
- data/app/views/goodmin/fields/date/_show.html.erb +1 -0
- data/app/views/goodmin/fields/date_time/_form.html.erb +6 -0
- data/app/views/goodmin/fields/date_time/_index.html.erb +1 -0
- data/app/views/goodmin/fields/date_time/_show.html.erb +1 -0
- data/app/views/goodmin/fields/enum/_index.html.erb +1 -0
- data/app/views/goodmin/fields/enum/_show.html.erb +1 -0
- data/app/views/goodmin/fields/nested_has_one/_form.html.erb +6 -0
- data/app/views/goodmin/fields/nested_has_one/_index.html.erb +1 -0
- data/app/views/goodmin/fields/nested_has_one/_show.html.erb +1 -0
- data/app/views/goodmin/fields/number/_form.html.erb +5 -0
- data/app/views/goodmin/fields/number/_index.html.erb +1 -0
- data/app/views/goodmin/fields/number/_show.html.erb +1 -0
- data/app/views/goodmin/fields/password/_form.html.erb +5 -0
- data/app/views/goodmin/fields/password/_index.html.erb +1 -0
- data/app/views/goodmin/fields/password/_show.html.erb +1 -0
- data/app/views/goodmin/fields/select/_form.html.erb +5 -0
- data/app/views/goodmin/fields/select/_index.html.erb +1 -0
- data/app/views/goodmin/fields/select/_show.html.erb +1 -0
- data/app/views/goodmin/fields/string/_form.html.erb +5 -0
- data/app/views/goodmin/fields/string/_index.html.erb +1 -0
- data/app/views/goodmin/fields/string/_show.html.erb +1 -0
- data/app/views/goodmin/fields/text/_form.html.erb +5 -0
- data/app/views/goodmin/fields/text/_index.html.erb +1 -0
- data/app/views/goodmin/fields/text/_show.html.erb +1 -0
- data/app/views/goodmin/resource/_actions.html.erb +9 -0
- data/app/views/goodmin/resource/_batch_actions.html.erb +12 -0
- data/app/views/goodmin/resource/_breadcrumb.html.erb +33 -0
- data/app/views/goodmin/resource/_breadcrumb_actions.html.erb +41 -0
- data/app/views/goodmin/resource/_button_actions.html.erb +3 -0
- data/app/views/goodmin/resource/_errors.html.erb +9 -0
- data/app/views/goodmin/resource/_export_actions.html.erb +15 -0
- data/app/views/goodmin/resource/_filters.html.erb +22 -0
- data/app/views/goodmin/resource/_form.html.erb +26 -0
- data/app/views/goodmin/resource/_pagination.html.erb +40 -0
- data/app/views/goodmin/resource/_scopes.html.erb +14 -0
- data/app/views/goodmin/resource/_table.html.erb +45 -0
- data/app/views/goodmin/resource/columns/_actions.html.erb +28 -0
- data/app/views/goodmin/resource/edit.html.erb +5 -0
- data/app/views/goodmin/resource/index.csv.csvbuilder +5 -0
- data/app/views/goodmin/resource/index.html.erb +10 -0
- data/app/views/goodmin/resource/index.json.jbuilder +3 -0
- data/app/views/goodmin/resource/new.html.erb +5 -0
- data/app/views/goodmin/resource/show.html.erb +11 -0
- data/app/views/goodmin/resource/show.json.jbuilder +1 -0
- data/app/views/goodmin/sessions/new.html.erb +11 -0
- data/app/views/goodmin/shared/_navigation.html.erb +0 -0
- data/app/views/goodmin/shared/_navigation_aside.html.erb +7 -0
- data/app/views/layouts/goodmin/_content.html.erb +13 -0
- data/app/views/layouts/goodmin/_layout.html.erb +22 -0
- data/app/views/layouts/goodmin/application.html.erb +28 -0
- data/app/views/layouts/goodmin/login.html.erb +18 -0
- data/config/importmap.rb +5 -0
- data/config/locales/en.yml +49 -0
- data/config/locales/pl-BR.yml +49 -0
- data/config/locales/pt-BR.yml +49 -0
- data/config/locales/sv.yml +49 -0
- data/config/routes.rb +3 -0
- data/lib/generators/goodmin/authentication/authentication_generator.rb +41 -0
- data/lib/generators/goodmin/authentication/templates/sessions_controller.rb +9 -0
- data/lib/generators/goodmin/install/install_generator.rb +41 -0
- data/lib/generators/goodmin/policy/policy_generator.rb +7 -0
- data/lib/generators/goodmin/policy/templates/policy.rb +23 -0
- data/lib/generators/goodmin/resource/resource_generator.rb +31 -0
- data/lib/generators/goodmin/resource/templates/resource.rb +25 -0
- data/lib/generators/goodmin/resource/templates/resource_controller.rb +9 -0
- data/lib/generators/goodmin/resource/templates/resource_model.rb +4 -0
- data/lib/generators/goodmin/resource/templates/resource_service.rb +23 -0
- data/lib/goodmin/authentication/sessions_controller.rb +46 -0
- data/lib/goodmin/authentication/user.rb +27 -0
- data/lib/goodmin/authentication.rb +35 -0
- data/lib/goodmin/authorization/policy.rb +41 -0
- data/lib/goodmin/authorization.rb +69 -0
- data/lib/goodmin/engine.rb +30 -0
- data/lib/goodmin/fields/association.rb +62 -0
- data/lib/goodmin/fields/base.rb +57 -0
- data/lib/goodmin/fields/boolean.rb +6 -0
- data/lib/goodmin/fields/date.rb +6 -0
- data/lib/goodmin/fields/date_time.rb +6 -0
- data/lib/goodmin/fields/enum.rb +15 -0
- data/lib/goodmin/fields/nested_has_one.rb +41 -0
- data/lib/goodmin/fields/number.rb +6 -0
- data/lib/goodmin/fields/password.rb +6 -0
- data/lib/goodmin/fields/select.rb +19 -0
- data/lib/goodmin/fields/string.rb +6 -0
- data/lib/goodmin/fields/text.rb +6 -0
- data/lib/goodmin/generators/base.rb +49 -0
- data/lib/goodmin/generators/named_base.rb +31 -0
- data/lib/goodmin/helpers/application.rb +47 -0
- data/lib/goodmin/helpers/batch_actions.rb +21 -0
- data/lib/goodmin/helpers/filters.rb +123 -0
- data/lib/goodmin/helpers/forms.rb +42 -0
- data/lib/goodmin/helpers/navigation.rb +52 -0
- data/lib/goodmin/helpers/tables.rb +25 -0
- data/lib/goodmin/helpers/translations.rb +19 -0
- data/lib/goodmin/paginator.rb +55 -0
- data/lib/goodmin/resolver.rb +141 -0
- data/lib/goodmin/resources/attribute.rb +46 -0
- data/lib/goodmin/resources/form_builder.rb +96 -0
- data/lib/goodmin/resources/form_component.rb +65 -0
- data/lib/goodmin/resources/form_components/col.rb +33 -0
- data/lib/goodmin/resources/form_components/row.rb +25 -0
- data/lib/goodmin/resources/form_components/section.rb +51 -0
- data/lib/goodmin/resources/form_components/tab.rb +49 -0
- data/lib/goodmin/resources/resource/associations.rb +23 -0
- data/lib/goodmin/resources/resource/batch_actions.rb +56 -0
- data/lib/goodmin/resources/resource/filters.rb +44 -0
- data/lib/goodmin/resources/resource/ordering.rb +41 -0
- data/lib/goodmin/resources/resource/pagination.rb +22 -0
- data/lib/goodmin/resources/resource/scopes.rb +61 -0
- data/lib/goodmin/resources/resource.rb +199 -0
- data/lib/goodmin/resources/resource_controller/batch_actions.rb +49 -0
- data/lib/goodmin/resources/resource_service/associations.rb +23 -0
- data/lib/goodmin/resources/resource_service/batch_actions.rb +52 -0
- data/lib/goodmin/resources/resource_service/filters.rb +44 -0
- data/lib/goodmin/resources/resource_service/ordering.rb +41 -0
- data/lib/goodmin/resources/resource_service/pagination.rb +22 -0
- data/lib/goodmin/resources/resource_service/scopes.rb +61 -0
- data/lib/goodmin/resources/resource_service.rb +199 -0
- data/lib/goodmin/service_locator.rb +25 -0
- data/lib/goodmin/version.rb +3 -0
- data/lib/goodmin.rb +44 -0
- data/lib/tasks/goodmin_tasks.rake +4 -0
- metadata +461 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require "bundler/setup"
|
|
3
|
+
rescue LoadError
|
|
4
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require "rdoc/task"
|
|
8
|
+
|
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
|
10
|
+
rdoc.rdoc_dir = "rdoc"
|
|
11
|
+
rdoc.title = "Goodmin"
|
|
12
|
+
rdoc.options << "--line-numbers"
|
|
13
|
+
rdoc.rdoc_files.include("README.rdoc")
|
|
14
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
|
18
|
+
load "rails/tasks/engine.rake"
|
|
19
|
+
|
|
20
|
+
Bundler::GemHelper.install_tasks
|
|
21
|
+
|
|
22
|
+
require "rake/testtask"
|
|
23
|
+
|
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
|
25
|
+
t.libs << "lib"
|
|
26
|
+
t.libs << "test"
|
|
27
|
+
t.pattern = "test/**/*_test.rb"
|
|
28
|
+
t.warning = false
|
|
29
|
+
t.verbose = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
task default: :test
|
|
33
|
+
|
|
34
|
+
namespace :sandbox do
|
|
35
|
+
desc "Generate the Sandbox app then push it to GitHub which deploys it to Heroku"
|
|
36
|
+
task :deploy do
|
|
37
|
+
message = "Generated from: https://github.com/varvet/goodmin/commit/#{`git rev-parse HEAD`.strip}"
|
|
38
|
+
template_path = File.expand_path("../template.rb", __FILE__)
|
|
39
|
+
Bundler.with_unbundled_env do
|
|
40
|
+
Dir.mktmpdir do |dir|
|
|
41
|
+
Dir.chdir(dir)
|
|
42
|
+
system("git clone git@github.com:varvet/goodmin-sandbox.git")
|
|
43
|
+
if $CHILD_STATUS.success?
|
|
44
|
+
Dir.chdir("goodmin-sandbox")
|
|
45
|
+
system("rm -rf *")
|
|
46
|
+
system("rails _5.2.5_ new . -d postgresql -m #{template_path} --without-engine --skip-spring")
|
|
47
|
+
if $CHILD_STATUS.success?
|
|
48
|
+
system("git add --all")
|
|
49
|
+
system("git commit -m '#{message}'")
|
|
50
|
+
system("git push origin master")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin-bottom: 20px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
#login {
|
|
6
|
+
margin-top: 20px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#breadcrumb .dropdown:before {
|
|
10
|
+
content: " ";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#scopes {
|
|
14
|
+
margin-bottom: 20px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#filters .filter .control-label {
|
|
18
|
+
display: block;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#filters .filter .form-control {
|
|
22
|
+
min-width: 170px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#actions {
|
|
26
|
+
margin-bottom: 20px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#actions::after {
|
|
30
|
+
display: block;
|
|
31
|
+
content: "";
|
|
32
|
+
clear: both;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#table td {
|
|
36
|
+
vertical-align: middle;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#table td .btn-group {
|
|
40
|
+
display: flex;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.pagination {
|
|
44
|
+
margin: 0 0 10px 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.pagination-entries {
|
|
48
|
+
line-height: 35px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.highlight {
|
|
52
|
+
-moz-animation: highlight 3s;
|
|
53
|
+
-webkit-animation: highlight 3s;
|
|
54
|
+
animation: highlight 3s;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@-moz-keyframes highlight {
|
|
58
|
+
from {
|
|
59
|
+
background: #fffbcc;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
to {
|
|
63
|
+
background: none;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@-webkit-keyframes highlight {
|
|
68
|
+
from {
|
|
69
|
+
background: #fffbcc;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
to {
|
|
73
|
+
background: none;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
class ApplicationController < ActionController::Base
|
|
3
|
+
include Goodmin::Helpers::Translations
|
|
4
|
+
|
|
5
|
+
helper Goodmin::Helpers::Application
|
|
6
|
+
helper Goodmin::Helpers::Forms
|
|
7
|
+
helper Goodmin::Helpers::Navigation
|
|
8
|
+
helper Goodmin::Helpers::Translations
|
|
9
|
+
|
|
10
|
+
helper_method :authentication_enabled?
|
|
11
|
+
helper_method :authorization_enabled?
|
|
12
|
+
|
|
13
|
+
before_action :append_view_paths
|
|
14
|
+
|
|
15
|
+
layout "goodmin/application"
|
|
16
|
+
|
|
17
|
+
def welcome; end
|
|
18
|
+
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def append_view_paths
|
|
24
|
+
append_view_path Goodmin::Resolver.resolvers(controller_path)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def disable_authentication
|
|
28
|
+
@_disable_authentication = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def disable_authorization
|
|
32
|
+
@_disable_authorization = true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def authentication_enabled?
|
|
36
|
+
!@_disable_authentication && singleton_class.include?(Goodmin::Authentication)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def authorization_enabled?
|
|
40
|
+
!@_disable_authorization && singleton_class.include?(Goodmin::Authorization)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
require "goodmin/helpers/batch_actions"
|
|
2
|
+
|
|
3
|
+
module Goodmin
|
|
4
|
+
class ResourceController < ApplicationController
|
|
5
|
+
include Goodmin::Resources::Resource::BatchActions
|
|
6
|
+
|
|
7
|
+
helper Goodmin::Helpers::BatchActions
|
|
8
|
+
helper Goodmin::Helpers::Filters
|
|
9
|
+
helper Goodmin::Helpers::Tables
|
|
10
|
+
|
|
11
|
+
before_action :set_resource_service
|
|
12
|
+
before_action :set_resource_class
|
|
13
|
+
before_action :set_resource_parents
|
|
14
|
+
before_action :set_resources, only: :index
|
|
15
|
+
before_action :handle_batch_action, only: :update, if: -> { params[:batch_action].present? }
|
|
16
|
+
before_action :set_resource, only: [:show, :new, :edit, :create, :update, :destroy]
|
|
17
|
+
|
|
18
|
+
def index
|
|
19
|
+
respond_to do |format|
|
|
20
|
+
format.html
|
|
21
|
+
format.json
|
|
22
|
+
format.csv
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def show
|
|
27
|
+
respond_to do |format|
|
|
28
|
+
format.html
|
|
29
|
+
format.json
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def new; end
|
|
34
|
+
|
|
35
|
+
def edit; end
|
|
36
|
+
|
|
37
|
+
def create
|
|
38
|
+
respond_to do |format|
|
|
39
|
+
if @resource_service.create_resource(@resource)
|
|
40
|
+
format.html { redirect_to redirect_after_create, notice: redirect_flash_message }
|
|
41
|
+
format.json { render :show, status: :created, location: @resource }
|
|
42
|
+
else
|
|
43
|
+
format.html { render :edit }
|
|
44
|
+
format.json { render json: @resource.errors, status: :unprocessable_entity }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def update
|
|
50
|
+
respond_to do |format|
|
|
51
|
+
if @resource_service.update_resource(@resource, resource_params)
|
|
52
|
+
format.html { redirect_to redirect_after_update, notice: redirect_flash_message }
|
|
53
|
+
format.json { render :show, status: :ok, location: @resource }
|
|
54
|
+
else
|
|
55
|
+
format.html { render :edit }
|
|
56
|
+
format.json { render json: @resource.errors, status: :unprocessable_entity }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def destroy
|
|
62
|
+
@resource_service.destroy_resource(@resource)
|
|
63
|
+
|
|
64
|
+
respond_to do |format|
|
|
65
|
+
format.html { redirect_to redirect_after_destroy, notice: redirect_flash_message }
|
|
66
|
+
format.json { head :no_content }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
protected
|
|
71
|
+
|
|
72
|
+
def set_resource_service
|
|
73
|
+
@resource_service = resource_service
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def set_resource_class
|
|
77
|
+
@resource_class = resource_class
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def set_resource_parents
|
|
81
|
+
@resource_parents = resource_parents
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def set_resources
|
|
85
|
+
@resources = resources
|
|
86
|
+
authorize(@resources) if authorization_enabled?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def set_resource
|
|
90
|
+
@resource = resource
|
|
91
|
+
authorize(@resource) if authorization_enabled?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def resource_service_class
|
|
95
|
+
resource_name = controller_path.split("/").last.singularize.classify
|
|
96
|
+
"Goodmin::Resources::#{resource_name}Resource".constantize
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def resource_service
|
|
100
|
+
resource_service = resource_service_class.new
|
|
101
|
+
|
|
102
|
+
if authentication_enabled?
|
|
103
|
+
resource_service.options[:admin_user] = admin_user
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if resource_parents.present?
|
|
107
|
+
resource_service.options[:resource_parent] = resource_parents.last
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
resource_service
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def resource_class
|
|
114
|
+
@resource_service.resource_class
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def resource_parents
|
|
118
|
+
params.to_unsafe_h.each_with_object([]) do |(name, value), parents|
|
|
119
|
+
if name =~ /(.+)_id$/
|
|
120
|
+
parents << $1.classify.constantize.find(value)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def resources
|
|
126
|
+
@resource_service.resources(params)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def resource
|
|
130
|
+
if params[:id]
|
|
131
|
+
@resource_service.find_resource(params[:id])
|
|
132
|
+
else
|
|
133
|
+
case action_name
|
|
134
|
+
when "create"
|
|
135
|
+
@resource_service.build_resource(resource_params)
|
|
136
|
+
when "new"
|
|
137
|
+
@resource_service.build_resource(nil)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def resource_params
|
|
143
|
+
params.require(@resource_class.model_name.param_key.to_sym).permit(resource_params_defaults)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def resource_params_defaults
|
|
147
|
+
@resource_service.attrs_for_form.map do |attribute|
|
|
148
|
+
association = @resource_class.reflect_on_association(attribute.name)
|
|
149
|
+
|
|
150
|
+
if association && association.macro == :belongs_to
|
|
151
|
+
association.foreign_key.to_sym
|
|
152
|
+
elsif association && (many_to_many_association?(association) || has_many_association?(association))
|
|
153
|
+
{ "#{attribute.name.to_s.singularize}_ids".to_sym => [] }
|
|
154
|
+
elsif association && nested_attributes_accepted?(attribute.name)
|
|
155
|
+
{ "#{attribute.name}_attributes".to_sym => nested_attribute_permit_list(association) }
|
|
156
|
+
else
|
|
157
|
+
attribute.name
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def nested_attributes_accepted?(attribute_name)
|
|
163
|
+
@resource_class.method_defined?("#{attribute_name}_attributes=")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def nested_attribute_permit_list(association)
|
|
167
|
+
service_class = Goodmin::ServiceLocator.find_service_class_for(association.klass, context_service_class: @resource_service.class)
|
|
168
|
+
attrs = service_class&.attrs_for_form || []
|
|
169
|
+
[:id] + attrs.map(&:name)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def many_to_many_association?(association)
|
|
173
|
+
association.macro == :has_and_belongs_to_many ||
|
|
174
|
+
(association.macro == :has_many && association.options[:through].present?)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def has_many_association?(association)
|
|
178
|
+
association.macro == :has_many && !association.options[:through].present?
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def redirect_after_create
|
|
182
|
+
redirect_after_save
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def redirect_after_update
|
|
186
|
+
redirect_after_save
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def redirect_after_save
|
|
190
|
+
[*@resource_parents, @resource]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def redirect_after_destroy
|
|
194
|
+
[*@resource_parents, resource_class.model_name.route_key.to_sym]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def redirect_flash_message
|
|
198
|
+
translate_scoped("flash.#{action_name}", resource: @resource.class.model_name.human)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def handle_batch_action
|
|
202
|
+
action = params[:batch_action].to_sym
|
|
203
|
+
ids = params[:id].to_s.split(",")
|
|
204
|
+
records = @resource_class.where(id: ids)
|
|
205
|
+
|
|
206
|
+
@resource_service.batch_action(action, records)
|
|
207
|
+
|
|
208
|
+
redirect_path = if respond_to?("redirect_after_batch_action_#{action}", true)
|
|
209
|
+
send("redirect_after_batch_action_#{action}")
|
|
210
|
+
else
|
|
211
|
+
[*@resource_parents, @resource_class.model_name.route_key.to_sym]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
redirect_to redirect_path
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
def disable_authentication
|
|
220
|
+
@_disable_authentication = true
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def disable_authorization
|
|
224
|
+
@_disable_authorization = true
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def authentication_enabled?
|
|
228
|
+
!@_disable_authentication && singleton_class.include?(Goodmin::Authentication)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def authorization_enabled?
|
|
232
|
+
!@_disable_authorization && singleton_class.include?(Goodmin::Authorization)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
# Renders the provided partial with locals if it exists, otherwise
|
|
4
|
+
# yields the given block. The lookup context call is cached for
|
|
5
|
+
# each partial.
|
|
6
|
+
# Returns the display name for a resource record by looking up its service
|
|
7
|
+
# class via ServiceLocator and calling display_name on it. Falls back to
|
|
8
|
+
# record.to_s if no service class is found.
|
|
9
|
+
def resource_display_name(record)
|
|
10
|
+
service_class = Goodmin::ServiceLocator.find_service_class_for(
|
|
11
|
+
record.class,
|
|
12
|
+
context_service_class: @resource_service&.class
|
|
13
|
+
)
|
|
14
|
+
service = service_class&.new
|
|
15
|
+
service ? service.display_name(record) : record.to_s
|
|
16
|
+
rescue StandardError
|
|
17
|
+
record.to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def partial_override(partial, locals = {}, &block)
|
|
21
|
+
@_partial_override ||= {}
|
|
22
|
+
|
|
23
|
+
unless @_partial_override.key?(partial)
|
|
24
|
+
@_partial_override[partial] = lookup_context.exists?(partial, nil, true)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if @_partial_override[partial]
|
|
28
|
+
render partial: partial, locals: locals
|
|
29
|
+
else
|
|
30
|
+
capture(&block)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Wraps the policy helper so that it is always accessible, even when
|
|
35
|
+
# authorization is not enabled. When that is the case, it returns a
|
|
36
|
+
# policy that always returns true.
|
|
37
|
+
def policy(resource)
|
|
38
|
+
if authorization_enabled?
|
|
39
|
+
controller.policy(resource)
|
|
40
|
+
else
|
|
41
|
+
Authorization::Policy.new(nil, nil, default: true)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Application } from "@hotwired/stimulus"
|
|
2
|
+
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
3
|
+
|
|
4
|
+
const application = Application.start()
|
|
5
|
+
application.debug = false
|
|
6
|
+
window.Stimulus = application
|
|
7
|
+
|
|
8
|
+
eagerLoadControllersFrom("controllers", application)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["selectAll", "selectNone", "checkbox", "checkboxContainer", "actionLink"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this.updateState()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
toggle(event) {
|
|
11
|
+
event.preventDefault()
|
|
12
|
+
if (this.checkedIds().length > 0) {
|
|
13
|
+
this.checkboxTargets.forEach(cb => { cb.checked = false })
|
|
14
|
+
} else {
|
|
15
|
+
this.checkboxTargets.forEach(cb => { cb.checked = true })
|
|
16
|
+
}
|
|
17
|
+
this.updateActions()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
containerClick(event) {
|
|
21
|
+
if (event.currentTarget === event.target) {
|
|
22
|
+
event.currentTarget.querySelector("[data-batch-actions-target='checkbox']").click()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
checkboxChange() {
|
|
27
|
+
this.updateActions()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
triggerAction(event) {
|
|
31
|
+
event.preventDefault()
|
|
32
|
+
const link = event.currentTarget
|
|
33
|
+
const confirmMessage = link.dataset.confirm
|
|
34
|
+
if (confirmMessage && !window.confirm(confirmMessage)) return
|
|
35
|
+
|
|
36
|
+
const action = link.getAttribute("href") + "/" + this.checkedIds().join(",")
|
|
37
|
+
const batchAction = link.dataset.value
|
|
38
|
+
const csrfToken = document.querySelector("meta[name='csrf-token']")?.content
|
|
39
|
+
|
|
40
|
+
const form = document.createElement("form")
|
|
41
|
+
form.method = "post"
|
|
42
|
+
form.action = action
|
|
43
|
+
|
|
44
|
+
const methodInput = document.createElement("input")
|
|
45
|
+
methodInput.type = "hidden"
|
|
46
|
+
methodInput.name = "_method"
|
|
47
|
+
methodInput.value = "patch"
|
|
48
|
+
form.appendChild(methodInput)
|
|
49
|
+
|
|
50
|
+
const batchInput = document.createElement("input")
|
|
51
|
+
batchInput.type = "hidden"
|
|
52
|
+
batchInput.name = "batch_action"
|
|
53
|
+
batchInput.value = batchAction
|
|
54
|
+
form.appendChild(batchInput)
|
|
55
|
+
|
|
56
|
+
if (csrfToken) {
|
|
57
|
+
const csrfInput = document.createElement("input")
|
|
58
|
+
csrfInput.type = "hidden"
|
|
59
|
+
csrfInput.name = "authenticity_token"
|
|
60
|
+
csrfInput.value = csrfToken
|
|
61
|
+
form.appendChild(csrfInput)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
document.body.appendChild(form)
|
|
65
|
+
form.submit()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
checkedIds() {
|
|
69
|
+
return this.checkboxTargets
|
|
70
|
+
.filter(cb => cb.checked)
|
|
71
|
+
.map(cb => {
|
|
72
|
+
const match = cb.id.match(/\d+/)
|
|
73
|
+
return match ? match[0] : null
|
|
74
|
+
})
|
|
75
|
+
.filter(id => id !== null)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
showSelectAll() {
|
|
79
|
+
this.selectAllTargets.forEach(el => el.classList.remove("hidden"))
|
|
80
|
+
this.selectNoneTargets.forEach(el => el.classList.add("hidden"))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
showSelectNone() {
|
|
84
|
+
this.selectAllTargets.forEach(el => el.classList.add("hidden"))
|
|
85
|
+
this.selectNoneTargets.forEach(el => el.classList.remove("hidden"))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
updateActions() {
|
|
89
|
+
if (this.checkedIds().length > 0) {
|
|
90
|
+
this.actionLinkTargets.forEach(el => el.classList.remove("hidden"))
|
|
91
|
+
this.showSelectNone()
|
|
92
|
+
} else {
|
|
93
|
+
this.actionLinkTargets.forEach(el => el.classList.add("hidden"))
|
|
94
|
+
this.showSelectAll()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
updateState() {
|
|
99
|
+
this.updateActions()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = { type: String }
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
if (typeof $ === "undefined" || !$.fn.datetimepicker) return
|
|
8
|
+
|
|
9
|
+
const options = {}
|
|
10
|
+
if (this.typeValue === "date") {
|
|
11
|
+
options.pickTime = false
|
|
12
|
+
} else if (this.typeValue === "time") {
|
|
13
|
+
options.pickDate = false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
$(this.element).datetimepicker(options)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
disconnect() {
|
|
20
|
+
if (typeof $ === "undefined" || !$.fn.datetimepicker) return
|
|
21
|
+
const picker = $(this.element).data("DateTimePicker")
|
|
22
|
+
if (picker) picker.destroy()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
connect() {
|
|
5
|
+
this.setActiveLink()
|
|
6
|
+
this.removeEmptyDropdowns()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
setActiveLink() {
|
|
10
|
+
const pathWithSearch = window.location.pathname + window.location.search
|
|
11
|
+
let links = this.element.querySelectorAll(`.nav.navbar-nav a[href="${pathWithSearch}"]`)
|
|
12
|
+
|
|
13
|
+
if (links.length === 0) {
|
|
14
|
+
links = this.element.querySelectorAll(`.nav.navbar-nav a[href="${window.location.pathname}"]`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
links.forEach(link => {
|
|
18
|
+
link.closest("li")?.classList.add("active")
|
|
19
|
+
link.closest("li.dropdown")?.classList.add("active")
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
removeEmptyDropdowns() {
|
|
24
|
+
this.element.querySelectorAll(".navbar-nav .dropdown, .breadcrumb .dropdown").forEach(dropdown => {
|
|
25
|
+
if (dropdown.querySelectorAll("li").length === 0) {
|
|
26
|
+
dropdown.remove()
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div class="row">
|
|
2
|
+
<div class="col-md-6 col-md-offset-3">
|
|
3
|
+
<div class="jumbotron">
|
|
4
|
+
<h1>Welcome</h1>
|
|
5
|
+
<p>
|
|
6
|
+
You are running Goodmin <%= Goodmin::VERSION %>.
|
|
7
|
+
Add your own root route to the routes file in order to remove this page.
|
|
8
|
+
</p>
|
|
9
|
+
<p>
|
|
10
|
+
<a href="https://github.com/mbajur/goodmin/" class="btn btn-primary btn-lg" role="button">
|
|
11
|
+
<span class="glyphicon glyphicon-book"></span>
|
|
12
|
+
Browse the documentation
|
|
13
|
+
</a>
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<% if field.belongs_to? %>
|
|
2
|
+
<div class="form-group">
|
|
3
|
+
<%= f.label field.select_attribute %>
|
|
4
|
+
<%= f.select field.select_attribute, field.collection, {}, class: "form-control" %>
|
|
5
|
+
<%= content_tag(:p, field.hint, class: "help-block") if field.hint.present? %>
|
|
6
|
+
</div>
|
|
7
|
+
<% elsif field.many_to_many? || field.has_many? %>
|
|
8
|
+
<div class="form-group">
|
|
9
|
+
<%= f.label field.select_attribute, field.attribute.to_s.humanize %>
|
|
10
|
+
<%= f.select field.select_attribute, field.collection, {}, class: "form-control", multiple: true %>
|
|
11
|
+
<%= content_tag(:p, field.hint, class: "help-block") if field.hint.present? %>
|
|
12
|
+
</div>
|
|
13
|
+
<% else %>
|
|
14
|
+
<div class="form-group">
|
|
15
|
+
<%= f.label field.attribute %>
|
|
16
|
+
<%= f.text_field field.attribute, class: "form-control" %>
|
|
17
|
+
<%= content_tag(:p, field.hint, class: "help-block") if field.hint.present? %>
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|