custom_table 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +210 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/config/custom_table_manifest.js +1 -0
  5. data/app/assets/stylesheets/custom_table/application.scss +1 -0
  6. data/app/assets/stylesheets/custom_table/table.scss +89 -0
  7. data/app/controllers/concerns/custom_table_concern.rb +118 -0
  8. data/app/controllers/custom_table/application_controller.rb +4 -0
  9. data/app/controllers/custom_table/settings_controller.rb +86 -0
  10. data/app/helpers/custom_table/application_helper.rb +483 -0
  11. data/app/helpers/custom_table/fieldset_helper.rb +90 -0
  12. data/app/helpers/custom_table/icons_helper.rb +42 -0
  13. data/app/inputs/date_picker_input.rb +52 -0
  14. data/app/javascript/controllers/batch_actions_controller.js +53 -0
  15. data/app/javascript/controllers/table_controller.js +109 -0
  16. data/app/jobs/custom_table/application_job.rb +4 -0
  17. data/app/mailers/custom_table/application_mailer.rb +6 -0
  18. data/app/models/concerns/custom_table_settings.rb +42 -0
  19. data/app/models/custom_table/application_record.rb +5 -0
  20. data/app/views/custom_table/_download.haml +19 -0
  21. data/app/views/custom_table/_field.haml +8 -0
  22. data/app/views/custom_table/_field_plain.haml +1 -0
  23. data/app/views/custom_table/_fieldset.haml +2 -0
  24. data/app/views/custom_table/_filter.html.haml +104 -0
  25. data/app/views/custom_table/_settings.html.haml +57 -0
  26. data/app/views/custom_table/_table.html.haml +261 -0
  27. data/app/views/custom_table/_table.xlsx.axlsx +76 -0
  28. data/app/views/custom_table/_table_fe.xlsx.fast_excel +141 -0
  29. data/app/views/custom_table/_table_row.html.haml +72 -0
  30. data/app/views/custom_table/_table_row_data.html.haml +26 -0
  31. data/app/views/custom_table/settings/destroy.html.haml +4 -0
  32. data/app/views/custom_table/settings/edit.html.haml +2 -0
  33. data/app/views/custom_table/settings/update.html.haml +4 -0
  34. data/app/views/layouts/custom_table/application.html.erb +15 -0
  35. data/config/initializers/simple_form_bootstrap.rb +468 -0
  36. data/config/locales/en.yml +18 -0
  37. data/config/locales/ru.yml +18 -0
  38. data/config/routes.rb +5 -0
  39. data/lib/custom_table/configuration.rb +10 -0
  40. data/lib/custom_table/engine.rb +12 -0
  41. data/lib/custom_table/version.rb +3 -0
  42. data/lib/custom_table.rb +14 -0
  43. data/lib/generators/custom_table/USAGE +8 -0
  44. data/lib/generators/custom_table/custom_table_generator.rb +16 -0
  45. data/lib/generators/custom_table/templates/initializer.rb +3 -0
  46. data/lib/generators/custom_table/templates/migration.rb +5 -0
  47. data/lib/tasks/custom_table_tasks.rake +4 -0
  48. metadata +300 -0
