Skip to content

Commit

Permalink
Adds multiple file upload (#109)
Browse files Browse the repository at this point in the history
* Revert "Revert "Adds multiple file upload (#10)" (#108)"

This reverts commit 23ae498.

* Add some docs about multiple file_fields

---------

Co-authored-by: Jeremy Green <[email protected]>
  • Loading branch information
andrewculver and jagthedrummer authored Aug 23, 2023
1 parent 0363df3 commit 1e1e352
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
static targets = [
"removeFileFlag",
"downloadFileButton",
"removeFileButton",
"fileName"
];

static values = { id: Number }

removeFile() {
if (this.hasDownloadFileButtonTarget) {
this.downloadFileButtonTarget.classList.add("hidden");
}

this.removeFileButtonTarget.classList.add("hidden");
this.fileNameTarget.classList.add("hidden");
this.removeFileFlagTarget.value = this.idValue;
}

}
3 changes: 3 additions & 0 deletions bullet_train-fields/app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ColorPickerController from './fields/color_picker_controller'
import DateController from './fields/date_controller'
import EmojiPickerController from './fields/emoji_picker_controller'
import FileFieldController from './fields/file_field_controller'
import FileItemController from './fields/file_item_controller'
import PasswordController from './fields/password_controller'
import PhoneController from './fields/phone_controller'
import SuperSelectController from './fields/super_select_controller'
Expand All @@ -19,6 +20,7 @@ export const controllerDefinitions = [
[DateController, 'fields/date_controller.js'],
[EmojiPickerController, 'fields/emoji_picker_controller.js'],
[FileFieldController, 'fields/file_field_controller.js'],
[FileItemController, 'fields/file_item_controller.js'],
[PasswordController, 'fields/password_controller.js'],
[PhoneController, 'fields/phone_controller.js'],
[SuperSelectController, 'fields/super_select_controller.js'],
Expand All @@ -39,6 +41,7 @@ export {
DateController,
EmojiPickerController,
FileFieldController,
FileItemController,
PasswordController,
PhoneController,
SuperSelectController,
Expand Down
61 changes: 53 additions & 8 deletions bullet_train-super_scaffolding/lib/scaffolding/transformer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ def add_attributes_to_various_views(attributes, scaffolding_options = {})
when "number_field"
"number"
when "file_field"
"file"
"file#{"s" if is_multiple}"
when "password_field"
"text"
else
Expand Down Expand Up @@ -1065,6 +1065,9 @@ def valid_#{collection_name}
].each do |file|
if is_ids || is_multiple
scaffold_add_line_to_file(file, "#{name}: [],", RUBY_NEW_ARRAYS_HOOK, prepend: true)
if type == "file_field"
scaffold_add_line_to_file(file, "#{name}_removal: [],", RUBY_NEW_ARRAYS_HOOK, prepend: true)
end
else
scaffold_add_line_to_file(file, ":#{name},", RUBY_NEW_FIELDS_HOOK, prepend: true)
if type == "file_field"
Expand Down Expand Up @@ -1113,7 +1116,11 @@ def valid_#{collection_name}
when "date_and_time_field"
"assert_equal_or_nil DateTime.parse(tangible_thing_data['#{name}']), tangible_thing.#{name}"
when "file_field"
"assert_equal tangible_thing_data['#{name}'], rails_blob_path(@tangible_thing.#{name}) unless controller.action_name == 'create'"
if is_multiple
"assert_equal tangible_thing_data['#{name}'], @tangible_thing.#{name}.map{|file| rails_blob_path(file)} unless controller.action_name == 'create'"
else
"assert_equal tangible_thing_data['#{name}'], rails_blob_path(@tangible_thing.#{name}) unless controller.action_name == 'create'"
end
else
"assert_equal_or_nil tangible_thing_data['#{name}'], tangible_thing.#{name}"
end
Expand All @@ -1122,13 +1129,30 @@ def valid_#{collection_name}

# File fields are handled in a specific way when using the jsonapi-serializer.
if type == "file_field"
scaffold_add_line_to_file("./app/views/api/v1/scaffolding/completely_concrete/tangible_things/_tangible_thing.json.jbuilder", "json.#{name} url_for(tangible_thing.#{name}) if tangible_thing.#{name}.attached?", RUBY_FILES_HOOK, prepend: true, suppress_could_not_find: true)
jbuilder_content = if is_multiple
<<~RUBY
json.#{name} do
json.array! tangible_thing.#{name}.map { |file| url_for(file) }
end if tangible_thing.#{name}.attached?
RUBY
else
"json.#{name} url_for(tangible_thing.#{name}) if tangible_thing.#{name}.attached?"
end

scaffold_add_line_to_file("./app/views/api/v1/scaffolding/completely_concrete/tangible_things/_tangible_thing.json.jbuilder", jbuilder_content, RUBY_FILES_HOOK, prepend: true, suppress_could_not_find: true)
# We also want to make sure we attach the dummy file in the API test on setup
file_name = "./test/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller_test.rb"
content = <<~RUBY
@#{child.underscore}.#{name} = Rack::Test::UploadedFile.new("test/support/foo.txt")
@another_#{child.underscore}.#{name} = Rack::Test::UploadedFile.new("test/support/foo.txt")
RUBY
content = if is_multiple
<<~RUBY
@#{child.underscore}.#{name} = [Rack::Test::UploadedFile.new("test/support/foo.txt")]
@another_#{child.underscore}.#{name} = [Rack::Test::UploadedFile.new("test/support/foo.txt")]
RUBY
else
<<~RUBY
@#{child.underscore}.#{name} = Rack::Test::UploadedFile.new("test/support/foo.txt")
@another_#{child.underscore}.#{name} = Rack::Test::UploadedFile.new("test/support/foo.txt")
RUBY
end
scaffold_add_line_to_file(file_name, content, RUBY_FILES_HOOK, prepend: true)
end

Expand Down Expand Up @@ -1297,7 +1321,27 @@ def valid_#{collection_name}

case type
when "file_field"
remove_file_methods =
remove_file_methods = if is_multiple
<<~RUBY
def #{name}_removal?
#{name}_removal&.any?
end
def remove_#{name}
#{name}_attachments.where(id: #{name}_removal).map(&:purge)
end
def #{name}=(attachables)
attachables = Array(attachables).compact_blank
if attachables.any?
attachment_changes["#{name}"] =
ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables)
end
end
RUBY
else
<<~RUBY
def #{name}_removal?
#{name}_removal.present?
Expand All @@ -1307,6 +1351,7 @@ def remove_#{name}
#{name}.purge
end
RUBY
end

# Generating a model with an `attachment(s)` data type (i.e. - `rails g ModelName file:attachment`)
# adds `has_one_attached` or `has_many_attached` to our model, just not directly above the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<% object ||= current_attributes_object %>
<% strategy ||= current_attributes_strategy || :none %>
<% url ||= nil %>
<% if object.send(attribute).attached? %>
<%= render 'shared/attributes/attribute', object: object, attribute: attribute, strategy: strategy, url: url do %>
<% object.send(attribute).each do |file| %>
<%= link_to url_for(file), class: 'button download-file' do %>
<i class="leading-none mr-2 text-base ti ti-download"></i>
<span>Download File</span>
<% end %>
<% end %>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,33 @@
form ||= current_fields_form
options ||= {}
other_options ||= {}
multiple ||= false
persisted_files = multiple ? form.object.send(method) : [form.object.send(method)]
%>

<%= render 'shared/fields/field', form: form, method: method, helper: :file_field, options: options, other_options: other_options do %>
<% content_for :field do %>
<div class="file-field" data-controller="fields--file-field">
<%= form.hidden_field "#{method}_removal".to_sym, value: nil, data: {'fields--file-field-target': 'removeFileFlag'} %>
<%= form.file_field method, class: 'file-upload hidden', direct_upload: true, data: {'fields--file-field-target': 'fileField', action: 'change->fields--file-field#handleFileSelected'} %>
<%= form.file_field method, class: 'file-upload hidden', multiple: multiple, direct_upload: true, data: {'fields--file-field-target': 'fileField', action: 'change->fields--file-field#handleFileSelected'} %>
<div>
<% if form.object.send(method).attached? %>
<%= link_to url_for(form.object.send(method)), class: 'button download-file mr-3', data: {'fields--file-field-target': 'downloadFileButton'} do %>
<i class="leading-none mr-2 text-base ti ti-download"></i>
<span><%= t('fields.download_document') %></span>
<% end %>
<% end %>
<% if form.object.send(method).attached? %>
<% if form.object.send(method).representable? %>
<div class="mt-2">
<%= image_tag(form.object.send(method).representation(resize_to_limit: [200, 200])) %>
</div>
<% end %>
<div class="mt-2 button-alternative cursor-pointer mr-3" data-action="click->fields--file-field#removeFile" data-fields--file-field-target="removeFileButton">
<i class="leading-none mr-2 text-base ti ti-trash"></i>
<span><%= t('fields.remove_document') %></span>
<div class="divide-y-2 divide-dashed">
<% persisted_files.each do |file| %>
<div data-controller="fields--file-item" data-fields--file-item-id-value="<%= file.id %>">
<%= form.hidden_field "#{method}_removal".to_sym, multiple: multiple, value: nil, data: {'fields--file-item-target': 'removeFileFlag'} %>
<span data-fields--file-item-target="fileName" %>
<%= file.blob.filename %>
</span>
<%= link_to url_for(file), class: 'button download-file mr-3', data: {'fields--file-item-target': 'downloadFileButton'} do %>
<i class="leading-none mr-2 text-base ti ti-download"></i>
<span><%= t('fields.download_document') %></span>
<% end %>
<div class="button-alternative cursor-pointer mr-3" data-action="click->fields--file-item#removeFile" data-fields--file-item-target="removeFileButton">
<i class="leading-none mr-2 text-base ti ti-trash"></i>
<span><%= t('fields.remove_document') %></span>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
Expand Down
28 changes: 28 additions & 0 deletions bullet_train/docs/field-partials/file-field.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ In addition, Bullet Train has integrated the direct-uploads feature of Active St
## Example

The following steps illustrate how to add a `document` file attachment to a `Post` model.

Add the following to `app/models/post.rb`:

```ruby
Expand All @@ -22,3 +23,30 @@ Run the following command to generate the scaffolding for the `document` field o
```bash
./bin/super-scaffold crud-field Post document:file_field
```

## Multiple Attachment Example

The following steps illustrate how to add multiple `document` file attachments to a `Post` model.

Add the following to `app/models/post.rb`:

```ruby
has_many_attached :documents
```

Note, no database migration is required as ActiveStorage uses its own tables to store the attachments.

Run the following command to generate the scaffolding for the `documents` field on the `Post` model:

```bash
./bin/super-scaffold crud-field Post documents:file_field{multiple}
```

## Generating a Model & Super Scaffold Example

If you're starting fresh, and don't have an existing model you can do something like this:

```
rails g model Project team:references name:string specification:attachment documents:attachments
bin/super-scaffold crud Project Team name:text_field specification:file_field documents:file_field{multiple}
```

0 comments on commit 1e1e352

Please sign in to comment.