ez-resources 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +106 -0
- data/Rakefile +34 -0
- data/app/cells/ez/resources/application_cell.rb +40 -0
- data/app/cells/ez/resources/collection/table.slim +42 -0
- data/app/cells/ez/resources/collection_cell.rb +115 -0
- data/app/cells/ez/resources/field/show.slim +2 -0
- data/app/cells/ez/resources/field_cell.rb +41 -0
- data/app/cells/ez/resources/form/show.slim +16 -0
- data/app/cells/ez/resources/form_cell.rb +33 -0
- data/app/cells/ez/resources/search/show.slim +20 -0
- data/app/cells/ez/resources/search_cell.rb +43 -0
- data/lib/ez/resources.rb +21 -0
- data/lib/ez/resources/engine.rb +14 -0
- data/lib/ez/resources/manager.rb +114 -0
- data/lib/ez/resources/manager/action.rb +17 -0
- data/lib/ez/resources/manager/config.rb +150 -0
- data/lib/ez/resources/manager/config_store.rb +48 -0
- data/lib/ez/resources/manager/dsl.rb +22 -0
- data/lib/ez/resources/manager/field.rb +46 -0
- data/lib/ez/resources/manager/fields.rb +31 -0
- data/lib/ez/resources/manager/hook.rb +16 -0
- data/lib/ez/resources/manager/hooks.rb +38 -0
- data/lib/ez/resources/railtie.rb +8 -0
- data/lib/ez/resources/version.rb +7 -0
- data/lib/tasks/ez/resources_tasks.rake +5 -0
- metadata +327 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dadd125c3e79374bebeec76245b56d87f56060706dc2bf7622b546d3096ed319
|
4
|
+
data.tar.gz: 6beaea0d553ce0ae5ce008006ad0c7b435eb651a38709a69f976c87eb6e68e65
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f67e74bba229bc0a2c7bce3be22f4ee8dfd3ef6e922aab1f1f69e9e6d49f2bafbd356a91773a8e9794c71d0980f6c65ca5f9cfb96f030ea1d67b0dcdd6ad1d65
|
7
|
+
data.tar.gz: 6dfc56195eb5877e94bd3b6d197679e7fb48ed5ae368d85eb1601de7be6292fdefadfba9d733f96bb3e348faa036105c39dac5f606742565bea83a282aeb798a
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Volodya Sveredyuk
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# Ez::Resources
|
2
|
+
CRUD is boring. As a rails developer, you are familiar with typical tasks as a list of records, create new records, update records and delete it if need it. Next will be pagination, maybe search and filtering. Every new project the same shit. Boring. Boring. Boring.
|
3
|
+
|
4
|
+
I want to manage resources within fancy DSL that will do everything for me. Why? Because I can!
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
### Router
|
9
|
+
|
10
|
+
Use your rails router as it was used before. No hidden magic here
|
11
|
+
```ruby
|
12
|
+
# config/routes.rb
|
13
|
+
|
14
|
+
resources :users
|
15
|
+
|
16
|
+
```
|
17
|
+
|
18
|
+
### Controller
|
19
|
+
|
20
|
+
Are you familiar with rails controllers? Of course, you do. Use rails controllers as you already know but with the possibility to automate default workflows such as a collection, create, update or delete
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
class UsersController < ApplicationController
|
24
|
+
# Include resource manager module
|
25
|
+
include Ez::Resources::Manager
|
26
|
+
|
27
|
+
# Now you have fancy DSL to manage your common workflows
|
28
|
+
ez_resource do |config|
|
29
|
+
|
30
|
+
# Set model class or it will try to guess it from controller name
|
31
|
+
# config.model = User
|
32
|
+
|
33
|
+
# Collection query or it will try to perfrom .all
|
34
|
+
# config.collection_query = -> (search_relation, ctx) { search_relation.where(user_id: ctx.params[:user_id]) }
|
35
|
+
|
36
|
+
# Single resource title presentation
|
37
|
+
# config.resource_label = :title
|
38
|
+
|
39
|
+
# Allow list of actions or default index, show, new, create, edit, update and destroy
|
40
|
+
# config.actions = %[index new update]
|
41
|
+
|
42
|
+
# Define default collection items
|
43
|
+
config.collection_columns do
|
44
|
+
column :email
|
45
|
+
column :active, type: :boolean
|
46
|
+
column :name, type: :link, presenter: -> (user) { user.name.humanize }
|
47
|
+
column :age
|
48
|
+
column :avatar, type: :image, getter: ->(user) { "/avatars/#{user.id}.jpg" }, class: "t-image-tag"
|
49
|
+
column :custom, type: :custom, builder: ->(user) { "custom #{user.email}" }
|
50
|
+
column :gender, type: :association, getter: ->(user) { user.gender.upcase }
|
51
|
+
column :title, type: :association, getter: ->(user) { user.posts.pluck(:title) }, association: :posts, title: 'Post title'
|
52
|
+
end
|
53
|
+
|
54
|
+
# Add custom collection templates
|
55
|
+
# config.collection_views = [:table, :gallery]
|
56
|
+
|
57
|
+
# Form fields
|
58
|
+
config.form_fields do
|
59
|
+
field :email
|
60
|
+
field :name
|
61
|
+
field :active, type: :boolean, default: -> { true }
|
62
|
+
field :age, type: :integer, default: -> { 18 }, required: false
|
63
|
+
field :gender, type: :select, default: -> { 'Other' }, collection: %w(Male Female Other)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Custom collection actions
|
67
|
+
config.collection_actions do
|
68
|
+
action :clone, proc { |_ctx, user| "/users/#{user.id}/clone" }, method: :post, class: 'custom-action-class'
|
69
|
+
end
|
70
|
+
|
71
|
+
# Hooks for authentication or any what you need
|
72
|
+
config.hooks do
|
73
|
+
add :can_update?, proc { |ctx, user| user.age >= 18 }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
### Enjoy
|
80
|
+
That's all. More documentation coming soon ;)
|
81
|
+
|
82
|
+
## Installation
|
83
|
+
Add this line to your application's Gemfile:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
gem 'ez-resources'
|
87
|
+
```
|
88
|
+
|
89
|
+
And then execute:
|
90
|
+
```bash
|
91
|
+
$ bundle
|
92
|
+
```
|
93
|
+
|
94
|
+
Or install it yourself as:
|
95
|
+
```bash
|
96
|
+
$ gem install ez-resources
|
97
|
+
```
|
98
|
+
|
99
|
+
## TODO
|
100
|
+
- [ ] Add generators for configuration and I18n
|
101
|
+
|
102
|
+
## Contributing
|
103
|
+
Contribution directions go here.
|
104
|
+
|
105
|
+
## License
|
106
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rdoc/task'
|
10
|
+
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
12
|
+
rdoc.rdoc_dir = 'rdoc'
|
13
|
+
rdoc.title = 'Ez::Resources'
|
14
|
+
rdoc.options << '--line-numbers'
|
15
|
+
rdoc.rdoc_files.include('README.md')
|
16
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
17
|
+
end
|
18
|
+
|
19
|
+
APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
|
20
|
+
load 'rails/tasks/engine.rake'
|
21
|
+
|
22
|
+
load 'rails/tasks/statistics.rake'
|
23
|
+
|
24
|
+
require 'bundler/gem_tasks'
|
25
|
+
|
26
|
+
require 'rake/testtask'
|
27
|
+
|
28
|
+
Rake::TestTask.new(:test) do |t|
|
29
|
+
t.libs << 'spec'
|
30
|
+
t.pattern = 'spec/**/*_test.rb'
|
31
|
+
t.verbose = false
|
32
|
+
end
|
33
|
+
|
34
|
+
task default: :test
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ez
|
4
|
+
module Resources
|
5
|
+
class ApplicationCell < Cell::ViewModel
|
6
|
+
self.view_paths = ["#{Ez::Resources::Engine.root}/app/cells", "#{::Rails.root}/app/cells/custom"]
|
7
|
+
|
8
|
+
CSS_SCOPE = 'ez-resources'
|
9
|
+
|
10
|
+
def self.form
|
11
|
+
include ActionView::Helpers::FormHelper
|
12
|
+
include ActionView::Helpers::DateHelper
|
13
|
+
include ActionView::Helpers::AssetTagHelper
|
14
|
+
include SimpleForm::ActionViewExtensions::FormHelper
|
15
|
+
include ActionView::RecordIdentifier
|
16
|
+
include ActionView::Helpers::FormOptionsHelper
|
17
|
+
end
|
18
|
+
|
19
|
+
def div_for(item, extra = nil, &block)
|
20
|
+
content_tag :div, class: css_for(item, extra), &block
|
21
|
+
end
|
22
|
+
|
23
|
+
def css_for(item, extra = nil)
|
24
|
+
scoped_item = "#{CSS_SCOPE}-#{item}"
|
25
|
+
|
26
|
+
css_class = custom_css_map[scoped_item] || scoped_item
|
27
|
+
|
28
|
+
extra ? "#{css_class} #{extra}" : css_class
|
29
|
+
end
|
30
|
+
|
31
|
+
def custom_css_map
|
32
|
+
@custom_css_map ||= Ez::Resources.config.ui_custom_css_map || {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def t(args)
|
36
|
+
I18n.t(args, scope: Ez::Resources.config.i18n_scope)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
= div_for 'container'
|
2
|
+
= div_for 'collection-container'
|
3
|
+
= div_for 'collection-inner-container'
|
4
|
+
= div_for 'collection-top-container'
|
5
|
+
= div_for 'collection-header-container'
|
6
|
+
h2 class=(css_for('header-label')) = header_text
|
7
|
+
|
8
|
+
= div_for 'collection-actions-container'
|
9
|
+
= div_for 'actions-inner-container'
|
10
|
+
- if model.collection_views.present?
|
11
|
+
- model.collection_views.each do |view_name|
|
12
|
+
= view_switch_link(view_name)
|
13
|
+
= new_link
|
14
|
+
|
15
|
+
= div_for 'collection-table-container'
|
16
|
+
table class=(css_for 'collection-table')
|
17
|
+
thead class=(css_for 'collection-table-thead')
|
18
|
+
tr class=(css_for 'collection-table-thead-tr')
|
19
|
+
- collection_columns.each do |column|
|
20
|
+
th class=(css_for 'collection-table-thead-th') id=("ez-t-#{column.name}") = column.title
|
21
|
+
th class=(css_for 'collection-table-thead-th') colspan="2" style="width: 1%"
|
22
|
+
tbody class=(css_for 'collection-table-tbody')
|
23
|
+
- collection.each do |record|
|
24
|
+
= record_tr record do
|
25
|
+
- collection_columns.each do |column|
|
26
|
+
td class=(css_for 'collection-table-td', "ez-t-#{column.name}")
|
27
|
+
= record_column_value(record, column)
|
28
|
+
td class=(css_for 'collection-table-td-actions')
|
29
|
+
= div_for 'collection-table-td-actions-container' do
|
30
|
+
i class=(css_for('collection-table-actions-icon'))
|
31
|
+
= div_for 'collection-table-td-actions-content' do
|
32
|
+
= show_link(record)
|
33
|
+
= edit_link(record)
|
34
|
+
= remove_link(record)
|
35
|
+
- model.collection_actions.each do |custom_action|
|
36
|
+
= link_to t("actions.#{custom_action.name}"), custom_action.builder.call(controller, record), custom_action.options.merge(class: css_for('collection-table-td-action-item'))
|
37
|
+
|
38
|
+
- if paginator && paginator.count > 20
|
39
|
+
== pagination
|
40
|
+
|
41
|
+
- if model.collection_search?
|
42
|
+
= cell 'ez/resources/search', model
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ez
|
4
|
+
module Resources
|
5
|
+
class CollectionCell < ApplicationCell
|
6
|
+
include Pagy::Frontend
|
7
|
+
|
8
|
+
delegate :resources_name, :collection_columns, :paginator, to: :model
|
9
|
+
|
10
|
+
def show
|
11
|
+
if model.params[:view]
|
12
|
+
render params[:view]
|
13
|
+
else
|
14
|
+
render :table
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def collection
|
19
|
+
@collection ||= model.data
|
20
|
+
end
|
21
|
+
|
22
|
+
def header_text
|
23
|
+
resources_name
|
24
|
+
end
|
25
|
+
|
26
|
+
def record_column_value(record, column)
|
27
|
+
result = if column.type == :association
|
28
|
+
column.getter.call(record)
|
29
|
+
elsif column.type == :boolean
|
30
|
+
maybe_use_custom_boolean_presenter(record.public_send(column.name))
|
31
|
+
elsif column.type == :custom
|
32
|
+
column.builder.call(record)
|
33
|
+
elsif column.type == :image
|
34
|
+
url = column.getter.call(record)
|
35
|
+
|
36
|
+
image_tag url, column.options if url
|
37
|
+
elsif column.type == :link
|
38
|
+
as_a_link(record, column)
|
39
|
+
else
|
40
|
+
record.public_send(column.name)
|
41
|
+
end
|
42
|
+
|
43
|
+
result = column.presenter.call(record) if column.presenter
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
def record_tr(record, &block)
|
48
|
+
if model.actions.include?(:show) && Manager::Hooks.can?(:can_read?, model, record)
|
49
|
+
content_tag :tr, class: css_for('collection-table-tr'), id: "#{resources_name.downcase}-#{record.id}", data: { link: model.path_for(action: :show, id: record.id).to_s }, &block
|
50
|
+
else
|
51
|
+
content_tag :tr, class: css_for('collection-table-tr'), id: "#{resources_name.downcase}-#{record.id}", &block
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def new_link
|
56
|
+
return unless model.actions.include?(:new)
|
57
|
+
return unless Manager::Hooks.can?(:can_create?, model)
|
58
|
+
|
59
|
+
link_to t('actions.add'), model.path_for(action: :new), class: css_for('actions-new-link')
|
60
|
+
end
|
61
|
+
|
62
|
+
def show_link(record)
|
63
|
+
return unless model.actions.include?(:show)
|
64
|
+
return unless Manager::Hooks.can?(:can_read?, model, record)
|
65
|
+
|
66
|
+
link_to t('actions.show'), model.path_for(action: :show, id: record.id), class: css_for('collection-table-td-action-item')
|
67
|
+
end
|
68
|
+
|
69
|
+
def as_a_link(record, column)
|
70
|
+
return unless model.actions.include?(:show)
|
71
|
+
return unless Manager::Hooks.can?(:can_read?, model, record)
|
72
|
+
|
73
|
+
link_to record.public_send(column.name), model.path_for(action: :show, id: record.id)
|
74
|
+
end
|
75
|
+
|
76
|
+
def edit_link(record)
|
77
|
+
return unless model.actions.include?(:edit)
|
78
|
+
return unless Manager::Hooks.can?(:can_read?, model, record)
|
79
|
+
|
80
|
+
link_to t('actions.edit'), model.path_for(action: :edit, id: record.id), class: css_for('collection-table-td-action-item')
|
81
|
+
end
|
82
|
+
|
83
|
+
def remove_link(record)
|
84
|
+
# TODO
|
85
|
+
end
|
86
|
+
|
87
|
+
def pagination
|
88
|
+
if Ez::Resources.config.pagination_method
|
89
|
+
instance_exec paginator, &Ez::Resources.config.pagination_method
|
90
|
+
else
|
91
|
+
pagy_nav(paginator)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def view_switch_link(view_name)
|
96
|
+
return unless model.collection_views.present?
|
97
|
+
|
98
|
+
params = model.params.to_unsafe_hash.slice(:q, :page, :s).symbolize_keys.merge(view: view_name)
|
99
|
+
selected = css_for('collection-view-selected-link') if model.params[:view] == view_name.to_s
|
100
|
+
|
101
|
+
link_to model.path_for(action: :index, params: params), id: "ez-view-#{view_name}", class: css_for("collection-view-link-#{view_name}", selected) do
|
102
|
+
content_tag :i, nil, class: css_for("collection-view-link-#{view_name}-icon")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def maybe_use_custom_boolean_presenter(bool)
|
109
|
+
return bool unless Ez::Resources.config.ui_custom_boolean_presenter
|
110
|
+
|
111
|
+
instance_exec bool, &Ez::Resources.config.ui_custom_boolean_presenter
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ez
|
4
|
+
module Resources
|
5
|
+
class FieldCell < ApplicationCell
|
6
|
+
def html_options
|
7
|
+
if options[:new_record?]
|
8
|
+
base_options.merge(
|
9
|
+
selected: model.default&.call,
|
10
|
+
input_html: {
|
11
|
+
value: model.default&.call,
|
12
|
+
checked: model.default&.call
|
13
|
+
}
|
14
|
+
)
|
15
|
+
else
|
16
|
+
base_options
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def base_options
|
21
|
+
{
|
22
|
+
label: model.title,
|
23
|
+
as: model.type,
|
24
|
+
collection: model.collection,
|
25
|
+
include_blank: model.required?,
|
26
|
+
required: model.required?,
|
27
|
+
readonly: options[:readonly],
|
28
|
+
checked_value: true.to_s,
|
29
|
+
unchecked_value: false.to_s,
|
30
|
+
wrapper: model.wrapper,
|
31
|
+
right_label: model.suffix,
|
32
|
+
input_html: {
|
33
|
+
min: model.min
|
34
|
+
}
|
35
|
+
}.merge(model.options)
|
36
|
+
end
|
37
|
+
|
38
|
+
# TODO: add default value in case of new record
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
= div_for 'container'
|
2
|
+
= div_for 'form-container'
|
3
|
+
= div_for 'form-header-container'
|
4
|
+
= div_for 'header-inner-container'
|
5
|
+
h2 class=(css_for('header-label')) = form_header_label
|
6
|
+
|
7
|
+
= div_for 'form-inner-container'
|
8
|
+
= simple_form_for model.data, as: :ez_resource, html: { readonly: readonly? } do |f|
|
9
|
+
- model.form_fields.each do |field|
|
10
|
+
= cell 'ez/resources/field', field, form: f, new_record?: model.data.new_record?, readonly: readonly?
|
11
|
+
|
12
|
+
= div_for 'form-actions'
|
13
|
+
- unless readonly?
|
14
|
+
= f.submit submit_button_text, class: css_for('actions-submit-button')
|
15
|
+
|
16
|
+
= link_to t('actions.cancel'), model.path_for(action: :index)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ez
|
4
|
+
module Resources
|
5
|
+
class FormCell < ApplicationCell
|
6
|
+
form
|
7
|
+
|
8
|
+
def form_header_label
|
9
|
+
if model.data.new_record?
|
10
|
+
"#{t('actions.new')} #{model.resource_name.downcase}"
|
11
|
+
else
|
12
|
+
"#{t('actions.edit')} #{model.data.public_send(model.resource_label)} #{model.resource_name.downcase}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def readonly?
|
17
|
+
@readonly ||= if model.data.new_record?
|
18
|
+
!Ez::Resources::Manager::Hooks.can?(:can_create?, model, model.data)
|
19
|
+
else
|
20
|
+
!Ez::Resources::Manager::Hooks.can?(:can_update?, model, model.data)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def submit_button_text
|
25
|
+
if model.data.new_record?
|
26
|
+
t('actions.create')
|
27
|
+
else
|
28
|
+
t('actions.update')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|