@@ -0,0 +1,53 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+
5
+ static targets = [ "checkbox", "form" ]
6
+
7
+ connect() {
8
+ if (this.hasFormTarget && this.hasCheckboxTarget) {
9
+ console.log("Batch actions form", this.formTarget)
10
+ this.refresh();
11
+ }
12
+ }
13
+
14
+ submit(event) {
15
+
16
+ if (!this.hasCheckboxTarget) {
17
+ console.log("No checkbox targets at all")
18
+ return
19
+ }
20
+
21
+ // Clearing any matching hidden fields from form
22
+ this.formTarget.querySelectorAll('input[name="'+this.checkboxTargets[0].getAttribute("name")+'"]').forEach((cb) => {
23
+ cb.parentNode.removeChild(cb)
24
+ });
25
+
26
+ // Adding selected fields to form as hiddens
27
+ this.checkboxTargets.forEach((cb) => {
28
+
29
+ if (!cb.checked) return;
30
+
31
+ let input = document.createElement('input');
32
+ input.setAttribute('name', cb.getAttribute("name"));
33
+ input.setAttribute('value', cb.getAttribute("value"));
34
+ input.setAttribute('type', "hidden")
35
+
36
+ this.formTarget.appendChild(input);//append the input to the form
37
+
38
+ })
39
+
40
+ // event.preventDefault()
41
+ console.log("Batch actions submit form")
42
+
43
+ }
44
+
45
+ refresh(){
46
+ let v = false
47
+ this.checkboxTargets.forEach((cb) => {
48
+ if (cb.checked) v = true;
49
+ });
50
+ this.formTarget.querySelector('input[type="submit"]').disabled = !v
51
+ }
52
+
53
+ }
@@ -0,0 +1,109 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+
5
+ static targets = [ "move", "hide", "sum", "sumval" ]
6
+
7
+ connect() {
8
+ if (this.hasMoveTarget) this.moveup();
9
+ if (this.hasHideTarget) this.hideEmpty();
10
+
11
+ if (!this.hasSumTarget) {
12
+ let d = document.createElement("caption")
13
+ d.classList.add("d-none", "table-sum")
14
+ d.dataset.tableTarget = "sum"
15
+ d.innerHTML = '<i class="fa fa-times-circle mt-1 float-start" data-action="click->table#clearSum"></i><span data-table-target="sumval"></span>'
16
+ this.element.appendChild(d)
17
+ }
18
+
19
+ this.element.querySelector("tbody").addEventListener('click', (e) => {
20
+ const cell = e.target.closest('td.amount');
21
+ if (!cell) {return;} // Quit, not clicked on a cell
22
+ if (!isNaN(this.cellValue(cell))) {
23
+ const row = cell.parentElement;
24
+ cell.classList.toggle("selected")
25
+ this.showSum();
26
+ }
27
+ });
28
+
29
+ }
30
+
31
+ moveup() {
32
+ console.log("Moving row")
33
+ let elm = this.moveTarget
34
+ let tbl = elm.parentElement
35
+ this.moveTarget.remove()
36
+ tbl.prepend(elm)
37
+ }
38
+
39
+ showSum(){
40
+ let elements = this.element.querySelectorAll("td.selected");
41
+ if (elements.length == 0) {
42
+ this.sumTarget.classList.add("d-none")
43
+ return
44
+ }
45
+ this.sumTarget.classList.remove("d-none")
46
+ let sum = 0
47
+ elements.forEach((e) => {
48
+ sum += this.cellValue(e)
49
+ })
50
+ if (this.hasSumTarget) {
51
+ this.sumvalTarget.innerHTML = "SUM="+Math.round(sum * 100) / 100
52
+ }
53
+ }
54
+
55
+ cellValue(e){
56
+ if (e.querySelector("span[data-raw]")) {
57
+ return Number(e.querySelector("span[data-raw]").dataset.raw);
58
+ }
59
+ else {
60
+ return Number(e.innerText.replace(/\s,/g, ''));
61
+ }
62
+ }
63
+
64
+ clearSum(){
65
+ this.element.querySelectorAll("td.selected").forEach((e) => {
66
+ e.classList.remove("selected")
67
+ })
68
+ this.showSum();
69
+ }
70
+
71
+ hideEmpty(){
72
+
73
+ console.log("Hiding empty!!!")
74
+
75
+ this.element.querySelectorAll("tr th").forEach((e, i) => {
76
+
77
+ if (!e.classList.contains("hide-empty")) return;
78
+
79
+ let tds = this.element.querySelectorAll("tr td:nth-child(".concat(i+1, ")"));
80
+ if (Array.prototype.slice.call(tds).every(td => { return (td.parentElement.classList.contains("totals") || td.textContent == "0" || !td.textContent || Number(td.textContent) == 0); })) {
81
+ e.hidden = true;
82
+ tds.forEach(e => {e.hidden = true})
83
+ }
84
+
85
+ });
86
+
87
+ }
88
+
89
+ toggle(event) {
90
+ let params = event.params
91
+ console.log("Table Toggle", event.currentTarget.innerHTML)
92
+ this.element.querySelectorAll(params.css).forEach((e, i) => {
93
+ e.classList.toggle("d-none")
94
+ });
95
+ event.currentTarget.classList.toggle("opened")
96
+ }
97
+
98
+ search(event) {
99
+ console.log("Table Search", event.currentTarget.value)
100
+ this.element.querySelectorAll("tbody tr").forEach((tr) => {
101
+ if (tr.innerText.toLowerCase().includes(event.currentTarget.value.toLowerCase())) {
102
+ tr.classList.remove("d-none")
103
+ } else {
104
+ tr.classList.add("d-none")
105
+ }
106
+ })
107
+ }
108
+
109
+ }
@@ -0,0 +1,4 @@
1
+ module CustomTable
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module CustomTable
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,42 @@
1
+ module CustomTableSettings
2
+
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ serialize :custom_table
7
+ end
8
+
9
+ def save_custom_table_settings model_class, variant = nil, fields: nil, sorts: nil, per_page: nil
10
+
11
+ model = model_class.model_name.to_s
12
+ key = model
13
+ key = "#{model}-#{variant}" if !variant.nil?
14
+ self.custom_table = {} if self.custom_table.nil?
15
+ self.custom_table[key] = {} if self.custom_table[key].nil?
16
+ self.custom_table[key][:model] = model
17
+ self.custom_table[key][:fields] = fields.symbolize_keys if !fields.nil?
18
+ self.custom_table[key][:sorts] = sorts if !sorts.nil?
19
+ self.custom_table[key][:per_page] = per_page.to_i if !per_page.nil? && [25, 50, 100].include?(per_page.to_i)
20
+
21
+ return save!
22
+ # write_attribute :custom_table, (custom_table||{}).merge(ss)
23
+ end
24
+
25
+
26
+ def destroy_custom_table_settings model_class, variant = nil
27
+
28
+ return true if self.custom_table.nil?
29
+
30
+ model = model_class.model_name.to_s
31
+ key = model
32
+ key = "#{model}-#{variant}" if !variant.nil?
33
+
34
+ return true if self.custom_table[key].nil?
35
+
36
+ self.custom_table.delete(key)
37
+
38
+ return save
39
+ # write_attribute :custom_table, (custom_table||{}).merge(ss)
40
+ end
41
+
42
+ end
@@ -0,0 +1,5 @@
1
+ module CustomTable
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ - model_class = local_assigns[:collection].nil? ? local_assigns[:class] : local_assigns[:collection].model
2
+ - local_assigns[:with_selected_fields] = true if local_assigns[:with_selected_fields].nil?
3
+ - if (model_class.present? && can?(:download, model_class))
4
+ .btn-group
5
+ = link_to params.permit!.merge({:format => :xlsx, all_fields: true}), :class => "btn btn-outline-primary" do
6
+ = t("download")
7
+ = custom_table_download_icon
8
+
9
+ - if (local_assigns[:downloads] && downloads.length > 0) || (local_assigns[:with_selected_fields] == true)
10
+
11
+ %button.btn.btn-outline-primary.dropdown-toggle{type: "button", data: {"bs-toggle": "dropdown"}}
12
+ %ul.dropdown-menu
13
+ - if local_assigns[:with_selected_fields] == true
14
+ %li
15
+ = link_to params.permit!.merge({:format => :xlsx}), :class => "dropdown-item" do
16
+ = t("custom_table.download_only_selected_fields")
17
+ - downloads.each do |d|
18
+ %li
19
+ = link_to d[:title], d[:href], class: "dropdown-item"
@@ -0,0 +1,8 @@
1
+ %div.d-flex.justify-content-between.custom-table-field
2
+ %b
3
+ - if local_assigns[:hint]
4
+ %abbr{title: hint}= label
5
+ - else
6
+ = label
7
+ %span.text-end.d-inline{id: "#{column}_#{object.model_name.singular}_#{object.id}"}= yield
8
+ -# %dt{class: adaptive ? "col-lg-2 col-md-5" : "col-5"}= label
@@ -0,0 +1 @@
1
+ %span{id: "#{column}_#{object.model_name.singular}_#{object.id}"}= yield
@@ -0,0 +1,2 @@
1
+ %div
2
+ = yield
@@ -0,0 +1,104 @@
1
+ .card
2
+
3
+ .card-body.bg-light
4
+
5
+ - url = local_assigns[:url].presence
6
+ - variant = local_assigns[:variant].presence
7
+
8
+ - a = {}
9
+
10
+ = custom_table_form_for @q, url: url do |f|
11
+
12
+ .d-none
13
+ = f.input :s, as: :hidden, input_html: {value: params[:q].try(:[], :s)}
14
+
15
+ - fields = custom_table_fields_for(search_model, variant: variant, current_search: params[:q], predefined_fields: local_assigns[:fields]) if local_assigns[:fields].nil?
16
+
17
+ - fields.each do |key, defs|
18
+
19
+ - next if defs[:search].nil?
20
+
21
+ - a[key] = []
22
+
23
+ - defs[:search] = [defs[:search]] if !defs[:search].kind_of?(Array)
24
+ - defs[:label] = defs[:search][0][:label] if defs[:label].nil?
25
+ - defs[:label] = search_model.human_attribute_name(key) if defs[:label].nil?
26
+
27
+ - defs[:search].each do |field|
28
+
29
+ - a[key].push field[:q]
30
+ - label = field[:label] || defs[:label]
31
+
32
+ - if field[:type] == :text
33
+
34
+ - ih = {class: "form-control-sm"}
35
+ - ih = ih.merge(defs[:input_html]) if !defs[:input_html].nil?
36
+
37
+ = f.input field[:q], input_html: ih, required: false, label: false, placeholder: label
38
+
39
+ - if field[:type] == :boolean
40
+
41
+ = f.input field[:q], as: :boolean, input_html: {}, required: false, label: label, unchecked_value: ""
42
+
43
+ - if field[:type] == :switch
44
+
45
+ = f.input field[:q], as: :select, input_html: {:class => "form-select form-select-sm"}, required: false, label: false, :collection => [[t("yes")+" | #{label}", "true"], [t("no")+" | #{label}", "false"]], include_blank: label
46
+
47
+ - if field[:type] == :select
48
+
49
+ - collection = field[:collection]
50
+ - label_method = nil
51
+
52
+ - if collection.class.to_s =~ /ActiveRecord_Relation/i
53
+ - collection = collection.accessible_by(current_ability)
54
+ - label_method = :to_s
55
+
56
+ = f.input field[:q], :as => :select, :input_html => {:class => "form-select form-select-sm"}, :required => false, :label => false, :collection => collection, label_method: label_method, include_blank: label, value_method: field[:value_method]
57
+
58
+ - if field[:type] == :grouped_select
59
+
60
+ - collection = field[:collection]
61
+
62
+ - collection = collection.accessible_by(current_ability) if collection.class.to_s =~ /ActiveRecord_Relation/i
63
+
64
+ = f.input field[:q], :as => :grouped_select, :input_html => {:class => "form-select form-select-sm"}, :required => false, :label => false, group_method: field[:group_method], :collection => collection, include_blank: label
65
+
66
+ - if field[:type] == :autocomplete
67
+
68
+ - collection = field[:collection]
69
+
70
+ - collection = collection.accessible_by(current_ability) if collection.class.to_s =~ /ActiveRecord_Relation/i
71
+
72
+ = f.input field[:q], :as => :autocomplete, :input_html => {:class => "input-sm"}, :required => false, :label => false, :collection => collection, prompt: label
73
+
74
+ - if field[:type] == :enum
75
+
76
+ - collection = field[:collection]
77
+ - collection = [search_model, key.to_s.pluralize] if collection.nil?
78
+ - coll = collection[0].send(collection[1]).keys.map { |w| [(collection[0].method_defined?(:human_enum_name) ? collection[0].human_enum_name(collection[1].to_s.singularize, w) : w), w] }
79
+
80
+ = f.input field[:q], :as => :select, :input_html => {:class => "form-select form-select-sm"}, :required => false, :label => false, :collection => coll, include_blank: label
81
+
82
+ - if field[:type] == :dates
83
+ .col-12
84
+ .input-group.input-group-sm{"data-controller": "dates"}
85
+ = f.input_field field[:q][0], :as => :date_picker, :label => false, :class => "form-control", data: {"dates-target": "dateFrom"}, :placeholder => label+" ("+t("custom_table.date_from")+")"
86
+ = f.input_field field[:q][1], :as => :date_picker, :label => false, :class => "form-control", data: {"dates-target": "dateTo"}, :placeholder => label+" ("+t("custom_table.date_to")+")"
87
+ %button.btn.btn-outline-secondary.dropdown-toggle(type="button" data-bs-toggle="dropdown" aria-expanded="false")
88
+ %ul.dropdown-menu.dropdown-menu-end
89
+ %button.dropdown-item{type: "button", "data-action": "dates#prevWeek"}= t("analytics.previos_week")
90
+ %button.dropdown-item{type: "button", "data-action": "dates#prevMonth"}= t("analytics.previos_month")
91
+ %button.dropdown-item{type: "button", "data-action": "dates#prevYear"}= t("analytics.previos_year")
92
+ %hr.dropdown-divider
93
+ %button.dropdown-item{type: "button", "data-action": "dates#clear"}= t("clear")
94
+
95
+ .btn-group
96
+ = f.button :submit, t("search"), name: "", :class => "btn-sm btn-primary"
97
+ - if user_signed_in? && current_user_has_customizable_fields_for?(search_model, variant) && !local_assigns[:hide_customization] && !local_assigns[:fields]# && !(profile_path rescue nil).nil?
98
+ = custom_table_settings_button search_model, variant
99
+ - if params[:q].present? && !params[:q].except(:s).empty?
100
+ = link_to url_for(params.permit!.except(:q)), class: "btn btn-sm btn-outline-danger", title: t("custom_table.clear_search") do
101
+ = custom_table_cancel_icon
102
+
103
+ = yield f
104
+
@@ -0,0 +1,57 @@
1
+ - variant = nil if !defined?("variant")
2
+ - field_key = search_model.model_name.to_s
3
+ - field_key += "-#{variant}" if !variant.nil?
4
+ - cls = local_assigns[:large].nil? ? "btn-sm" : ""
5
+ - items = custom_table_fields_settings_for(search_model, variant: variant)
6
+
7
+ - columns = 1; size = "modal-md"
8
+ - (columns = 2; size = "modal-lg") if items.length > 10
9
+ - (columns = 3; size = "modal-xl") if items.length > 30
10
+
11
+ .modal.fade.show{id: "customize-items-#{field_key}", tabindex: "-1", role: "dialog", "aria-labelledby": "loginLabel", "aria-hidden": "true", data: {controller: "remote-modal",
12
+ action: "turbo:before-render@document->remote-modal#hideBeforeRender"}}
13
+ .modal-dialog{class: size}
14
+ .modal-content
15
+ .modal-header
16
+ %h4#loginLabel.modal-title
17
+ = t("custom_table.customize_table")+":"
18
+ = search_model.model_name.human
19
+ - if !variant.nil?
20
+ %small= "(#{variant})"
21
+
22
+ %button.btn-close(type="button" data-bs-dismiss="modal" aria-label="close")
23
+ .modal-body
24
+
25
+ = turbo_frame_tag "custom-table-settings" do
26
+
27
+ %p= t("custom_table.customize_table_description")
28
+
29
+ = simple_form_for current_user, url: custom_table.setting_path(id: search_model.model_name) do |f|
30
+
31
+ = f.hidden_field "model", value: search_model.model_name
32
+ = f.hidden_field "variant", value: variant
33
+
34
+ = f.simple_fields_for :fields do |sfi|
35
+ %div{data: {controller: "sortable", "data-sortable-handle-value": ".handle"}, style: "columns: #{columns}"}
36
+
37
+ - items.each do |field, item|
38
+ -# = abort item.inspect
39
+ %div
40
+ %i.handle{style: "cursor: move", class: custom_table_move_icon_class}
41
+ .form-check.form-switch.d-inline-block
42
+ -# - if item[:appear] == :always
43
+ = sfi.hidden_field field, value: 1 if item[:appear] == :always
44
+ = sfi.check_box field, {checked: item[:selected], class: "form-check-input", disabled: (item[:appear] == :always)}, 1, 0#, as: :boolean, wrapper: :custom_boolean_switch, label: item[0], checked: true
45
+ %label.form-check-label= item[:label]
46
+ -# - if items.length < 20
47
+ -# = sf.input "fields", :as => :check_boxes, wrapper: :custom_boolean_switch, wrapper_html: {data: "kek"}, :collection => items, :label_text => false, label: false, checked: checkeds
48
+ -# - else
49
+ -# .row
50
+ -# .col-md-6
51
+ -# = sf.input "fields", :as => :check_boxes, wrapper: :custom_boolean_switch, :collection => items[0..(items.length/2)], :label_text => false, label: false, checked: checkeds
52
+ -# .col-md-6
53
+ -# = sf.input "fields", :as => :check_boxes, wrapper: :custom_boolean_switch, :collection => items[(items.length/2+1)..], :label_text => false, label: false, checked: checkeds
54
+
55
+ .mt-2
56
+ = f.button :submit, class: "btn-success", value: t("custom_table.save_settings")
57
+ = link_to t("custom_table.reset_settings"), custom_table.setting_path(id: search_model.model_name, variant: variant), data: {"turbo-method": "delete", "turbo-confirm": t("custom_table.are_you_sure")}, class: "btn btn-outline-secondary"