fustrate-rails 0.4.1 → 0.10.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.
Files changed (61) hide show
  1. checksums.yaml +5 -5
  2. data/config/initializers/jbuilder.rb +13 -0
  3. data/config/initializers/renderers.rb +40 -0
  4. data/config/initializers/sanitize.rb +112 -0
  5. data/config/rubocop/default.yml +96 -0
  6. data/config/rubocop/rails.yml +22 -0
  7. data/lib/fustrate/rails/concerns/clean_attributes.rb +49 -0
  8. data/lib/fustrate/rails/concerns/model.rb +48 -0
  9. data/lib/fustrate/rails/concerns/sanitize_html.rb +34 -0
  10. data/lib/fustrate/rails/engine.rb +14 -7
  11. data/lib/fustrate/rails/services/base.rb +46 -0
  12. data/lib/fustrate/rails/services/generate_csv.rb +35 -0
  13. data/lib/fustrate/rails/services/generate_excel.rb +32 -0
  14. data/lib/fustrate/rails/services/log_edit.rb +132 -0
  15. data/lib/fustrate/rails/spec_helper.rb +62 -0
  16. data/lib/fustrate/rails/version.rb +5 -1
  17. data/lib/fustrate-rails.rb +5 -2
  18. metadata +44 -140
  19. data/vendor/assets/javascripts/awesomplete.js +0 -402
  20. data/vendor/assets/javascripts/fustrate/_module.coffee +0 -140
  21. data/vendor/assets/javascripts/fustrate/components/_module.coffee +0 -3
  22. data/vendor/assets/javascripts/fustrate/components/alert_box.coffee +0 -10
  23. data/vendor/assets/javascripts/fustrate/components/autocomplete.coffee +0 -161
  24. data/vendor/assets/javascripts/fustrate/components/disclosure.coffee +0 -12
  25. data/vendor/assets/javascripts/fustrate/components/drop_zone.coffee +0 -9
  26. data/vendor/assets/javascripts/fustrate/components/dropdown.coffee +0 -48
  27. data/vendor/assets/javascripts/fustrate/components/file_picker.coffee +0 -11
  28. data/vendor/assets/javascripts/fustrate/components/flash.coffee +0 -31
  29. data/vendor/assets/javascripts/fustrate/components/modal.coffee +0 -273
  30. data/vendor/assets/javascripts/fustrate/components/pagination.coffee +0 -84
  31. data/vendor/assets/javascripts/fustrate/components/tabs.coffee +0 -28
  32. data/vendor/assets/javascripts/fustrate/components/tooltip.coffee +0 -72
  33. data/vendor/assets/javascripts/fustrate/generic_form.coffee +0 -30
  34. data/vendor/assets/javascripts/fustrate/generic_page.coffee +0 -40
  35. data/vendor/assets/javascripts/fustrate/generic_table.coffee +0 -57
  36. data/vendor/assets/javascripts/fustrate/listenable.coffee +0 -25
  37. data/vendor/assets/javascripts/fustrate/object.coffee +0 -21
  38. data/vendor/assets/javascripts/fustrate/record.coffee +0 -23
  39. data/vendor/assets/javascripts/fustrate.coffee +0 -6
  40. data/vendor/assets/stylesheets/_fustrate.sass +0 -7
  41. data/vendor/assets/stylesheets/awesomplete.sass +0 -76
  42. data/vendor/assets/stylesheets/fustrate/_colors.sass +0 -12
  43. data/vendor/assets/stylesheets/fustrate/_settings.sass +0 -20
  44. data/vendor/assets/stylesheets/fustrate/components/_components.sass +0 -36
  45. data/vendor/assets/stylesheets/fustrate/components/_functions.sass +0 -41
  46. data/vendor/assets/stylesheets/fustrate/components/alerts.sass +0 -86
  47. data/vendor/assets/stylesheets/fustrate/components/buttons.sass +0 -99
  48. data/vendor/assets/stylesheets/fustrate/components/disclosures.sass +0 -23
  49. data/vendor/assets/stylesheets/fustrate/components/dropdowns.sass +0 -36
  50. data/vendor/assets/stylesheets/fustrate/components/flash.sass +0 -38
  51. data/vendor/assets/stylesheets/fustrate/components/forms.sass +0 -195
  52. data/vendor/assets/stylesheets/fustrate/components/grid.sass +0 -196
  53. data/vendor/assets/stylesheets/fustrate/components/labels.sass +0 -64
  54. data/vendor/assets/stylesheets/fustrate/components/modals.sass +0 -167
  55. data/vendor/assets/stylesheets/fustrate/components/pagination.sass +0 -69
  56. data/vendor/assets/stylesheets/fustrate/components/panels.sass +0 -67
  57. data/vendor/assets/stylesheets/fustrate/components/popovers.sass +0 -19
  58. data/vendor/assets/stylesheets/fustrate/components/tables.sass +0 -62
  59. data/vendor/assets/stylesheets/fustrate/components/tabs.sass +0 -44
  60. data/vendor/assets/stylesheets/fustrate/components/tooltips.sass +0 -28
  61. data/vendor/assets/stylesheets/fustrate/components/typography.sass +0 -391
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 75f3e16d4415d2f05ab4f21f67359a8963e49ffe
4
- data.tar.gz: ff2d47763cf43d7259269ceeb79ff8831e2abbbf
2
+ SHA256:
3
+ metadata.gz: 881e74c991300471aafa028edfad4b01f100244396234b0da2688db01e2073be
4
+ data.tar.gz: 6cf4952d8518fe41835ea8bb4c13f6764c8aed89a97c43e6814af3d61cc787a9
5
5
  SHA512:
6
- metadata.gz: 68ced5f9d51d6cf972c81ce3b16e1fd5021fac7ae30a64b7df2b40e104431cc7edbaab19e2ab6a119bdc6c726c075444dc1e5e0425b734bf171641a12e9abd1d
7
- data.tar.gz: d242938ab2900d2f0889fe5f640c7c65572f6410400165fca2a1b0762096f9d4a368d70de0c8ff20091e862a2fe8691bd2c4f359410bb03b11011b16602f76cc
6
+ metadata.gz: 0da0314b9449d2dd31c7ad36dfb515094e91564b0805c8618d9a18bca6a9ff7930e154575f29070f7eff24a3df666d4c967a3af0ed7366234ed79b82ea7f9226
7
+ data.tar.gz: 8f0cd3d69cc37559f7c011f49e50396abb4f535b2cd665b8bb3d5926f765c6dd67abd75c93080686d5166f7bce3df4b7898684532a6a20365cc5395d0b3f8f3b
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Steven Hoffman
4
+ # All rights reserved.
5
+
6
+ class Jbuilder
7
+ def pagination!(results)
8
+ _set_value :currentPage, results.current_page
9
+ _set_value :totalPages, results.total_pages
10
+ _set_value :totalEntries, results.total_entries
11
+ _set_value :perPage, results.per_page
12
+ end
13
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Steven Hoffman
4
+ # All rights reserved.
5
+
6
+ ::ActionController::Renderers.add :excel do |data, options|
7
+ name = options[:filename] || 'export'
8
+
9
+ xlsx_options = { filename: "#{name}.xlsx", disposition: 'attachment', type: :xlsx }
10
+
11
+ case data
12
+ when ::Axlsx::Package
13
+ send_data(data.to_stream.read, **xlsx_options)
14
+ when ::Array
15
+ send_data(::Fustrate::Rails::Services::GenerateExcel.new.call(data, options[:sheet] || name), **xlsx_options)
16
+ when ::Pathname
17
+ xlsx_options[:filename] = data.basename.to_s if xlsx_options[:filename] == 'export'
18
+
19
+ send_file(data, **xlsx_options)
20
+ else
21
+ send_data(data, **xlsx_options)
22
+ end
23
+ end
24
+
25
+ ::ActionController::Renderers.add :csv do |data, options|
26
+ name = options[:filename] || 'export'
27
+
28
+ csv_options = { filename: "#{name}.csv", disposition: 'attachment', type: :csv }
29
+
30
+ case data
31
+ when ::Array, ::Hash
32
+ send_data(::Fustrate::Rails::Services::GenerateCsv.new.call(data), **csv_options)
33
+ else
34
+ send_data(data, **csv_options)
35
+ end
36
+ end
37
+
38
+ ::ActionController::Renderers.add :zip do |path, options|
39
+ send_file path, type: :zip, filename: "#{options[:filename]}.zip"
40
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Steven Hoffman
4
+ # All rights reserved.
5
+
6
+ class Sanitize
7
+ class CSS
8
+ def properties(css)
9
+ tree = ::Crass.parse_properties(
10
+ css,
11
+ preserve_comments: @config[:allow_comments],
12
+ preserve_hacks: @config[:allow_hacks]
13
+ )
14
+
15
+ tree!(tree)
16
+
17
+ ::CSSTreeStringifier.new.stringify(tree)
18
+ end
19
+ end
20
+
21
+ SELF_CLOSING = %w[br td img hr input].freeze
22
+
23
+ class << self
24
+ def clean_nodes(node)
25
+ # Replace non-root empty nodes with a blank space
26
+ return node.replace(' ') if empty_element?(node)
27
+
28
+ # Replace root empty nodes with a newline
29
+ if node.text? && !node.parent&.parent && node.text.blank?
30
+ # Replacing a newline with a newline causes an infinite loop
31
+ return node.text == "\n" ? node : node.replace("\n")
32
+ end
33
+
34
+ clean_style_attribute(node)
35
+ clean_attributes(node)
36
+ clean_whitespace(node)
37
+ end
38
+
39
+ def clean_page_breaks(node)
40
+ return unless node.element?
41
+
42
+ if node.content.match?(/↡/)
43
+ node.content = '↡'
44
+ node['class'] = 'pagebreak'
45
+ node.remove_attribute 'style'
46
+ elsif node['class']
47
+ node.remove_attribute 'class'
48
+ end
49
+ end
50
+
51
+ protected
52
+
53
+ # Remove useless padding and text-align attributes
54
+ def clean_style_attribute(node)
55
+ return unless node['style']
56
+
57
+ style = node['style']
58
+ .gsub(/padding: 0(?:px|rem|in|em|);/, '')
59
+ .gsub(/text-align: (?:start|justify|left);/, '')
60
+ .strip
61
+
62
+ if style.present?
63
+ node['style'] = style
64
+ else
65
+ node.remove_attribute 'style'
66
+ end
67
+ end
68
+
69
+ # Remove empty attributes from a node, e.g. alt=""
70
+ def clean_attributes(node)
71
+ return unless node.element?
72
+
73
+ node.attributes.each do |key, value|
74
+ node.remove_attribute(key) if value.text.empty?
75
+ end
76
+ end
77
+
78
+ def clean_whitespace(node)
79
+ return clean_element_whitespace(node) unless node.text?
80
+
81
+ node.content = node.content.gsub(/[[:space:]]+/, ' ')
82
+ end
83
+
84
+ def clean_element_whitespace(node)
85
+ return unless node.name.casecmp('p').zero?
86
+
87
+ clean_paragraph_tag(node.children.first, true)
88
+ clean_paragraph_tag(node.children.last, false)
89
+ end
90
+
91
+ def clean_paragraph_tag(node, is_first)
92
+ return unless node&.text?
93
+
94
+ return node.content = '' if node.text.blank?
95
+
96
+ # Remove all leading whitespace and replace all space-like chars with a single space
97
+ node.content = node.content
98
+ .gsub(is_first ? /\A[[:space:]]+/ : /[[:space:]]+\z/, '')
99
+ .gsub(/[[:space:]]+/, ' ')
100
+ end
101
+
102
+ def empty_element?(node) = node.element? && !self_closing?(node) && !content?(node)
103
+
104
+ def content?(node) = text?(node) || (node.element? && node.children.any? { content_or_self_closing?(_1) })
105
+
106
+ def content_or_self_closing?(node) = self_closing?(node) || text?(node) || content?(node)
107
+
108
+ def self_closing?(node) = self::SELF_CLOSING.include?(node.name.downcase)
109
+
110
+ def text?(node) = node.text? && node.text.present?
111
+ end
112
+ end
@@ -0,0 +1,96 @@
1
+ require:
2
+ - rubocop-performance
3
+
4
+ AllCops:
5
+ # Always enable new cops, disabling manually when they don't fit.
6
+ EnabledByDefault: true
7
+ NewCops: enable
8
+ SuggestExtensions: false
9
+
10
+ # ----------------------------------------------------------------------------------------------------------------------
11
+ # Coding Style
12
+ # ----------------------------------------------------------------------------------------------------------------------
13
+
14
+ Layout/MultilineMethodCallIndentation:
15
+ EnforcedStyle: indented
16
+
17
+ Lint/SuppressedException:
18
+ AllowComments: true
19
+
20
+ Performance/FlatMap:
21
+ EnabledForFlattenWithoutParams: true
22
+
23
+ Style/DateTime:
24
+ AllowCoercion: true
25
+
26
+ # ----------------------------------------------------------------------------------------------------------------------
27
+ # Cops that should not be run
28
+ # ----------------------------------------------------------------------------------------------------------------------
29
+
30
+ # Recommends each array item be on its own line.
31
+ Layout/MultilineArrayLineBreaks:
32
+ Enabled: false
33
+
34
+ # Recommends right hand side of multi-line assignment be on a new line
35
+ Layout/MultilineAssignmentLayout:
36
+ Enabled: false
37
+
38
+ # Wants every argument in a multi-line method call to be on its own line
39
+ Layout/MultilineMethodArgumentLineBreaks:
40
+ Enabled: false
41
+
42
+ # Recommends squishing a multi-line hash into one line. I don't like that. It's not readable.
43
+ Layout/RedundantLineBreak:
44
+ Enabled: false
45
+
46
+ # Recommends `::File` instead of `File`
47
+ Lint/ConstantResolution:
48
+ Enabled: false
49
+
50
+ # Recommends Integer(xxx, 10) instead of xxx.to_i
51
+ Lint/NumberConversion:
52
+ Enabled: false
53
+
54
+ # Seriously? "Postmaster" is racist now?
55
+ Naming/InclusiveLanguage:
56
+ Enabled: false
57
+
58
+ # String#drop is not a function, and besides, arr.drop(n) doesn't read better than arr[n..]
59
+ Performance/ArraySemiInfiniteRangeSlice:
60
+ Enabled: false
61
+
62
+ # I want to chain flatten/compact/uniq
63
+ Performance/ChainArrayAllocation:
64
+ Enabled: false
65
+
66
+ # Converts if-elsif to case-when.
67
+ Style/CaseLikeIf:
68
+ Enabled: false
69
+
70
+ # Wants every constant to be listed in #public_constant or #private_constant
71
+ Style/ConstantVisibility:
72
+ Enabled: false
73
+
74
+ # Converts [1, 2, three: 3] to [1, 2, { three: 3 }]
75
+ Style/HashAsLastArrayItem:
76
+ Enabled: false
77
+
78
+ # Prefers `if !condition` over `unless condition`
79
+ Style/InvertibleUnlessCondition:
80
+ Enabled: false
81
+
82
+ # Converts `a_method 1` to `a_method(1)`
83
+ Style/MethodCallWithArgsParentheses:
84
+ Enabled: false
85
+
86
+ # Not every `if` needs an `else`
87
+ Style/MissingElse:
88
+ Enabled: false
89
+
90
+ # Hashes that come from a database JSON column use strings for keys
91
+ Style/StringHashKeys:
92
+ Enabled: false
93
+
94
+ # I consider myself capable of understanding logical operators in an unless
95
+ Style/UnlessLogicalOperators:
96
+ Enabled: false
@@ -0,0 +1,22 @@
1
+ require:
2
+ - rubocop-rails
3
+
4
+ AllCops:
5
+ # Exclude auto-generated files and uploads
6
+ Exclude:
7
+ - 'bin/**/*'
8
+ - 'db/schema.rb'
9
+ - 'node_modules/**/*'
10
+ - 'public/**/*'
11
+ - 'storage/**/*'
12
+
13
+ Rails:
14
+ Enabled: true
15
+
16
+ # There are a ton of times I don't want to validate!
17
+ Rails/SkipsModelValidations:
18
+ Enabled: false
19
+
20
+ # This cop "may detect #to_s calls that are not related to Active Support"
21
+ Rails/ToSWithArgument:
22
+ Enabled: false
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Steven Hoffman
4
+ # All rights reserved.
5
+
6
+ module Fustrate
7
+ module Rails
8
+ module Concerns
9
+ module CleanAttributes
10
+ extend ::ActiveSupport::Concern
11
+
12
+ STRING_TYPES = %i[string text citext].freeze
13
+
14
+ # Collapse multiple spaces, remove leading/trailing whitespace, and remove carriage returns
15
+ def self.strip(text)
16
+ return text.map { strip(_1) } if text.is_a?(::Array)
17
+
18
+ return if text.blank?
19
+
20
+ text.strip.gsub(/ {2,}/, ' ').gsub(/^[ \t]+|[ \t]+$/, '').gsub(/\r\n?/, "\n").gsub(/\n{3,}/, "\n\n")
21
+ end
22
+
23
+ def self.string_columns(klass)
24
+ # There's no reason to clean polymorphic type columns
25
+ polymorphic_type_columns = klass.reflect_on_all_associations
26
+ .select { _1.options[:polymorphic] }
27
+ .map { "#{_1.name}_type" }
28
+
29
+ klass.columns
30
+ .select { self::STRING_TYPES.include?(_1.sql_type_metadata.type) }
31
+ .reject { polymorphic_type_columns.include?(_1.name) }
32
+ .map(&:name)
33
+ end
34
+
35
+ def self.clean_record(record)
36
+ string_columns(record.class).each do |attribute|
37
+ next unless record[attribute]
38
+
39
+ record[attribute] = strip record[attribute]
40
+ end
41
+ end
42
+
43
+ included do
44
+ before_validation { ::Fustrate::Rails::Concerns::CleanAttributes.clean_record(self) }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Steven Hoffman
4
+ # All rights reserved.
5
+
6
+ module Fustrate
7
+ module Rails
8
+ module Concerns
9
+ module Model
10
+ extend ::ActiveSupport::Concern
11
+
12
+ module ClassMethods
13
+ # Allow models to define a more reasonable name, usually used to remove a module/namespace.
14
+ def human_name(new_name = nil)
15
+ @human_name = new_name.to_s if new_name
16
+
17
+ @human_name ||= to_s.underscore.gsub(%r{\A.*/}, '')
18
+ end
19
+
20
+ def build_from_params(permitted_params, **attributes)
21
+ key = attributes.delete(:params_key)
22
+
23
+ new(**attributes) { _1.assign_params(permitted_params, key:) }
24
+ end
25
+ end
26
+
27
+ included do
28
+ self.abstract_class = true
29
+ self.inheritance_column = :_disabled
30
+
31
+ # Assign strong parameters based on the class name - just a convenience method for services.
32
+ def assign_params(permitted_params, key: nil)
33
+ if key == false
34
+ assign_attributes ::Current.params.permit(permitted_params)
35
+ else
36
+ assign_attributes ::Current.params.require(key || default_param_key).permit(permitted_params)
37
+ end
38
+ end
39
+
40
+ # Define a different Editable record to log edits on
41
+ def log_edits_on = self
42
+
43
+ def default_param_key = ::ActiveModel::Naming.param_key(self.class)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Steven Hoffman
4
+ # All rights reserved.
5
+
6
+ module Fustrate
7
+ module Rails
8
+ module Concerns
9
+ module SanitizeHtml
10
+ extend ::ActiveSupport::Concern
11
+
12
+ def self.sanitize(html, config) = smart_strip ::Sanitize.fragment(normalize(html), config)
13
+
14
+ # Remove non-breaking & ideographic spaces before sanitizing, and un-fancy quotes.
15
+ def self.normalize(html) = html.tr('‘’“”', %q(''"")).gsub(/(?:[\u00A0\u3000]|&nbsp;) ?/, ' ')
16
+
17
+ # There shouldn't be whitespace or newlines at the beginning or end of the text
18
+ def self.smart_strip(html) = html.gsub(/\A(?:[[:space:]]|<br>)+|(?:[[:space:]]|<br>)+\z/, '')
19
+
20
+ module ClassMethods
21
+ def sanitize_html(*attributes, config)
22
+ before_validation do
23
+ attributes.flatten.each do |attribute|
24
+ next unless self[attribute]
25
+
26
+ self[attribute] = ::Fustrate::Rails::Concerns::SanitizeHtml.sanitize(self[attribute], config)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,14 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Steven Hoffman
4
+ # All rights reserved.
5
+
1
6
  require 'rails/engine'
