ez-resources 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,2 @@
1
+ = div_for "form-field-#{model.type}-type"
2
+ = options[:form].input model.name, html_options
@@ -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