mensa 0.6.2 → 0.6.3
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/Gemfile.lock +1 -1
- data/app/components/mensa/control_bar/component.css +12 -0
- data/app/components/mensa/copyable/component_controller.js +32 -0
- data/app/javascript/mensa/controllers/index.js +3 -0
- data/app/jobs/mensa/export_job.rb +52 -22
- data/app/tables/mensa/filter.rb +8 -1
- data/app/views/mensa/exports/_list.html.erb +9 -0
- data/config/locales/en.yml +1 -0
- data/config/locales/nl.yml +1 -0
- data/db/migrate/20260616113603_add_password_to_export.rb +5 -0
- data/lib/mensa/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6c3d12b5f69c5a0893eae56ffda155d75a658dc46620aa18cbd379d0773e16e
|
|
4
|
+
data.tar.gz: 9e26900083f2aba091e70e5cbcc695f575432c0705b2cc6cda56df78b2d97ed3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5edc68c51ba784fb05689a538fdeec88dccfaeadee54df43a10af537700b442eb16344848a60fa7b8da20ed4c4dde8754e3c2a9bad3848ba71ed255e2598f07
|
|
7
|
+
data.tar.gz: 327b344e58309db72a11b1d90c8d8eae950b92571434144c3f237e6aa4161ac5eb9221bb3d33609f883e1a38bd6a6c4f0eb5a11a564adf79890fe9f69083ae25
|
data/Gemfile.lock
CHANGED
|
@@ -89,6 +89,18 @@
|
|
|
89
89
|
@apply text-xs font-normal text-gray-400 dark:text-gray-500;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
&__item-password {
|
|
93
|
+
@apply inline-flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&__item-password-value {
|
|
97
|
+
@apply rounded bg-gray-100 dark:bg-gray-700 px-1 py-0.5 font-mono text-[0.6875rem] text-gray-700 dark:text-gray-200;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&__item-password-copy {
|
|
101
|
+
@apply inline-flex h-5 w-5 items-center justify-center rounded text-gray-400 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 cursor-pointer;
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
&__download {
|
|
93
105
|
@apply inline-flex items-center gap-1.5 rounded-md bg-white dark:bg-gray-700 px-2.5 py-1.5 text-sm font-medium text-primary-600 dark:text-primary-300 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600;
|
|
94
106
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import ApplicationController from "mensa/controllers/application_controller";
|
|
2
|
+
|
|
3
|
+
export default class CopyableComponentController extends ApplicationController {
|
|
4
|
+
static values = {
|
|
5
|
+
text: String,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
async copy(event) {
|
|
9
|
+
event.preventDefault();
|
|
10
|
+
|
|
11
|
+
if (!this.hasTextValue) return;
|
|
12
|
+
|
|
13
|
+
if (navigator.clipboard?.writeText) {
|
|
14
|
+
await navigator.clipboard.writeText(this.textValue);
|
|
15
|
+
} else {
|
|
16
|
+
this._copyWithFallback(this.textValue);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_copyWithFallback(text) {
|
|
21
|
+
const input = document.createElement("textarea");
|
|
22
|
+
input.value = text;
|
|
23
|
+
input.setAttribute("readonly", "");
|
|
24
|
+
input.style.position = "fixed";
|
|
25
|
+
input.style.top = "-9999px";
|
|
26
|
+
input.style.left = "-9999px";
|
|
27
|
+
document.body.appendChild(input);
|
|
28
|
+
input.select();
|
|
29
|
+
document.execCommand("copy");
|
|
30
|
+
input.remove();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -30,6 +30,9 @@ application.register("mensa-selection", SelectionComponentController);
|
|
|
30
30
|
import ColumnCustomizerController from "mensa/components/column_customizer/component_controller";
|
|
31
31
|
application.register("mensa-column-customizer", ColumnCustomizerController);
|
|
32
32
|
|
|
33
|
+
import CopyableComponentController from "mensa/components/copyable/component_controller";
|
|
34
|
+
application.register("mensa-copyable", CopyableComponentController);
|
|
35
|
+
|
|
33
36
|
// Eager load all controllers defined in the import map under controllers/**/*_controller
|
|
34
37
|
// import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
35
38
|
// eagerLoadControllersFrom("controllers", application)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require "csv"
|
|
2
2
|
require "securerandom"
|
|
3
|
-
require "
|
|
3
|
+
require "tempfile"
|
|
4
4
|
|
|
5
5
|
module Mensa
|
|
6
6
|
# Generates the CSV for a Mensa::Export, attaches it to the export's +asset+
|
|
@@ -21,17 +21,19 @@ module Mensa
|
|
|
21
21
|
return
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
tempfile, filename, content_type = generate(table, export)
|
|
25
25
|
|
|
26
26
|
export.asset.purge if export.asset.attached?
|
|
27
|
-
export.asset.attach(io:
|
|
27
|
+
export.asset.attach(io: tempfile, filename: filename, content_type: content_type)
|
|
28
28
|
finalize(export, status: "completed", filename: filename)
|
|
29
29
|
|
|
30
30
|
Mensa.config.callbacks[:export_complete]&.call(export)
|
|
31
31
|
rescue => e
|
|
32
|
-
Mensa.config.logger&.error("Mensa::ExportJob failed for export #{
|
|
32
|
+
Mensa.config.logger&.error("Mensa::ExportJob failed for export #{export&.id}: #{e.class}: #{e.message}")
|
|
33
33
|
finalize(export, status: "failed") if export
|
|
34
34
|
raise
|
|
35
|
+
ensure
|
|
36
|
+
tempfile&.close!
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
private
|
|
@@ -56,35 +58,63 @@ module Mensa
|
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
def generate(table, export)
|
|
59
|
-
|
|
61
|
+
base_filename = "#{export.table_name}_export_#{export.created_at.strftime("%Y-%m-%d-%H%M%S")}"
|
|
62
|
+
csv_file = write_csv_file(table, export, base_filename)
|
|
63
|
+
|
|
64
|
+
if table.export_with_password?
|
|
65
|
+
zip_file = write_zip_file(csv_file, export, base_filename)
|
|
66
|
+
csv_file.close!
|
|
67
|
+
[zip_file, "#{base_filename}.zip", "application/zip"]
|
|
68
|
+
else
|
|
69
|
+
[csv_file, "#{base_filename}.csv", "text/csv"]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def write_csv_file(table, export, base_filename)
|
|
74
|
+
tempfile = Tempfile.new([base_filename, ".csv"], binmode: true)
|
|
75
|
+
|
|
60
76
|
# A UTF-8 BOM makes spreadsheet programs such as Excel detect the encoding
|
|
61
77
|
# correctly. The "plain" CSV variant omits it for maximum compatibility
|
|
62
78
|
# with programmatic consumers.
|
|
63
|
-
|
|
79
|
+
tempfile.write("\uFEFF") if export.format == "csv_excel"
|
|
64
80
|
|
|
65
|
-
csv = CSV.new(
|
|
81
|
+
csv = CSV.new(tempfile)
|
|
66
82
|
csv << table.display_columns.map(&:name)
|
|
67
83
|
export_rows(table, export).each do |row|
|
|
68
84
|
csv << table.display_columns.map { |column| Mensa::Cell.new(row: row, column: column).render(:csv) }
|
|
69
85
|
end
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
csv.close
|
|
87
|
+
tempfile.open
|
|
88
|
+
tempfile.binmode
|
|
89
|
+
tempfile.rewind
|
|
90
|
+
tempfile
|
|
91
|
+
rescue
|
|
92
|
+
tempfile&.close!
|
|
93
|
+
raise
|
|
94
|
+
end
|
|
72
95
|
|
|
73
|
-
|
|
96
|
+
def write_zip_file(csv_file, export, base_filename)
|
|
97
|
+
require "zip"
|
|
74
98
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
else
|
|
86
|
-
[data, "#{base_filename}.csv", "text/csv"]
|
|
99
|
+
zip_file = Tempfile.new([base_filename, ".zip"], binmode: true)
|
|
100
|
+
zip_path = zip_file.path
|
|
101
|
+
zip_file.close
|
|
102
|
+
|
|
103
|
+
export.password = SecureRandom.hex(6)
|
|
104
|
+
encrypter = Zip::TraditionalEncrypter.new(export.password)
|
|
105
|
+
Zip::OutputStream.open(zip_path, encrypter: encrypter) do |zio|
|
|
106
|
+
zio.put_next_entry("#{base_filename}.csv")
|
|
107
|
+
csv_file.rewind
|
|
108
|
+
IO.copy_stream(csv_file, zio)
|
|
87
109
|
end
|
|
110
|
+
|
|
111
|
+
zip_file.open
|
|
112
|
+
zip_file.binmode
|
|
113
|
+
zip_file.rewind
|
|
114
|
+
zip_file
|
|
115
|
+
rescue
|
|
116
|
+
zip_file&.close!
|
|
117
|
+
raise
|
|
88
118
|
end
|
|
89
119
|
|
|
90
120
|
def export_rows(table, export)
|
data/app/tables/mensa/filter.rb
CHANGED
|
@@ -25,7 +25,8 @@ module Mensa
|
|
|
25
25
|
[:lt, I18n.t("mensa.operators.lt"), true],
|
|
26
26
|
[:lteq, I18n.t("mensa.operators.lteq"), true],
|
|
27
27
|
[:is_current, I18n.t("mensa.operators.is_current"), false],
|
|
28
|
-
[:is_empty, I18n.t("mensa.operators.is_empty"), false]
|
|
28
|
+
[:is_empty, I18n.t("mensa.operators.is_empty"), false],
|
|
29
|
+
[:isnt_empty, I18n.t("mensa.operators.isnt_empty"), false]
|
|
29
30
|
].freeze
|
|
30
31
|
end
|
|
31
32
|
end
|
|
@@ -85,6 +86,12 @@ module Mensa
|
|
|
85
86
|
else
|
|
86
87
|
record_scope.where("#{column.attribute_for_condition} IS NULL")
|
|
87
88
|
end
|
|
89
|
+
when :isnt_empty
|
|
90
|
+
if column.type == :string
|
|
91
|
+
record_scope.where("#{column.attribute_for_condition} IS NOT NULL AND #{column.attribute_for_condition} != ''")
|
|
92
|
+
else
|
|
93
|
+
record_scope.where("#{column.attribute_for_condition} IS NOT NULL")
|
|
94
|
+
end
|
|
88
95
|
when :is_current
|
|
89
96
|
record_scope.where("#{column.attribute_for_condition} = ?", Current.send(column.name))
|
|
90
97
|
when :matches
|
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
<% if export.repeating? %>
|
|
20
20
|
<span class="mensa-table__export-dialog__item-repeat"><%= export.repeat_label %></span>
|
|
21
21
|
<% end %>
|
|
22
|
+
<% if export.password.present? %>
|
|
23
|
+
<span class="mensa-table__export-dialog__item-password" data-controller="mensa-copyable" data-mensa-copyable-text-value="<%= export.password %>">
|
|
24
|
+
<span class="mensa-table__export-dialog__item-password-label">Password:</span>
|
|
25
|
+
<code class="mensa-table__export-dialog__item-password-value"><%= export.password %></code>
|
|
26
|
+
<button class="mensa-table__export-dialog__item-password-copy" type="button" data-action="mensa-copyable#copy" aria-label="Copy password">
|
|
27
|
+
<i class="fas fa-copy" aria-hidden="true"></i>
|
|
28
|
+
</button>
|
|
29
|
+
</span>
|
|
30
|
+
<% end %>
|
|
22
31
|
<span class="mensa-table__export-dialog__item-meta"><%= export.created_at.strftime("%Y-%m-%d %H:%M") %></span>
|
|
23
32
|
</div>
|
|
24
33
|
<div class="mensa-table__export-dialog__item-action">
|
data/config/locales/en.yml
CHANGED
data/config/locales/nl.yml
CHANGED
data/lib/mensa/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mensa
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom de Grunt
|
|
@@ -193,6 +193,7 @@ files:
|
|
|
193
193
|
- app/components/mensa/control_bar/component.css
|
|
194
194
|
- app/components/mensa/control_bar/component.html.erb
|
|
195
195
|
- app/components/mensa/control_bar/component.rb
|
|
196
|
+
- app/components/mensa/copyable/component_controller.js
|
|
196
197
|
- app/components/mensa/empty_state/component.css
|
|
197
198
|
- app/components/mensa/empty_state/component.html.erb
|
|
198
199
|
- app/components/mensa/empty_state/component.rb
|
|
@@ -294,6 +295,7 @@ files:
|
|
|
294
295
|
- db/migrate/20251112143558_add_description_to_table_view.rb
|
|
295
296
|
- db/migrate/20260604120000_create_mensa_exports.rb
|
|
296
297
|
- db/migrate/20260612110000_add_repeat_to_mensa_exports.rb
|
|
298
|
+
- db/migrate/20260616113603_add_password_to_export.rb
|
|
297
299
|
- docs/columns.png
|
|
298
300
|
- docs/export.png
|
|
299
301
|
- docs/filters.png
|