2
7
 
3
- require 'bourbon'
4
- require 'js-routes'
5
- require 'modernizr-rails'
6
- require 'momentjs-rails'
7
- require 'font-awesome-rails'
8
+ require 'fustrate/rails/concerns/clean_attributes'
9
+ require 'fustrate/rails/concerns/model'
10
+ require 'fustrate/rails/concerns/sanitize_html'
11
+
12
+ require 'fustrate/rails/services/base'
13
+ require 'fustrate/rails/services/generate_csv'
14
+ require 'fustrate/rails/services/generate_excel'
15
+ require 'fustrate/rails/services/log_edit'
8
16
 
9
17
  module Fustrate
10
18
  module Rails
11
- class Engine < ::Rails::Engine
12
- end
19
+ class Engine < ::Rails::Engine; end
13
20
  end
14
21
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Steven Hoffman
4
+ # All rights reserved.
5
+
6
+ module Fustrate
7
+ module Rails
8
+ module Services
9
+ class Base
10
+ # Lets us use `t` and `l` helpers.
11
+ include ::ActionView::Helpers::TranslationHelper
12
+
13
+ protected
14
+
15
+ def service(service_class) = service_class.new
16
+
17
+ def authorize(action, resource) = ::Authority.enforce(action, resource, ::Current.user)
18
+
19
+ def transaction(&) = ::ActiveRecord::Base.transaction(&)
20
+
21
+ class LoadPage < self
22
+ DEFAULT_ORDER = nil
23
+ DEFAULT_INCLUDES = nil
24
+ RESULTS_PER_PAGE = 25
25
+
26
+ def call(page: nil, includes: nil, scope: nil, order: nil)
27
+ (scope || default_scope)
28
+ .reorder(order || default_order)
29
+ .paginate(page: page || params[:page], per_page: self.class::RESULTS_PER_PAGE)
30
+ .includes(includes || self.class::DEFAULT_INCLUDES)
31
+ end
32
+
33
+ protected
34
+
35
+ def default_scope = (raise ::NotImplementedError, '#default_scope not defined')
36
+
37
+ def default_order
38
+ return self.class::DEFAULT_ORDER.call if self.class::DEFAULT_ORDER.is_a? ::Proc
39
+
40
+ self.class::DEFAULT_ORDER
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Steven Hoffman
4
+ # All rights reserved.
5
+
6
+ module Fustrate
7
+ module Rails
8
+ module Services
9
+ class GenerateCsv
10
+ def call(data) = data.first.is_a?(::Hash) ? csv_from_hash(data) : csv_from_array(data)
11
+
12
+ protected
13
+
14
+ def csv_from_hash(data)
15
+ ::CSV.generate do |csv|
16
+ csv << data.first.keys
17
+
18
+ data.each do |row|
19
+ csv << (row.values.map { _1&.to_s&.tr("\n", "\v") })
20
+ end
21
+ end
22
+ end
23
+
24
+ def csv_from_array(data)
25
+ ::CSV.generate do |csv|
26
+ # It's just an array of arrays; the first row is likely the header
27
+ data.each do |row|
28
+ csv << (Array(row).map { _1&.to_s&.tr("\n", "\v") })
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) Steven Hoffman
4
+ # All rights reserved.
5
+
6
+ module Fustrate
7
+ module Rails
8
+ module Services
9
+ class GenerateExcel
10
+ def call(data, name = 'Sheet 1')
11
+ ::Axlsx::Package.new do |package|
12
+ package.use_shared_strings = true
13
+
14
+ @wrap = package.workbook.styles.add_style(alignment: { wrap_text: true })
15
+
16
+ package.workbook.add_worksheet(name:) { add_data_to_sheet(data, _1) }
17
+
18
+ return package.to_stream.read
19
+ end
20
+ end
21
+
22
+ protected
23
+
24
+ def add_data_to_sheet(data, sheet)
25
+ sheet.add_row data.first.keys if data.any?
26
+
27
+ data.each { sheet.add_row _1.values, style: @wrap }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end