mensa 0.2.4 → 0.2.6
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 +4 -4
- data/.devcontainer/Dockerfile +6 -2
- data/.devcontainer/compose.yaml +1 -1
- data/.devcontainer/devcontainer.json +31 -29
- data/.devcontainer/postCreate.sh +8 -0
- data/.devcontainer/postStart.sh +9 -0
- data/.gitignore +3 -1
- data/.zed/tasks.json +12 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile.lock +155 -153
- data/Procfile +1 -1
- data/README.md +95 -60
- data/app/assets/stylesheets/mensa/application.css +14 -11
- data/app/components/mensa/add_filter/component.css +110 -5
- data/app/components/mensa/add_filter/component.html.slim +10 -12
- data/app/components/mensa/add_filter/component.rb +8 -2
- data/app/components/mensa/add_filter/component_controller.js +697 -85
- data/app/components/mensa/cell/component.css +9 -0
- data/app/components/mensa/column_customizer/component.css +40 -0
- data/app/components/mensa/column_customizer/component.html.slim +14 -0
- data/app/components/mensa/column_customizer/component.rb +13 -0
- data/app/components/mensa/column_customizer/component_controller.js +383 -0
- data/app/components/mensa/control_bar/component.css +127 -4
- data/app/components/mensa/control_bar/component.html.slim +41 -14
- data/app/components/mensa/control_bar/component.rb +2 -6
- data/app/components/mensa/empty_state/component.css +20 -0
- data/app/components/mensa/empty_state/component.html.slim +7 -0
- data/app/components/mensa/empty_state/component.rb +18 -0
- data/app/components/mensa/filter_pill/component.css +23 -0
- data/app/components/mensa/filter_pill/component.html.slim +9 -0
- data/app/components/mensa/filter_pill/component.rb +24 -0
- data/app/components/mensa/filter_pill/component_controller.js +52 -0
- data/app/components/mensa/filter_pill_list/component.css +63 -0
- data/app/components/mensa/filter_pill_list/component.html.slim +11 -0
- data/app/components/mensa/{filter_list → filter_pill_list}/component.rb +1 -1
- data/app/components/mensa/filter_pill_list/component_controller.js +749 -0
- data/app/components/mensa/header/component.css +41 -43
- data/app/components/mensa/header/component.html.slim +7 -7
- data/app/components/mensa/header/component.rb +1 -1
- data/app/components/mensa/row_action/component.html.slim +2 -2
- data/app/components/mensa/row_action/component.rb +1 -1
- data/app/components/mensa/search/component.css +68 -9
- data/app/components/mensa/search/component.html.slim +19 -15
- data/app/components/mensa/search/component.rb +1 -1
- data/app/components/mensa/search/component_controller.js +39 -49
- data/app/components/mensa/selection/component_controller.js +147 -0
- data/app/components/mensa/table/component.css +28 -0
- data/app/components/mensa/table/component.html.slim +9 -6
- data/app/components/mensa/table/component.rb +1 -0
- data/app/components/mensa/table/component_controller.js +524 -76
- data/app/components/mensa/table_row/component.css +6 -0
- data/app/components/mensa/table_row/component.html.slim +8 -3
- data/app/components/mensa/table_row/component.rb +1 -1
- data/app/components/mensa/view/component.css +97 -29
- data/app/components/mensa/view/component.html.slim +23 -10
- data/app/components/mensa/view/component.rb +5 -0
- data/app/components/mensa/views/component.css +106 -13
- data/app/components/mensa/views/component.html.slim +51 -17
- data/app/components/mensa/views/component_controller.js +245 -20
- data/app/controllers/mensa/application_controller.rb +1 -1
- data/app/controllers/mensa/tables/batch_actions_controller.rb +24 -0
- data/app/controllers/mensa/tables/exports_controller.rb +96 -0
- data/app/controllers/mensa/tables/filters_controller.rb +6 -2
- data/app/controllers/mensa/tables/views_controller.rb +108 -0
- data/app/controllers/mensa/tables_controller.rb +5 -14
- data/app/helpers/mensa/application_helper.rb +4 -1
- data/app/javascript/mensa/application.js +2 -2
- data/app/javascript/mensa/controllers/application_controller.js +5 -21
- data/app/javascript/mensa/controllers/index.js +16 -7
- data/app/jobs/mensa/export_job.rb +77 -85
- data/app/models/mensa/export.rb +93 -0
- data/app/tables/mensa/action.rb +3 -1
- data/app/tables/mensa/base.rb +103 -17
- data/app/tables/mensa/batch_action.rb +27 -0
- data/app/tables/mensa/cell.rb +21 -6
- data/app/tables/mensa/column.rb +30 -25
- data/app/tables/mensa/config/action_dsl.rb +1 -1
- data/app/tables/mensa/config/batch_dsl.rb +13 -0
- data/app/tables/mensa/config/column_dsl.rb +1 -0
- data/app/tables/mensa/config/dsl_logic.rb +8 -4
- data/app/tables/mensa/config/filter_dsl.rb +4 -1
- data/app/tables/mensa/config/render_dsl.rb +1 -1
- data/app/tables/mensa/config/table_dsl.rb +14 -4
- data/app/tables/mensa/config/view_dsl.rb +2 -0
- data/app/tables/mensa/config_readers.rb +34 -3
- data/app/tables/mensa/filter.rb +94 -14
- data/app/tables/mensa/row.rb +1 -1
- data/app/tables/mensa/scope.rb +25 -13
- data/app/views/mensa/exports/_badge.html.slim +5 -0
- data/app/views/mensa/exports/_dialog.html.slim +42 -0
- data/app/views/mensa/exports/_list.html.slim +29 -0
- data/app/views/mensa/tables/filters/show.turbo_stream.slim +34 -6
- data/app/views/mensa/tables/show.html.slim +2 -0
- data/app/views/mensa/tables/show.turbo_stream.slim +1 -1
- data/app/views/mensa/tables/views/create.turbo_stream.slim +11 -0
- data/app/views/mensa/tables/views/destroy.turbo_stream.slim +11 -0
- data/app/views/mensa/tables/views/update.turbo_stream.slim +11 -0
- data/bin/setup +1 -1
- data/config/locales/en.yml +45 -1
- data/config/locales/nl.yml +46 -1
- data/config/routes.rb +7 -0
- data/db/migrate/20260604120000_create_mensa_exports.rb +25 -0
- data/docs/columns.png +0 -0
- data/docs/export.png +0 -0
- data/docs/filters.png +0 -0
- data/docs/table.png +0 -0
- data/lib/generators/mensa/tailwind_config_generator.rb +3 -3
- data/lib/generators/mensa/templates/config/initializers/mensa.rb +1 -1
- data/lib/mensa/configuration.rb +35 -15
- data/lib/mensa/engine.rb +15 -10
- data/lib/mensa/version.rb +1 -1
- data/lib/mensa.rb +2 -2
- data/lib/tasks/mensa_tasks.rake +1 -1
- data/mensa.gemspec +3 -2
- data/mise.toml +8 -0
- data/package-lock.json +0 -7
- metadata +60 -15
- data/app/components/mensa/filter/component_controller.js +0 -12
- data/app/components/mensa/filter_list/component.css +0 -14
- data/app/components/mensa/filter_list/component.html.slim +0 -14
- data/app/components/mensa/filter_list/component_controller.js +0 -14
- /data/{rubocop.yml → .rubocop.yml} +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mensa
|
|
4
|
+
module Tables
|
|
5
|
+
class BatchActionsController < ::ApplicationController
|
|
6
|
+
def create
|
|
7
|
+
table_name = params[:table_id]
|
|
8
|
+
batch_action_name = params[:batch_action_name]&.to_sym
|
|
9
|
+
record_ids = Array(params[:record_ids])
|
|
10
|
+
|
|
11
|
+
table = Mensa.for_name(table_name)
|
|
12
|
+
table.original_view_context = helpers
|
|
13
|
+
|
|
14
|
+
batch_action = table.batch_actions.find { |a| a.name == batch_action_name }
|
|
15
|
+
return head :not_found unless batch_action
|
|
16
|
+
|
|
17
|
+
records = table.model.where(id: record_ids).to_a
|
|
18
|
+
batch_action.process.call(records)
|
|
19
|
+
|
|
20
|
+
redirect_back_or_to helpers.mensa.table_path(table_name)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module Mensa
|
|
2
|
+
module Tables
|
|
3
|
+
# Lists a user's available downloads for a table and creates new export
|
|
4
|
+
# requests. Generating the CSV happens asynchronously in Mensa::ExportJob;
|
|
5
|
+
# both the export button badge and the downloads list are refreshed via
|
|
6
|
+
# Turbo streams once the job completes.
|
|
7
|
+
class ExportsController < ::ApplicationController
|
|
8
|
+
# Returns the current downloads list for the table, used to refresh the
|
|
9
|
+
# contents of the export dialog when it is opened.
|
|
10
|
+
def index
|
|
11
|
+
respond_to do |format|
|
|
12
|
+
format.turbo_stream { render turbo_stream: list_stream }
|
|
13
|
+
format.html { render partial: "mensa/exports/list", locals: list_locals }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Creates a new export for the current user and enqueues the job that
|
|
18
|
+
# generates and attaches the CSV.
|
|
19
|
+
def create
|
|
20
|
+
export = Mensa::Export.new(
|
|
21
|
+
table_name: params[:table_id],
|
|
22
|
+
table_view_id: params[:table_view_id].presence,
|
|
23
|
+
user: current_mensa_user,
|
|
24
|
+
format: params[:export_format].to_s.presence_in(Mensa::Export::FORMATS) || "csv_excel",
|
|
25
|
+
scope: params[:scope].to_s.presence_in(Mensa::Export::SCOPES) || "all",
|
|
26
|
+
config: params.permit(:query, :page, order: {}, filters: {}).to_h,
|
|
27
|
+
status: "pending"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if export.save
|
|
31
|
+
Mensa::ExportJob.perform_later(export)
|
|
32
|
+
|
|
33
|
+
respond_to do |format|
|
|
34
|
+
format.turbo_stream { render turbo_stream: [list_stream, badge_stream] }
|
|
35
|
+
format.json { render json: {id: export.id}, status: :created }
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
respond_to do |format|
|
|
39
|
+
format.turbo_stream { head :unprocessable_entity }
|
|
40
|
+
format.json { render json: {errors: export.errors.full_messages}, status: :unprocessable_entity }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Streams the generated CSV and then removes the export, purging the
|
|
46
|
+
# attached asset. Downloads are single-use: routing them through the
|
|
47
|
+
# controller (instead of a direct Active Storage link) gives us a hook to
|
|
48
|
+
# delete the Mensa::Export record and free the stored file afterwards.
|
|
49
|
+
def download
|
|
50
|
+
export = exports.find(params[:id])
|
|
51
|
+
return head :not_found unless export.downloadable?
|
|
52
|
+
|
|
53
|
+
data = export.asset.download
|
|
54
|
+
filename = export.asset.filename.to_s.presence || export.filename.presence || "#{export.table_name}_export.csv"
|
|
55
|
+
content_type = export.asset.content_type.presence || "text/csv"
|
|
56
|
+
|
|
57
|
+
send_data data, filename: filename, type: content_type, disposition: "attachment"
|
|
58
|
+
|
|
59
|
+
# Clean up after a successful send. `has_one_attached :asset` purges the
|
|
60
|
+
# blob when the record is destroyed (dependent: :purge_later). Never let
|
|
61
|
+
# cleanup failures break the download itself.
|
|
62
|
+
begin
|
|
63
|
+
export.destroy
|
|
64
|
+
Mensa::Export.broadcast_refresh(export.table_name, export.user)
|
|
65
|
+
rescue => e
|
|
66
|
+
Mensa.config.logger&.warn("Mensa::Export cleanup failed for #{export.id}: #{e.class}: #{e.message}")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def exports
|
|
73
|
+
@exports ||= Mensa::Export.for_table(params[:table_id]).for_user(current_mensa_user).recent
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def list_stream
|
|
77
|
+
turbo_stream.replace(Mensa::Export.list_dom_id(params[:table_id], current_mensa_user),
|
|
78
|
+
partial: "mensa/exports/list", locals: list_locals)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def badge_stream
|
|
82
|
+
turbo_stream.replace(Mensa::Export.badge_dom_id(params[:table_id], current_mensa_user),
|
|
83
|
+
partial: "mensa/exports/badge",
|
|
84
|
+
locals: {table_name: params[:table_id], user: current_mensa_user})
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def list_locals
|
|
88
|
+
{table_name: params[:table_id], user: current_mensa_user, exports: exports}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def current_mensa_user
|
|
92
|
+
Current.user if defined?(Current) && Current.respond_to?(:user)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -5,7 +5,8 @@ module Mensa
|
|
|
5
5
|
|
|
6
6
|
class Filter
|
|
7
7
|
include ActiveModel::Model
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
attr_accessor :column, :value
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def index
|
|
@@ -15,10 +16,13 @@ module Mensa
|
|
|
15
16
|
|
|
16
17
|
# Returns the filter information on the column-name
|
|
17
18
|
def show
|
|
18
|
-
config = {}.merge(params.permit(:id, :page, :table_id, :target, :table_view_id, :turbo_frame_id, order: {}, filters: {}).to_h)
|
|
19
|
+
config = {}.merge(params.permit(:id, :value, :operator, :page, :table_id, :target, :table_view_id, :turbo_frame_id, order: {}, filters: {}).to_h)
|
|
19
20
|
@table = Mensa.for_name(params[:table_id], config)
|
|
20
21
|
@table.original_view_context = helpers
|
|
21
22
|
@column = @table.column(params[:id])
|
|
23
|
+
@operator = params[:operator].presence || "is"
|
|
24
|
+
@multiple = @column.filter.multiple?
|
|
25
|
+
@values = Array(params[:value]).flatten.compact
|
|
22
26
|
respond_to do |format|
|
|
23
27
|
format.turbo_stream
|
|
24
28
|
format.html
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module Mensa
|
|
2
|
+
module Tables
|
|
3
|
+
class ViewsController < ::ApplicationController
|
|
4
|
+
# Persists the current filters, ordering and search query as a named
|
|
5
|
+
# custom view, owned by the current user. Without a current user there is
|
|
6
|
+
# nobody to own the view, so the request is rejected (and the UI hides the
|
|
7
|
+
# save button in that case).
|
|
8
|
+
def create
|
|
9
|
+
user = current_mensa_user
|
|
10
|
+
return head(:forbidden) if user.blank?
|
|
11
|
+
|
|
12
|
+
view = Mensa::TableView.new(
|
|
13
|
+
table_name: params[:table_id],
|
|
14
|
+
name: params[:name],
|
|
15
|
+
description: params[:description],
|
|
16
|
+
config: view_config,
|
|
17
|
+
user: user
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if view.save
|
|
21
|
+
respond_to do |format|
|
|
22
|
+
format.turbo_stream do
|
|
23
|
+
# Re-use the same turbo_frame_id that the client sent so the
|
|
24
|
+
# generated element IDs match what is already in the DOM.
|
|
25
|
+
table_config = view.config
|
|
26
|
+
.deep_transform_keys(&:to_sym)
|
|
27
|
+
.merge(turbo_frame_id: params[:turbo_frame_id])
|
|
28
|
+
|
|
29
|
+
@table = Mensa.for_name(params[:table_id], table_config)
|
|
30
|
+
@table.request = request
|
|
31
|
+
@table.original_view_context = helpers
|
|
32
|
+
@table.table_view = view
|
|
33
|
+
end
|
|
34
|
+
format.json { render json: {id: view.id, name: view.name}, status: :created }
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
render json: {errors: view.errors.full_messages}, status: :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def update
|
|
42
|
+
user = current_mensa_user
|
|
43
|
+
return head(:forbidden) if user.blank?
|
|
44
|
+
|
|
45
|
+
view = Mensa::TableView.find_by(table_name: params[:table_id], id: params[:id], user: user)
|
|
46
|
+
return head(:not_found) if view.blank?
|
|
47
|
+
|
|
48
|
+
# When only renaming, preserve the existing config rather than overwriting
|
|
49
|
+
# with an empty hash from view_config.
|
|
50
|
+
new_config = view_config
|
|
51
|
+
update_attrs = {config: new_config.present? ? new_config : view.config}
|
|
52
|
+
update_attrs[:name] = params[:name] if params[:name].present?
|
|
53
|
+
|
|
54
|
+
if view.update(update_attrs)
|
|
55
|
+
respond_to do |format|
|
|
56
|
+
format.turbo_stream do
|
|
57
|
+
table_config = view.config
|
|
58
|
+
.deep_transform_keys(&:to_sym)
|
|
59
|
+
.merge(turbo_frame_id: params[:turbo_frame_id])
|
|
60
|
+
|
|
61
|
+
@table = Mensa.for_name(params[:table_id], table_config)
|
|
62
|
+
@table.request = request
|
|
63
|
+
@table.original_view_context = helpers
|
|
64
|
+
@table.table_view = view
|
|
65
|
+
end
|
|
66
|
+
format.json { render json: {id: view.id, name: view.name} }
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
render json: {errors: view.errors.full_messages}, status: :unprocessable_entity
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def destroy
|
|
74
|
+
user = current_mensa_user
|
|
75
|
+
return head(:forbidden) if user.blank?
|
|
76
|
+
|
|
77
|
+
view = Mensa::TableView.find_by(table_name: params[:table_id], id: params[:id], user: user)
|
|
78
|
+
return head(:not_found) if view.blank?
|
|
79
|
+
|
|
80
|
+
view.destroy
|
|
81
|
+
|
|
82
|
+
respond_to do |format|
|
|
83
|
+
format.turbo_stream do
|
|
84
|
+
@table = Mensa.for_name(params[:table_id], {turbo_frame_id: params[:turbo_frame_id]})
|
|
85
|
+
@table.request = request
|
|
86
|
+
@table.original_view_context = helpers
|
|
87
|
+
end
|
|
88
|
+
format.json { head :no_content }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# The view configuration mirrors the query parameters the table reads on
|
|
95
|
+
# show (query, ordering and filters), so a saved view can be replayed by
|
|
96
|
+
# merging this hash back into the request params.
|
|
97
|
+
def view_config
|
|
98
|
+
config = params.permit(:query, order: {}, column_order: [], hidden_columns: []).to_h
|
|
99
|
+
config[:filters] = params[:filters]&.to_unsafe_h || {}
|
|
100
|
+
config
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def current_mensa_user
|
|
104
|
+
Current.user if defined?(Current) && Current.respond_to?(:user)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
module Mensa
|
|
2
|
-
class TablesController <
|
|
3
|
-
layout :decide_layout
|
|
4
|
-
|
|
2
|
+
class TablesController < ApplicationController
|
|
5
3
|
def show
|
|
6
4
|
@table = Mensa.for_name(params[:id])
|
|
7
5
|
|
|
@@ -9,11 +7,12 @@ module Mensa
|
|
|
9
7
|
if params[:table_view_id]
|
|
10
8
|
@view = Mensa::TableView.find_by(table_name: params[:id], id: params[:table_view_id])
|
|
11
9
|
@view ||= @table.system_views.find { |v| v.id == params[:table_view_id].to_sym }
|
|
12
|
-
config = @view&.config
|
|
10
|
+
config = @view&.config&.deep_transform_keys(&:to_sym)
|
|
13
11
|
end
|
|
14
12
|
|
|
15
13
|
config = config.merge(params.permit!.to_h)
|
|
16
|
-
config = config.merge(params.permit(:format, :query, :id, :page, :table_view_id, :turbo_frame_id, order: {},
|
|
14
|
+
config = config.merge(params.permit(:format, :query, :id, :page, :table_view_id, :turbo_frame_id, order: {}, column_order: [], hidden_columns: []).to_h)
|
|
15
|
+
config[:filters] = params[:filters]&.to_unsafe_h || config[:filters] || {}
|
|
17
16
|
|
|
18
17
|
@table = Mensa.for_name(params[:id], config)
|
|
19
18
|
@table.request = request
|
|
@@ -21,17 +20,9 @@ module Mensa
|
|
|
21
20
|
@table.original_view_context = helpers
|
|
22
21
|
|
|
23
22
|
respond_to do |format|
|
|
24
|
-
format.turbo_stream
|
|
23
|
+
format.turbo_stream # Used for filterering
|
|
25
24
|
format.html
|
|
26
|
-
format.xlsx do
|
|
27
|
-
Mensa::ExportJob.perform_later(current_user, params[:id])
|
|
28
|
-
head 200
|
|
29
|
-
end
|
|
30
25
|
end
|
|
31
26
|
end
|
|
32
|
-
|
|
33
|
-
def decide_layout
|
|
34
|
-
false if params[:turbo_frame_id]
|
|
35
|
-
end
|
|
36
27
|
end
|
|
37
28
|
end
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import "@hotwired/turbo-rails"
|
|
2
|
-
import "mensa/controllers"
|
|
1
|
+
import "@hotwired/turbo-rails";
|
|
2
|
+
import "mensa/controllers";
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import { Controller } from '@hotwired/stimulus'
|
|
3
3
|
|
|
4
4
|
export default class ApplicationController extends Controller {
|
|
5
|
-
connect
|
|
5
|
+
connect() {
|
|
6
6
|
this.element[this.identifier] = this
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
getController
|
|
9
|
+
getController(element, identifier) {
|
|
10
10
|
return this.application.getControllerForElementAndIdentifier(element, identifier)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
triggerEvent
|
|
13
|
+
triggerEvent(el, name, data) {
|
|
14
14
|
let event
|
|
15
15
|
if (typeof window.CustomEvent === 'function') {
|
|
16
16
|
event = new CustomEvent(name, { detail: data, cancelable: true, bubbles: true })
|
|
@@ -21,14 +21,14 @@ export default class ApplicationController extends Controller {
|
|
|
21
21
|
el.dispatchEvent(event)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
elementScrolled
|
|
24
|
+
elementScrolled(element) {
|
|
25
25
|
if (element.scrollHeight - Math.round(element.scrollTop) === element.clientHeight) {
|
|
26
26
|
return true
|
|
27
27
|
}
|
|
28
28
|
return false
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
debouncedHover
|
|
31
|
+
debouncedHover(element, timeout, handler) {
|
|
32
32
|
var timeoutId = null
|
|
33
33
|
element.addEventListener(marker, 'mouseover', function () {
|
|
34
34
|
timeoutId = setTimeout(handler, timeout)
|
|
@@ -38,20 +38,4 @@ export default class ApplicationController extends Controller {
|
|
|
38
38
|
clearTimeout(timeoutId)
|
|
39
39
|
})
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
get ourUrl () {
|
|
43
|
-
// This requires the mensaTableOutlet
|
|
44
|
-
let turboFrame = this.mensaTableOutlet?.turboFrameTarget
|
|
45
|
-
if(!turboFrame) {
|
|
46
|
-
turboFrame = this.turboFrameTarget
|
|
47
|
-
}
|
|
48
|
-
let url
|
|
49
|
-
|
|
50
|
-
if (turboFrame && turboFrame.getAttribute('src')) {
|
|
51
|
-
url = new URL(turboFrame.getAttribute('src'))
|
|
52
|
-
} else {
|
|
53
|
-
url = new URL(window.location.href)
|
|
54
|
-
}
|
|
55
|
-
return url
|
|
56
|
-
}
|
|
57
41
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { application } from "mensa/controllers/application"
|
|
1
|
+
import { application } from "mensa/controllers/application";
|
|
2
2
|
|
|
3
3
|
// import AddFilterComponentController from 'components/add_filter/component_controller';
|
|
4
4
|
// application.register('mensa-add-filter', AddFilterComponentController)
|
|
@@ -6,21 +6,30 @@ import { application } from "mensa/controllers/application"
|
|
|
6
6
|
import AddFilterComponentController from "mensa/components/add_filter/component_controller";
|
|
7
7
|
application.register("mensa-add-filter", AddFilterComponentController);
|
|
8
8
|
|
|
9
|
-
import
|
|
10
|
-
application.register("mensa-filter",
|
|
9
|
+
import FilterPillComponentController from "mensa/components/filter_pill/component_controller";
|
|
10
|
+
application.register("mensa-filter-pill", FilterPillComponentController);
|
|
11
11
|
|
|
12
|
-
import
|
|
13
|
-
application.register(
|
|
12
|
+
import FilterPillListComponentController from "mensa/components/filter_pill_list/component_controller";
|
|
13
|
+
application.register(
|
|
14
|
+
"mensa-filter-pill-list",
|
|
15
|
+
FilterPillListComponentController,
|
|
16
|
+
);
|
|
14
17
|
|
|
15
18
|
import SearchComponentController from "mensa/components/search/component_controller";
|
|
16
19
|
application.register("mensa-search", SearchComponentController);
|
|
17
20
|
|
|
18
|
-
import TableComponentController from
|
|
21
|
+
import TableComponentController from "mensa/components/table/component_controller";
|
|
19
22
|
application.register("mensa-table", TableComponentController);
|
|
20
23
|
|
|
21
|
-
import ViewsComponentController from
|
|
24
|
+
import ViewsComponentController from "mensa/components/views/component_controller";
|
|
22
25
|
application.register("mensa-views", ViewsComponentController);
|
|
23
26
|
|
|
27
|
+
import SelectionComponentController from "mensa/components/selection/component_controller";
|
|
28
|
+
application.register("mensa-selection", SelectionComponentController);
|
|
29
|
+
|
|
30
|
+
import ColumnCustomizerController from "mensa/components/column_customizer/component_controller";
|
|
31
|
+
application.register("mensa-column-customizer", ColumnCustomizerController);
|
|
32
|
+
|
|
24
33
|
// Eager load all controllers defined in the import map under controllers/**/*_controller
|
|
25
34
|
// import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
26
35
|
// eagerLoadControllersFrom("controllers", application)
|
|
@@ -1,111 +1,103 @@
|
|
|
1
|
+
require "csv"
|
|
1
2
|
require "securerandom"
|
|
3
|
+
require "stringio"
|
|
2
4
|
|
|
3
5
|
module Mensa
|
|
6
|
+
# Generates the CSV for a Mensa::Export, attaches it to the export's +asset+
|
|
7
|
+
# and broadcasts the refreshed export button badge and downloads list so the
|
|
8
|
+
# requesting user sees the finished download appear without reloading.
|
|
4
9
|
class ExportJob < ApplicationJob
|
|
5
10
|
queue_as :default
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# export_complete: lambda do |user_id, table_name, attachment|
|
|
10
|
-
# end
|
|
12
|
+
def perform(export)
|
|
13
|
+
return unless export
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return unless table.exportable?
|
|
15
|
+
export.update(status: "processing")
|
|
16
|
+
Mensa.config.callbacks[:export_started]&.call(export)
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
table = build_table(export)
|
|
19
|
+
unless table.exportable?
|
|
20
|
+
finalize(export, status: "failed")
|
|
21
|
+
return
|
|
22
|
+
end
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
p.use_shared_strings = true
|
|
24
|
+
data, filename, content_type = generate(table, export)
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
export.asset.attach(io: StringIO.new(data), filename: filename, content_type: content_type)
|
|
27
|
+
finalize(export, status: "completed", filename: filename)
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
Mensa.config.callbacks[:export_complete]&.call(export)
|
|
30
|
+
rescue => e
|
|
31
|
+
Mensa.config.logger&.error("Mensa::ExportJob failed for export #{export_id}: #{e.class}: #{e.message}")
|
|
32
|
+
finalize(export, status: "failed") if export
|
|
33
|
+
raise
|
|
34
|
+
end
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
custom_style = {}
|
|
36
|
+
private
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# column_widths[index] = width if width
|
|
43
|
-
end
|
|
38
|
+
# Rebuilds the table the export was requested for, layering the view
|
|
39
|
+
# configuration (if any) underneath the captured request configuration
|
|
40
|
+
# (filters, query, ordering, page) so the generated data matches what the
|
|
41
|
+
# user saw when they requested the export.
|
|
42
|
+
def build_table(export)
|
|
43
|
+
config = {}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
table.export_rows.each do |row|
|
|
51
|
-
# next if row.reject { |column, value| value.nil? || value == '' }.blank?
|
|
52
|
-
#
|
|
53
|
-
# row_types = :string #row.map(&:second)
|
|
54
|
-
# row_values = row.map(&:third)
|
|
55
|
-
# options = row.map(&:last)
|
|
56
|
-
#
|
|
57
|
-
# row_styles = options.map.with_index { |option, i|
|
|
58
|
-
# type = row_types[i]
|
|
59
|
-
# value = row_values[i]
|
|
60
|
-
# export_style = option.dig(:export, :xlsx_style)
|
|
61
|
-
#
|
|
62
|
-
# if export_style.present?
|
|
63
|
-
# wb.styles.add_style(export_style)
|
|
64
|
-
# elsif custom_styles[type].present?
|
|
65
|
-
# wb.styles.add_style(custom_styles[type])
|
|
66
|
-
# elsif [:integer, :float, :decimal, :number].include?(type)
|
|
67
|
-
# number_format
|
|
68
|
-
# elsif [:datetime, :time, :timestamp].include?(type)
|
|
69
|
-
# datetime_format
|
|
70
|
-
# elsif type == :date
|
|
71
|
-
# date_format
|
|
72
|
-
# elsif value.is_a?(Axlsx::RichText) || type == :text
|
|
73
|
-
# wrap_text
|
|
74
|
-
# else
|
|
75
|
-
# nowrap_text
|
|
76
|
-
# end
|
|
77
|
-
# }
|
|
78
|
-
values = table.display_columns.map { |column| row.value(column) }
|
|
79
|
-
row_styles = table.display_columns.map { |column| nowrap_text }
|
|
80
|
-
last_row = sheet.add_row(values, style: row_styles)
|
|
81
|
-
end
|
|
45
|
+
if export.table_view_id.present?
|
|
46
|
+
view = Mensa::TableView.find_by(table_name: export.table_name, id: export.table_view_id)
|
|
47
|
+
config = view.config.deep_symbolize_keys if view&.config
|
|
48
|
+
end
|
|
82
49
|
|
|
83
|
-
|
|
50
|
+
config = config.merge((export.config.compact_blank || {}).deep_symbolize_keys)
|
|
51
|
+
table = Mensa.for_name(export.table_name, config)
|
|
52
|
+
table.request = ActionDispatch::Request.new({})
|
|
53
|
+
table.request.set_header("action_dispatch.request.query_parameters", {"page" => config[:page]})
|
|
54
|
+
table
|
|
55
|
+
end
|
|
84
56
|
|
|
85
|
-
|
|
86
|
-
|
|
57
|
+
def generate(table, export)
|
|
58
|
+
io = StringIO.new
|
|
59
|
+
# A UTF-8 BOM makes spreadsheet programs such as Excel detect the encoding
|
|
60
|
+
# correctly. The "plain" CSV variant omits it for maximum compatibility
|
|
61
|
+
# with programmatic consumers.
|
|
62
|
+
io.write("\uFEFF") if export.format == "csv_excel"
|
|
63
|
+
|
|
64
|
+
csv = CSV.new(io)
|
|
65
|
+
csv << table.display_columns.map(&:name)
|
|
66
|
+
export_rows(table, export).each do |row|
|
|
67
|
+
csv << table.display_columns.map { |column| Mensa::Cell.new(row: row, column: column).render(:csv) }
|
|
87
68
|
end
|
|
69
|
+
io.rewind
|
|
70
|
+
data = io.read
|
|
71
|
+
|
|
72
|
+
base_filename = "#{export.table_name}_export_#{export.created_at.strftime("%Y-%m-%d-%H%M%S")}"
|
|
88
73
|
|
|
89
|
-
base_filename = "#{table_name}_export_#{Time.current.strftime("%Y-%m-%d-%H:%M:%S")}"
|
|
90
|
-
enc = nil
|
|
91
|
-
password = nil
|
|
92
74
|
if table.export_with_password?
|
|
75
|
+
require "zip"
|
|
93
76
|
password = SecureRandom.hex(6)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
77
|
+
encrypter = Zip::TraditionalEncrypter.new(password)
|
|
78
|
+
zip_io = Zip::OutputStream.write_buffer(encrypter: encrypter) do |zio|
|
|
79
|
+
zio.put_next_entry("#{base_filename}.csv")
|
|
80
|
+
zio.write data
|
|
81
|
+
end
|
|
82
|
+
zip_io.rewind
|
|
83
|
+
[zip_io.read, "#{base_filename}.zip", "application/zip"]
|
|
84
|
+
else
|
|
85
|
+
[data, "#{base_filename}.csv", "text/csv"]
|
|
99
86
|
end
|
|
100
|
-
|
|
87
|
+
end
|
|
101
88
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
end
|
|
89
|
+
def export_rows(table, export)
|
|
90
|
+
scope = (export.scope == "current_page") ? table.paged_scope : table.selected_scope
|
|
91
|
+
scope.map { |row| Mensa::Row.new(table, row) }
|
|
92
|
+
end
|
|
107
93
|
|
|
108
|
-
|
|
94
|
+
def finalize(export, status:, filename: nil)
|
|
95
|
+
attributes = {status: status}
|
|
96
|
+
attributes[:filename] = filename if filename
|
|
97
|
+
export.update(attributes)
|
|
98
|
+
# Refresh the export button badge (download count) and the downloads list
|
|
99
|
+
# inside the export dialog for everyone viewing this table.
|
|
100
|
+
Mensa::Export.broadcast_refresh(export.table_name, export.user)
|
|
109
101
|
end
|
|
110
102
|
end
|
|
111
103
|
end
|