inline_forms 8.1.47 → 8.1.55
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/.gitignore +3 -0
- data/CHANGELOG.md +178 -0
- data/Gemfile +3 -0
- data/README.rdoc +47 -0
- data/app/assets/stylesheets/inline_forms/inline_forms.scss +34 -0
- data/app/controllers/concerns/file_slots_concern.rb +211 -0
- data/app/controllers/inline_forms/file_trash_controller.rb +115 -0
- data/app/controllers/inline_forms_controller.rb +82 -16
- data/app/helpers/inline_forms_file_trash_helper.rb +131 -0
- data/app/helpers/inline_forms_helper.rb +5 -1
- data/app/models/concerns/inline_forms/stored_files.rb +350 -0
- data/app/models/inline_forms/current.rb +8 -0
- data/app/models/inline_forms/file_trash_sweep.rb +37 -0
- data/app/models/inline_forms/trashed_file.rb +205 -0
- data/app/views/devise/passwords/edit.html.erb +1 -1
- data/app/views/devise/passwords/new.html.erb +1 -1
- data/app/views/devise/shared/_header_and_errors.html.erb +9 -2
- data/app/views/inline_forms/_file_slot.html.erb +39 -0
- data/app/views/inline_forms/_header.html.erb +3 -0
- data/app/views/inline_forms/_model_top_bar.html.erb +3 -4
- data/app/views/inline_forms/_show.html.erb +6 -0
- data/app/views/inline_forms/_versions.html.erb +4 -4
- data/app/views/inline_forms/_versions_file_entry.html.erb +36 -0
- data/app/views/inline_forms/_versions_list.html.erb +11 -2
- data/app/views/inline_forms/file_field_frame.html.erb +22 -0
- data/app/views/inline_forms/file_trash/index.html.erb +112 -0
- data/app/views/inline_forms/file_trash_panel.html.erb +90 -0
- data/app/views/layouts/inline_forms.html.erb +5 -1
- data/lib/generators/inline_forms/file_trash_generator.rb +36 -0
- data/lib/generators/inline_forms/templates/create_inline_forms_file_trash.rb.erb +41 -0
- data/lib/generators/inline_forms_addto_generator.rb +47 -0
- data/lib/generators/inline_forms_generator.rb +19 -2
- data/lib/generators/templates/model.erb +4 -0
- data/lib/inline_forms/form_element_from_callee.rb +25 -0
- data/lib/inline_forms/form_elements/simple_file_field_helper.rb +30 -2
- data/lib/inline_forms/gem_files.rb +37 -0
- data/lib/inline_forms/version.rb +1 -1
- data/lib/inline_forms.rb +55 -0
- data/lib/locales/inline_forms.en.yml +87 -0
- data/lib/locales/inline_forms.nl.yml +87 -3
- data/lib/tasks/inline_forms_files.rake +61 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/controllers/documents_controller.rb +4 -0
- data/test/dummy/app/controllers/dossiers_controller.rb +4 -0
- data/test/dummy/app/models/ability.rb +15 -0
- data/test/dummy/app/models/document.rb +33 -0
- data/test/dummy/app/models/dossier.rb +38 -0
- data/test/dummy/config/application.rb +4 -3
- data/test/dummy/config/routes.rb +13 -0
- data/test/gem_package_modes_test.rb +86 -0
- data/test/inline_forms_addto_generator_test.rb +47 -0
- data/test/inline_forms_generator_test.rb +24 -0
- data/test/integration/field_authorization_test.rb +131 -0
- data/test/integration/file_slots_test.rb +574 -0
- data/test/integration/layout_title_test.rb +56 -0
- data/test/integration/model_top_bar_test.rb +9 -0
- data/test/integration/revert_skipped_columns_test.rb +59 -0
- data/test/integration_test_helper.rb +62 -0
- metadata +36 -3
- data/.claude/settings.local.json +0 -9
- data/CLAUDE.md +0 -68
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<%# Per-record trash panel ("Removed files (n)"), beside Versions. Lists the
|
|
2
|
+
trash rows of the slots shown on this page (a Client tab only its own),
|
|
3
|
+
newest first; restored and purged rows stay as greyed history. The frame
|
|
4
|
+
is always rendered, empty or not, so a file action can stream into it.
|
|
5
|
+
Never loads file bytes (without_data). -%>
|
|
6
|
+
<% object = local_assigns[:object] || @object -%>
|
|
7
|
+
<% update_span = local_assigns[:update_span] || @update_span -%>
|
|
8
|
+
<% slots = local_assigns.key?(:slots) ? local_assigns[:slots] : @file_trash_slots -%>
|
|
9
|
+
<% open = local_assigns.key?(:open) ? local_assigns[:open] : @file_trash_open -%>
|
|
10
|
+
<% slots = Array(slots) -%>
|
|
11
|
+
<% entries = slots.empty? ? [] : InlineForms::TrashedFile.without_data.trash_entries.for_record(object).where(attribute_name: slots).newest_first.to_a -%>
|
|
12
|
+
<% slots_param = slots.join(",") -%>
|
|
13
|
+
<turbo-frame id="<%= update_span %>">
|
|
14
|
+
<% if entries.any? %>
|
|
15
|
+
<% in_trash = entries.count(&:trashed?) -%>
|
|
16
|
+
<% toggle_path = polymorphic_path(object, action: :file_trash, update: update_span, slots: slots_param, close: (open ? true : nil)) -%>
|
|
17
|
+
<div class="row form_element_header associated_auto_header callout inline_forms-file-trash">
|
|
18
|
+
<div class='medium-11 large-11 column' >
|
|
19
|
+
<%= t("inline_forms.files.trash_header", count: in_trash) %>
|
|
20
|
+
</div>
|
|
21
|
+
<div class='medium-1 large-1 column'>
|
|
22
|
+
<%= inline_forms_file_icon_link (open ? "fi-x" : "fi-list"),
|
|
23
|
+
(open ? t("inline_forms.files.close_trash") : t("inline_forms.files.open_trash")),
|
|
24
|
+
toggle_path,
|
|
25
|
+
{ class: "button #{open ? 'close_button' : 'new_button'}", data: { turbo: true, turbo_frame: update_span } } %>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<% if open %>
|
|
29
|
+
<% if (stale = entries.select(&:expired?).reject { |e| e.purged? || e.restored? }).any? %>
|
|
30
|
+
<div class="row"><div class="small-12 column inline_forms-file-meta"><%= t("inline_forms.files.expired_pending", count: stale.size) %></div></div>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% entries.each do |entry| %>
|
|
33
|
+
<% attribute = entry.attribute_name -%>
|
|
34
|
+
<% field_frame = inline_forms_file_field_frame_id(object, attribute) -%>
|
|
35
|
+
<div class="row <%= cycle('odd', 'even') %> inline_forms-file-trash-row<%= ' inline_forms-file-trash-history' unless entry.trashed? %>">
|
|
36
|
+
<div class="small-12 medium-3 column">
|
|
37
|
+
<strong><%= object.class.human_attribute_name(attribute) %></strong>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="small-12 medium-5 column">
|
|
40
|
+
<span class="inline_forms-file-name"><%= inline_forms_file_entry_name(entry) %></span>
|
|
41
|
+
<span class="inline_forms-file-meta">
|
|
42
|
+
<%= inline_forms_file_size(entry.byte_size) %> ·
|
|
43
|
+
<%= t("inline_forms.files.reason.#{entry.reason}") %>
|
|
44
|
+
<%= t("inline_forms.files.by_on", user: inline_forms_file_user_name(entry.trashed_by_id), at: l(entry.trashed_at, format: :short)) %>
|
|
45
|
+
</span>
|
|
46
|
+
<span class="inline_forms-file-meta">
|
|
47
|
+
<% if entry.state == :restored %>
|
|
48
|
+
<%= t("inline_forms.files.state_restored", user: inline_forms_file_user_name(entry.restored_by_id), at: l(entry.restored_at, format: :short)) %>
|
|
49
|
+
<% elsif entry.state == :purged %>
|
|
50
|
+
<%= t("inline_forms.files.state_purged", user: inline_forms_file_user_name(entry.purged_by_id), at: l(entry.purged_at, format: :short)) %>
|
|
51
|
+
<% elsif entry.state == :expired %>
|
|
52
|
+
<%= t("inline_forms.files.state_expired") %>
|
|
53
|
+
<% else %>
|
|
54
|
+
<% if entry.purge_after %>
|
|
55
|
+
<% days_left = ((entry.purge_after - Time.current) / 1.day).ceil -%>
|
|
56
|
+
<span class="<%= 'inline_forms-file-expiring' if days_left <= 7 %>"><%= t("inline_forms.files.expires_in", count: days_left, date: l(entry.purge_after.to_date)) %></span>
|
|
57
|
+
<% end %>
|
|
58
|
+
<% end %>
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="small-12 medium-4 column inline_forms-file-actions">
|
|
62
|
+
<% if entry.trashed? %>
|
|
63
|
+
<% if inline_forms_file_can?(:download_trashed_file, object, attribute) %>
|
|
64
|
+
<%= link_to t("inline_forms.files.download"),
|
|
65
|
+
polymorphic_path(object, action: :trashed_file, attribute: attribute, trash_id: entry.id),
|
|
66
|
+
data: { turbo: false } %>
|
|
67
|
+
<% end %>
|
|
68
|
+
<% if inline_forms_file_can?(:restore_file, object, attribute) %>
|
|
69
|
+
<% occupied = object.inline_forms_file_present?(attribute) -%>
|
|
70
|
+
<% if inline_forms_file_can?(occupied ? :replace_file : :update, object, attribute) %>
|
|
71
|
+
<% confirm = occupied ? t("inline_forms.files.confirm_restore_swap", current: object[attribute], filename: inline_forms_file_entry_name(entry)) : nil -%>
|
|
72
|
+
<%= link_to t("inline_forms.files.restore"),
|
|
73
|
+
polymorphic_path(object, action: :restore_file, attribute: attribute, trash_id: entry.id,
|
|
74
|
+
update: field_frame, slots: slots_param, panel: "open"),
|
|
75
|
+
inline_forms_file_post_data(update_span, confirm: confirm) %>
|
|
76
|
+
<% end %>
|
|
77
|
+
<% end %>
|
|
78
|
+
<% if inline_forms_file_can?(:purge_file, object, attribute) %>
|
|
79
|
+
<%= link_to t("inline_forms.files.purge"),
|
|
80
|
+
polymorphic_path(object, action: :purge_file, attribute: attribute, trash_id: entry.id,
|
|
81
|
+
update: field_frame, slots: slots_param, panel: "open"),
|
|
82
|
+
inline_forms_file_post_data(update_span, confirm: t("inline_forms.files.confirm_purge", filename: inline_forms_file_entry_name(entry))).deep_merge(class: "alert") %>
|
|
83
|
+
<% end %>
|
|
84
|
+
<% end %>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<% end %>
|
|
88
|
+
<% end %>
|
|
89
|
+
<% end %>
|
|
90
|
+
</turbo-frame>
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title
|
|
6
|
+
<%# A missing translation must not reach <title>: a bare t() returns an %>
|
|
7
|
+
<%# html_safe <span class="translation_missing"> that shows up as markup. %>
|
|
8
|
+
<%# The `default:` (the host's ApplicationHelper#application_name, which %>
|
|
9
|
+
<%# the header partial needs anyway) makes t() return plain, escaped text. %>
|
|
10
|
+
<title><%= t('application_name', default: application_name) %> v<%= InlineForms::VERSION %></title>
|
|
7
11
|
<%= stylesheet_link_tag "application" %>
|
|
8
12
|
<%= stylesheet_link_tag "inline_forms_fonts" %>
|
|
9
13
|
<%= stylesheet_link_tag "inline_forms/inline_forms" %>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
|
|
6
|
+
module InlineForms
|
|
7
|
+
module Generators
|
|
8
|
+
# `rails g inline_forms:file_trash`
|
|
9
|
+
#
|
|
10
|
+
# Writes the migration for the file trash of declared file slots
|
|
11
|
+
# (InlineForms::StoredFiles): inline_forms_trashed_files (one row per
|
|
12
|
+
# removed / replaced / uploaded file event, bytes until purged) and
|
|
13
|
+
# inline_forms_file_trash_sweeps (the retention sweep's heartbeat). The
|
|
14
|
+
# installer runs it for new apps; existing apps run it once by hand.
|
|
15
|
+
class FileTrashGenerator < Rails::Generators::Base
|
|
16
|
+
include Rails::Generators::Migration
|
|
17
|
+
|
|
18
|
+
source_root File.expand_path("templates", __dir__)
|
|
19
|
+
|
|
20
|
+
def self.next_migration_number(dirname)
|
|
21
|
+
# Collision-free against migrations generated in the same second
|
|
22
|
+
# (same policy as inline_forms_addto since 8.1.41).
|
|
23
|
+
now = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
24
|
+
existing = Dir.glob(File.join(dirname, "*.rb")).map { |f| File.basename(f)[/\A\d+/] }.compact
|
|
25
|
+
[ now, *existing.map(&:to_s) ].max.then do |highest|
|
|
26
|
+
highest == now ? now : (highest.to_i + 1).to_s
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_migration_file
|
|
31
|
+
migration_template "create_inline_forms_file_trash.rb.erb",
|
|
32
|
+
"db/migrate/create_inline_forms_file_trash.rb"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# File trash for declared file slots (inline_forms, InlineForms::StoredFiles).
|
|
2
|
+
class CreateInlineFormsFileTrash < ActiveRecord::Migration[8.1]
|
|
3
|
+
def change
|
|
4
|
+
# Trashed bytes can be as large as the slot's own column: LONGBLOB on
|
|
5
|
+
# MySQL (a plain BLOB stops at 64 KB); a plain binary elsewhere (SQLite
|
|
6
|
+
# and PostgreSQL have no size variants).
|
|
7
|
+
long_blob = connection.adapter_name.match?(/mysql|trilogy/i) ? { size: :long } : {}
|
|
8
|
+
|
|
9
|
+
create_table :inline_forms_trashed_files do |t|
|
|
10
|
+
t.string :record_type, null: false
|
|
11
|
+
t.bigint :record_id, null: false
|
|
12
|
+
t.string :attribute_name, null: false
|
|
13
|
+
t.string :filename
|
|
14
|
+
t.string :content_type
|
|
15
|
+
t.bigint :byte_size
|
|
16
|
+
t.string :checksum, limit: 64
|
|
17
|
+
t.binary :data, **long_blob
|
|
18
|
+
t.string :reason, null: false
|
|
19
|
+
t.boolean :redacted, null: false, default: false
|
|
20
|
+
t.bigint :trashed_by_id
|
|
21
|
+
t.datetime :trashed_at, null: false
|
|
22
|
+
t.bigint :restored_by_id
|
|
23
|
+
t.datetime :restored_at
|
|
24
|
+
t.bigint :purged_by_id
|
|
25
|
+
t.datetime :purged_at
|
|
26
|
+
t.datetime :purge_after
|
|
27
|
+
t.timestamps
|
|
28
|
+
end
|
|
29
|
+
add_index :inline_forms_trashed_files, [ :record_type, :record_id, :attribute_name ],
|
|
30
|
+
name: "index_inline_forms_trashed_files_on_slot"
|
|
31
|
+
add_index :inline_forms_trashed_files, [ :purged_at, :restored_at, :purge_after ],
|
|
32
|
+
name: "index_inline_forms_trashed_files_on_state"
|
|
33
|
+
|
|
34
|
+
create_table :inline_forms_file_trash_sweeps do |t|
|
|
35
|
+
t.datetime :ran_at, null: false
|
|
36
|
+
t.integer :purged_count, null: false, default: 0
|
|
37
|
+
t.text :error
|
|
38
|
+
t.timestamps
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -117,6 +117,12 @@ class InlineFormsAddtoGenerator < Rails::Generators::NamedBase
|
|
|
117
117
|
|
|
118
118
|
if attribute.column_type == :belongs_to
|
|
119
119
|
@migration_lines << " add_reference :#{table_name}, :#{attribute.name}, foreign_key: true\n"
|
|
120
|
+
elsif attribute.type == :simple_file_field
|
|
121
|
+
# Declared file slot: filename + bytes (LONGBLOB on MySQL) + content type.
|
|
122
|
+
columns = InlineForms.file_slot_columns(attribute.name)
|
|
123
|
+
@migration_lines << " add_column :#{table_name}, :#{columns[:filename]}, :string\n"
|
|
124
|
+
@migration_lines << " add_column :#{table_name}, :#{columns[:data]}, :binary, **(connection.adapter_name.match?(/mysql|trilogy/i) ? { size: :long } : {})\n"
|
|
125
|
+
@migration_lines << " add_column :#{table_name}, :#{columns[:content_type]}, :string\n"
|
|
120
126
|
else
|
|
121
127
|
commenter = attribute.attribute_type == :unknown ? "#" : " "
|
|
122
128
|
@migration_lines << "#{commenter} add_column :#{table_name}, :#{attribute.name}, :#{attribute.column_type}\n"
|
|
@@ -158,6 +164,8 @@ class InlineFormsAddtoGenerator < Rails::Generators::NamedBase
|
|
|
158
164
|
inject_class_line!(" has_and_belongs_to_many :#{attribute.name}\n")
|
|
159
165
|
when :rich_text
|
|
160
166
|
inject_class_line!(" has_rich_text :#{attribute.name}\n")
|
|
167
|
+
when :simple_file_field
|
|
168
|
+
add_file_slot!(attribute)
|
|
161
169
|
end
|
|
162
170
|
|
|
163
171
|
next unless attribute.attribute?
|
|
@@ -182,6 +190,45 @@ class InlineFormsAddtoGenerator < Rails::Generators::NamedBase
|
|
|
182
190
|
|
|
183
191
|
private
|
|
184
192
|
|
|
193
|
+
# A :simple_file_field is a declared file slot: the model gets
|
|
194
|
+
# InlineForms::StoredFiles + `inline_forms_file :attr`, and the resource
|
|
195
|
+
# block in routes.rb gets InlineForms.file_routes(self) (when it can be
|
|
196
|
+
# found; otherwise a reminder).
|
|
197
|
+
def add_file_slot!(attribute)
|
|
198
|
+
content = File.read(File.join(destination_root, model_file_path))
|
|
199
|
+
include_line = " include InlineForms::StoredFiles\n"
|
|
200
|
+
macro = " inline_forms_file :#{attribute.name}\n"
|
|
201
|
+
return if content.include?(macro)
|
|
202
|
+
|
|
203
|
+
if content.include?(include_line)
|
|
204
|
+
inject_into_file model_file_path, macro, after: include_line
|
|
205
|
+
else
|
|
206
|
+
# One injection: inject_into_class puts each line right under the
|
|
207
|
+
# class line, so two calls would land the macro above the include.
|
|
208
|
+
inject_class_line!(include_line + macro)
|
|
209
|
+
end
|
|
210
|
+
add_file_routes!
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def add_file_routes!
|
|
214
|
+
routes = "config/routes.rb"
|
|
215
|
+
path = File.join(destination_root, routes)
|
|
216
|
+
return unless File.exist?(path)
|
|
217
|
+
|
|
218
|
+
content = File.read(path)
|
|
219
|
+
opener = content[/^(\s*)resources :#{table_name} do\n/]
|
|
220
|
+
if opener.nil?
|
|
221
|
+
say_status :reminder, "add InlineForms.file_routes(self) inside `resources :#{table_name} do ... end` in #{routes}", :yellow
|
|
222
|
+
return
|
|
223
|
+
end
|
|
224
|
+
block = content[content.index(opener)..][/\A.*?^\s*end$/m]
|
|
225
|
+
return if block&.include?("InlineForms.file_routes")
|
|
226
|
+
|
|
227
|
+
indent = opener[/\A\s*/]
|
|
228
|
+
inject_into_file routes, "#{indent} InlineForms.file_routes(self)\n", after: opener
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
|
|
185
232
|
def model_file_name
|
|
186
233
|
name.underscore
|
|
187
234
|
end
|
|
@@ -66,6 +66,7 @@ module InlineForms
|
|
|
66
66
|
@order = "\n"
|
|
67
67
|
@list_scopes = ""
|
|
68
68
|
@carrierwave_mounters = "\n"
|
|
69
|
+
@stored_files = String.new
|
|
69
70
|
@inline_forms_attribute_list = String.new
|
|
70
71
|
|
|
71
72
|
for attribute in attributes
|
|
@@ -94,6 +95,12 @@ module InlineForms
|
|
|
94
95
|
if attribute.type == :rich_text
|
|
95
96
|
@has_rich_text << ' has_rich_text :' + attribute.name + "\n"
|
|
96
97
|
end
|
|
98
|
+
# Declared file slot: bytes + content type in the model's own table,
|
|
99
|
+
# replace/remove/undo through the file trash (InlineForms::StoredFiles).
|
|
100
|
+
if attribute.type == :simple_file_field
|
|
101
|
+
@stored_files << " include InlineForms::StoredFiles\n" if @stored_files.empty?
|
|
102
|
+
@stored_files << ' inline_forms_file :' + attribute.name + "\n"
|
|
103
|
+
end
|
|
97
104
|
if attribute.name == '_presentation'
|
|
98
105
|
@presentation << " def _presentation\n" +
|
|
99
106
|
" \"#{attribute.type.to_s}\"\n" +
|
|
@@ -142,10 +149,11 @@ module InlineForms
|
|
|
142
149
|
|
|
143
150
|
def generate_resource_route
|
|
144
151
|
if @flag_create_resource_route
|
|
152
|
+
file_routes = file_slot_attributes.any? ? "\n InlineForms.file_routes(self)" : ""
|
|
145
153
|
route <<-ROUTE.strip_heredoc
|
|
146
154
|
resources :#{resource_name} do
|
|
147
155
|
post 'revert', :on => :member
|
|
148
|
-
get 'list_versions', :on => :member
|
|
156
|
+
get 'list_versions', :on => :member#{file_routes}
|
|
149
157
|
end
|
|
150
158
|
ROUTE
|
|
151
159
|
end
|
|
@@ -156,7 +164,12 @@ module InlineForms
|
|
|
156
164
|
@columns = String.new
|
|
157
165
|
|
|
158
166
|
for attribute in attributes
|
|
159
|
-
if attribute.
|
|
167
|
+
if attribute.type == :simple_file_field
|
|
168
|
+
columns = InlineForms.file_slot_columns(attribute.name)
|
|
169
|
+
@columns << ' t.string :' + columns[:filename] + "\n"
|
|
170
|
+
@columns << ' t.binary :' + columns[:data] + ", **(connection.adapter_name.match?(/mysql|trilogy/i) ? { size: :long } : {})\n"
|
|
171
|
+
@columns << ' t.string :' + columns[:content_type] + "\n"
|
|
172
|
+
elsif attribute.column_type == :image
|
|
160
173
|
@columns << ' t.string :' + attribute.name + "_file_name\n"
|
|
161
174
|
@columns << ' t.string :' + attribute.name + "_content_type\n"
|
|
162
175
|
@columns << ' t.integer :' + attribute.name + "_file_size\n"
|
|
@@ -230,6 +243,10 @@ module InlineForms
|
|
|
230
243
|
|
|
231
244
|
|
|
232
245
|
private
|
|
246
|
+
def file_slot_attributes
|
|
247
|
+
attributes.select { |attribute| attribute.type == :simple_file_field }
|
|
248
|
+
end
|
|
249
|
+
|
|
233
250
|
def model_file_name
|
|
234
251
|
name.underscore
|
|
235
252
|
end
|
|
@@ -6,6 +6,10 @@ class <%= name %> < ApplicationRecord
|
|
|
6
6
|
<%= @habtm if @habtm.length > 1 -%>
|
|
7
7
|
<%= @has_rich_text if @has_rich_text.length > 1 -%>
|
|
8
8
|
<%= @has_attached_files if @has_attached_files.length > 1 -%>
|
|
9
|
+
<% unless @stored_files.empty? -%>
|
|
10
|
+
|
|
11
|
+
<%= @stored_files -%>
|
|
12
|
+
<% end -%>
|
|
9
13
|
<% if @list_scopes && !@list_scopes.empty? -%>
|
|
10
14
|
|
|
11
15
|
<%= @list_scopes -%>
|
|
@@ -8,4 +8,29 @@ module InlineForms
|
|
|
8
8
|
s = s.sub(/\Ablock in /, "")
|
|
9
9
|
s.delete_suffix("_show")
|
|
10
10
|
end
|
|
11
|
+
|
|
12
|
+
# Form elements whose +*_show+ helper delegates to another element's, so
|
|
13
|
+
# their edit link carries the delegate's name (+__callee__+ of the inner
|
|
14
|
+
# helper) as +form_element+ instead of the declared one.
|
|
15
|
+
FORM_ELEMENT_DELEGATES = {
|
|
16
|
+
plain_text_area: :plain_text,
|
|
17
|
+
text_area: :rich_text
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# The +inline_forms_attribute_list+ row for a single-field request, or nil
|
|
21
|
+
# when +attribute+/+form_element+ (both from params) do not name a row the
|
|
22
|
+
# UI could have linked to. The controller uses this as a whitelist before
|
|
23
|
+
# it dispatches +send("#{form_element}_update")+ or touches the attribute.
|
|
24
|
+
def self.attribute_list_row_for(attribute_list, attribute, form_element)
|
|
25
|
+
attribute = attribute.to_s
|
|
26
|
+
form_element = form_element.to_s
|
|
27
|
+
return nil if attribute.empty? || form_element.empty?
|
|
28
|
+
|
|
29
|
+
attribute_list.find do |listed_attribute, declared, *|
|
|
30
|
+
next false unless listed_attribute.to_s == attribute
|
|
31
|
+
|
|
32
|
+
declared.to_s == form_element ||
|
|
33
|
+
FORM_ELEMENT_DELEGATES[declared.to_s.to_sym].to_s == form_element
|
|
34
|
+
end
|
|
35
|
+
end
|
|
11
36
|
end
|
|
@@ -5,7 +5,15 @@ module InlineForms
|
|
|
5
5
|
module_eval(<<~'INLINE_FORMS_FORM_ELEMENT', __FILE__, __LINE__ + 1)
|
|
6
6
|
# -*- encoding : utf-8 -*-
|
|
7
7
|
|
|
8
|
+
# Two modes. A declared file slot (InlineForms::StoredFiles + routes from
|
|
9
|
+
# InlineForms.file_routes) renders the slot partial: download through the
|
|
10
|
+
# gem, replace / remove / undo icons by permission. Anything else keeps the
|
|
11
|
+
# legacy behaviour below: a link to the host's download route named in the
|
|
12
|
+
# values hash, and an edit link only while the field is empty.
|
|
8
13
|
def simple_file_field_show(object, attribute)
|
|
14
|
+
if inline_forms_file_slot_active?(object, attribute)
|
|
15
|
+
return render(partial: "inline_forms/file_slot", locals: { object: object, attribute: attribute })
|
|
16
|
+
end
|
|
9
17
|
o = object.send(attribute)
|
|
10
18
|
attributes = @inline_forms_attribute_list || object.inline_forms_attribute_list
|
|
11
19
|
values = attributes.assoc(attribute.to_sym)[2]
|
|
@@ -21,11 +29,31 @@ module InlineForms
|
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
def simple_file_field_edit(object, attribute)
|
|
24
|
-
file_field_tag attribute, :class => 'input_text_field'
|
|
32
|
+
field = file_field_tag attribute, :class => 'input_text_field'
|
|
33
|
+
return field unless object.class.respond_to?(:inline_forms_file_slot?) && object.class.inline_forms_file_slot?(attribute)
|
|
34
|
+
|
|
35
|
+
field = file_field_tag attribute, class: 'input_text_field', required: true
|
|
36
|
+
return field unless object.inline_forms_file_present?(attribute)
|
|
37
|
+
|
|
38
|
+
# Replacing: say which file goes, and where it goes.
|
|
39
|
+
hint = t("inline_forms.files.replace_hint", filename: object[attribute.to_s])
|
|
40
|
+
safe_join([ field, content_tag(:span, hint, class: "inline_forms-file-hint") ])
|
|
25
41
|
end
|
|
26
42
|
|
|
27
43
|
def simple_file_field_update(object, attribute)
|
|
28
|
-
|
|
44
|
+
value = params[attribute.to_sym]
|
|
45
|
+
if object.class.respond_to?(:inline_forms_file_slot?) && object.class.inline_forms_file_slot?(attribute)
|
|
46
|
+
# A declared slot only takes an uploaded file from a request (a crafted
|
|
47
|
+
# string would set a filename without bytes). Nothing chosen is a
|
|
48
|
+
# validation error, not a silent success.
|
|
49
|
+
if value.respond_to?(:original_filename)
|
|
50
|
+
object.send(attribute.to_s + '=', value)
|
|
51
|
+
else
|
|
52
|
+
object.inline_forms_file_missing!(attribute)
|
|
53
|
+
end
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
object.send(attribute.to_s + '=', value)
|
|
29
57
|
end
|
|
30
58
|
|
|
31
59
|
# You need to add a route to your routes.rb file:
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
|
|
3
|
+
require "delegate"
|
|
4
|
+
require "rubygems/package"
|
|
5
|
+
|
|
3
6
|
module InlineFormsGemFiles
|
|
4
7
|
INSTALLER_FILE_PREFIXES = %w[
|
|
5
8
|
bin/inline_forms
|
|
@@ -23,8 +26,13 @@ module InlineFormsGemFiles
|
|
|
23
26
|
# ignores stuff/ is per-machine (present here, absent on the release box), so
|
|
24
27
|
# the token leaked into `gem build` on the release machine. Exclude it hard,
|
|
25
28
|
# independent of any ignore configuration.
|
|
29
|
+
#
|
|
30
|
+
# Same for local Claude Code files (untracked, per checkout): .claude/
|
|
31
|
+
# (launch.json, settings.local.json) and CLAUDE.md.
|
|
26
32
|
EXCLUDED_FILE_PREFIXES = %w[
|
|
27
33
|
stuff/
|
|
34
|
+
.claude/
|
|
35
|
+
CLAUDE.md
|
|
28
36
|
].freeze
|
|
29
37
|
|
|
30
38
|
module_function
|
|
@@ -63,4 +71,33 @@ module InlineFormsGemFiles
|
|
|
63
71
|
include_installer ? !installer_file : installer_file
|
|
64
72
|
end
|
|
65
73
|
end
|
|
74
|
+
|
|
75
|
+
# Mode a file gets inside the .gem: 0755 if the owner may execute it,
|
|
76
|
+
# else 0644. RubyGems packages each file's on-disk mode (File.lstat) and
|
|
77
|
+
# `gem install` restores it, and this checkout is group-only (rw-rw----),
|
|
78
|
+
# so 8.1.53 shipped every file as 0660: only the installing user and group
|
|
79
|
+
# could read the installed gem, `require "inline_forms"` failed silently
|
|
80
|
+
# for anyone else (Bundler swallows the LoadError), and a Docker image that
|
|
81
|
+
# installs as root and runs as another user could not boot.
|
|
82
|
+
def packaged_mode(mode)
|
|
83
|
+
(mode & 0o100).zero? ? 0o644 : 0o755
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Wraps the tar writer Gem::Package#add_files writes to, so every file
|
|
87
|
+
# goes in with packaged_mode instead of its on-disk mode.
|
|
88
|
+
class ReadableTar < SimpleDelegator
|
|
89
|
+
def add_file_simple(name, mode, size, &block)
|
|
90
|
+
__getobj__.add_file_simple(name, InlineFormsGemFiles.packaged_mode(mode), size, &block)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
module ReadablePackage
|
|
95
|
+
def add_files(tar)
|
|
96
|
+
super(ReadableTar.new(tar))
|
|
97
|
+
end
|
|
98
|
+
end
|
|
66
99
|
end
|
|
100
|
+
|
|
101
|
+
# Loaded by every gemspec in this repo, so `gem build`, `rake build` and the
|
|
102
|
+
# CI full gate all package world-readable files whatever the checkout's modes.
|
|
103
|
+
Gem::Package.prepend(InlineFormsGemFiles::ReadablePackage) unless Gem::Package <= InlineFormsGemFiles::ReadablePackage
|
data/lib/inline_forms/version.rb
CHANGED
data/lib/inline_forms.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
|
2
2
|
|
|
3
|
+
require "active_support/core_ext/module/attribute_accessors"
|
|
4
|
+
require "active_support/core_ext/numeric/time"
|
|
3
5
|
require "inline_forms/version"
|
|
4
6
|
require "inline_forms/attribute_list"
|
|
5
7
|
require "inline_forms/schema_intent"
|
|
@@ -18,6 +20,59 @@ require "inline_forms/turbo_tabs_builder"
|
|
|
18
20
|
module InlineForms
|
|
19
21
|
class PlainTextColumnMissingError < StandardError; end
|
|
20
22
|
|
|
23
|
+
# ---- File slots and the file trash (InlineForms::StoredFiles) ------------
|
|
24
|
+
#
|
|
25
|
+
# How long a removed/replaced file stays restorable in the trash before the
|
|
26
|
+
# sweep (`rake inline_forms:file_trash:purge_expired`) deletes its bytes.
|
|
27
|
+
# nil = never expire (purge by hand only).
|
|
28
|
+
mattr_accessor :file_trash_retention, default: 30.days
|
|
29
|
+
|
|
30
|
+
# Purging also blanks the stored filename (rendered as "removed file").
|
|
31
|
+
# Off by default: the filename is kept as part of the history.
|
|
32
|
+
mattr_accessor :file_trash_redact_on_purge, default: false
|
|
33
|
+
|
|
34
|
+
# Largest accepted upload for a declared file slot, in bytes; nil = no
|
|
35
|
+
# limit beyond the web server's. Enforced as a validation error.
|
|
36
|
+
mattr_accessor :max_file_size, default: nil
|
|
37
|
+
|
|
38
|
+
# Column names of a :simple_file_field slot, from its attribute: `filename`
|
|
39
|
+
# -> data / content_type; `<base>_filename` and any other `<base>` ->
|
|
40
|
+
# `<base>_data` / `<base>_content_type`. Shared by InlineForms::StoredFiles
|
|
41
|
+
# (its defaults) and the generators (the columns they create).
|
|
42
|
+
def self.file_slot_columns(attribute)
|
|
43
|
+
attribute = attribute.to_s
|
|
44
|
+
base = attribute == "filename" ? nil : attribute.delete_suffix("_filename")
|
|
45
|
+
{ filename: attribute, data: base ? "#{base}_data" : "data", content_type: base ? "#{base}_content_type" : "content_type" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Member routes for a resource whose model declares file slots. Call it
|
|
49
|
+
# inside the resource block (order-independent, unlike a routing concern):
|
|
50
|
+
#
|
|
51
|
+
# resources :clients do
|
|
52
|
+
# post 'revert', on: :member
|
|
53
|
+
# get 'list_versions', on: :member
|
|
54
|
+
# InlineForms.file_routes(self)
|
|
55
|
+
# end
|
|
56
|
+
def self.file_routes(router)
|
|
57
|
+
router.member do
|
|
58
|
+
router.get "download_file"
|
|
59
|
+
router.post "remove_file"
|
|
60
|
+
router.post "restore_file"
|
|
61
|
+
router.post "purge_file"
|
|
62
|
+
router.get "trashed_file"
|
|
63
|
+
router.get "file_trash"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The cross-record trash page (superadmin). One line in routes.rb:
|
|
68
|
+
# InlineForms.draw_file_trash_routes(self)
|
|
69
|
+
def self.draw_file_trash_routes(router)
|
|
70
|
+
router.get "file_trash", to: "inline_forms/file_trash#index", as: :inline_forms_file_trash
|
|
71
|
+
router.post "file_trash/purge", to: "inline_forms/file_trash#purge", as: :inline_forms_file_trash_purge
|
|
72
|
+
router.post "file_trash/:id/restore", to: "inline_forms/file_trash#restore", as: :inline_forms_file_trash_restore
|
|
73
|
+
router.get "file_trash/:id/download", to: "inline_forms/file_trash#download", as: :inline_forms_file_trash_download
|
|
74
|
+
end
|
|
75
|
+
|
|
21
76
|
# DEFAULT_COLUMN_TYPES holds the standard ActiveRecord::Migration column types.
|
|
22
77
|
# This list provides compatability with the standard types, but we add our own
|
|
23
78
|
# later in 'Special Column Types'.
|
|
@@ -31,6 +31,89 @@ en:
|
|
|
31
31
|
trash: trash
|
|
32
32
|
list_versions: List versions
|
|
33
33
|
close_versions_list: Close versions list
|
|
34
|
+
files:
|
|
35
|
+
download: download
|
|
36
|
+
replace: replace file
|
|
37
|
+
remove: remove file
|
|
38
|
+
restore: restore
|
|
39
|
+
restore_file: restore file
|
|
40
|
+
purge: delete permanently
|
|
41
|
+
undo: undo
|
|
42
|
+
restore_previous: "restore removed file %{filename}"
|
|
43
|
+
replace_hint: "The current file %{filename} goes to the trash."
|
|
44
|
+
confirm_remove:
|
|
45
|
+
one: "Remove %{filename}? It goes to the trash and can be restored for 1 day."
|
|
46
|
+
other: "Remove %{filename}? It goes to the trash and can be restored for %{count} days."
|
|
47
|
+
confirm_remove_no_expiry: "Remove %{filename}? It goes to the trash and can be restored."
|
|
48
|
+
confirm_restore_swap: "The current file %{current} goes to the trash and %{filename} is put back. Continue?"
|
|
49
|
+
confirm_purge: "Delete %{filename} permanently? This cannot be undone."
|
|
50
|
+
notice_removed: "%{filename} removed"
|
|
51
|
+
notice_restored: File restored.
|
|
52
|
+
notice_purged: File deleted permanently.
|
|
53
|
+
notice_already_empty: There was no file to remove.
|
|
54
|
+
notice_not_restorable: This file can no longer be restored.
|
|
55
|
+
notice_not_purgeable: This file was already restored or deleted.
|
|
56
|
+
missing: This file has been removed.
|
|
57
|
+
trash_header:
|
|
58
|
+
zero: Removed files
|
|
59
|
+
one: Removed files (1)
|
|
60
|
+
other: Removed files (%{count})
|
|
61
|
+
open_trash: Show removed files
|
|
62
|
+
close_trash: Hide removed files
|
|
63
|
+
by_on: "by %{user} on %{at}"
|
|
64
|
+
by_system: the system
|
|
65
|
+
redacted_name: "[removed file]"
|
|
66
|
+
state_restored: "restored by %{user} on %{at}"
|
|
67
|
+
state_purged: "deleted permanently by %{user} on %{at}"
|
|
68
|
+
state_expired: expired, will be deleted
|
|
69
|
+
expired_pending:
|
|
70
|
+
one: 1 expired file waits for the nightly clean-up.
|
|
71
|
+
other: "%{count} expired files wait for the nightly clean-up."
|
|
72
|
+
expires_in:
|
|
73
|
+
one: "restorable until %{date} (1 day)"
|
|
74
|
+
other: "restorable until %{date} (%{count} days)"
|
|
75
|
+
reason:
|
|
76
|
+
removed: removed
|
|
77
|
+
replaced: replaced
|
|
78
|
+
record_destroyed: record deleted
|
|
79
|
+
orphaned: orphaned bytes
|
|
80
|
+
uploaded: uploaded
|
|
81
|
+
event:
|
|
82
|
+
uploaded: file uploaded
|
|
83
|
+
removed: file removed
|
|
84
|
+
replaced: file replaced
|
|
85
|
+
record_destroyed: file trashed (record deleted)
|
|
86
|
+
orphaned: orphaned file trashed
|
|
87
|
+
restored: file restored
|
|
88
|
+
purged: file deleted
|
|
89
|
+
global:
|
|
90
|
+
title: Removed files
|
|
91
|
+
model: Model
|
|
92
|
+
state: State
|
|
93
|
+
all: all
|
|
94
|
+
filter: filter
|
|
95
|
+
record: Record
|
|
96
|
+
record_gone: "%{model} #%{id} (deleted)"
|
|
97
|
+
slot: Field
|
|
98
|
+
file: File
|
|
99
|
+
trashed: Removed
|
|
100
|
+
actions: Actions
|
|
101
|
+
purge_selected: Delete selected permanently
|
|
102
|
+
confirm_purge_selected: Delete the selected files permanently? This cannot be undone.
|
|
103
|
+
purged_count:
|
|
104
|
+
one: 1 file deleted permanently.
|
|
105
|
+
other: "%{count} files deleted permanently."
|
|
106
|
+
nothing_selected: No files selected.
|
|
107
|
+
empty: No removed files.
|
|
108
|
+
sweep_last: "Last clean-up: %{at} (%{count} deleted)"
|
|
109
|
+
sweep_never: The nightly clean-up has never run.
|
|
110
|
+
sweep_overdue: The nightly clean-up has not run for more than 48 hours. Check the cron job.
|
|
111
|
+
sweep_failed: "Last clean-up failed: %{error}"
|
|
112
|
+
states:
|
|
113
|
+
trashed: in trash
|
|
114
|
+
restored: restored
|
|
115
|
+
purged: deleted
|
|
116
|
+
expired: expired
|
|
34
117
|
pending_migration:
|
|
35
118
|
notice: "pending migration \u2014 run rails db:migrate"
|
|
36
119
|
succes: success.
|
|
@@ -41,3 +124,7 @@ en:
|
|
|
41
124
|
client:
|
|
42
125
|
first_name: first name
|
|
43
126
|
last_name: last name
|
|
127
|
+
errors:
|
|
128
|
+
messages:
|
|
129
|
+
inline_forms_file_too_large: "is too large (maximum %{count} MB)"
|
|
130
|
+
inline_forms_file_missing: "no file chosen"
|