-
Notifications
You must be signed in to change notification settings - Fork 22
WIP: Fix image/file uploads for Community News, Resources, and Stories #1084
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7b8bdd7
c2d79dc
82aef75
2e4ca14
4805c3e
46290a3
bc739c3
9fce529
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,30 +1,109 @@ | ||
| import { Controller } from "@hotwired/stimulus" | ||
|
|
||
| const FILE_TYPE_ICONS = { | ||
| "application/pdf": "fa-regular fa-file-pdf", | ||
| "application/zip": "fa-regular fa-file-zipper", | ||
| "application/msword": "fa-regular fa-file-word", | ||
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "fa-regular fa-file-word", | ||
| "application/vnd.oasis.opendocument.text": "fa-regular fa-file-lines", | ||
| "text/html": "fa-regular fa-file-code", | ||
| } | ||
|
|
||
| export default class extends Controller { | ||
| static targets = ["input", "preview", "placeholder", "filename"] | ||
|
|
||
| connect() { | ||
| this.handleUploadError = this.onUploadError.bind(this) | ||
| this.handleUploadEnd = this.onUploadEnd.bind(this) | ||
| this.element.addEventListener("direct-upload:error", this.handleUploadError) | ||
| this.element.addEventListener("direct-upload:end", this.handleUploadEnd) | ||
| } | ||
|
|
||
| disconnect() { | ||
| this.element.removeEventListener("direct-upload:error", this.handleUploadError) | ||
| this.element.removeEventListener("direct-upload:end", this.handleUploadEnd) | ||
| } | ||
|
|
||
| update(event) { | ||
| const file = event.target.files[0] | ||
| if (!file) return | ||
|
|
||
| this.clearError() | ||
|
|
||
| // Update filename | ||
| if (this.hasFilenameTarget) { | ||
| this.filenameTarget.textContent = file.name | ||
| } | ||
|
|
||
| // Update preview image (if you have one) | ||
| if (this.hasPreviewTarget) { | ||
| const reader = new FileReader() | ||
| reader.onload = e => { | ||
| this.previewTarget.src = e.target.result | ||
| this.previewTarget.classList.remove("hidden") | ||
| } | ||
| reader.readAsDataURL(file) | ||
| if (file.type.startsWith("image/")) { | ||
| this.showImagePreview(file) | ||
| } else { | ||
| this.showFileIcon(file.type) | ||
| } | ||
| } | ||
|
|
||
| // Ensure a placeholder div exists (server may not render one when a persisted file exists) | ||
| ensurePlaceholder() { | ||
| if (this.hasPlaceholderTarget) return this.placeholderTarget | ||
|
|
||
| const wrapper = this.hasPreviewTarget | ||
| ? this.previewTarget.parentElement | ||
| : this.element.querySelector(".flex.flex-col") | ||
|
|
||
| const placeholder = document.createElement("div") | ||
| placeholder.dataset.filePreviewTarget = "placeholder" | ||
| placeholder.className = "absolute inset-0 flex items-center justify-center border border-gray-300 shadow-sm bg-gray-50 text-gray-400 text-6xl" | ||
| wrapper.appendChild(placeholder) | ||
| return placeholder | ||
| } | ||
|
|
||
| showImagePreview(file) { | ||
| if (!this.hasPreviewTarget) return | ||
|
|
||
| const reader = new FileReader() | ||
| reader.onload = e => { | ||
| this.previewTarget.src = e.target.result | ||
| this.previewTarget.classList.remove("hidden") | ||
| } | ||
| reader.readAsDataURL(file) | ||
|
|
||
| // Hide placeholder if present | ||
| if (this.hasPlaceholderTarget) { | ||
| this.placeholderTarget.classList.add("hidden") | ||
| } | ||
| } | ||
|
|
||
| showFileIcon(mimeType) { | ||
| const placeholder = this.ensurePlaceholder() | ||
| const iconClass = FILE_TYPE_ICONS[mimeType] || "fa-regular fa-file" | ||
| placeholder.innerHTML = `<i class="${iconClass} text-5xl"></i>` | ||
| placeholder.classList.remove("hidden") | ||
|
|
||
| if (this.hasPreviewTarget) { | ||
| this.previewTarget.classList.add("hidden") | ||
| } | ||
| } | ||
|
|
||
| onUploadError(event) { | ||
| event.preventDefault() | ||
| const { error } = event.detail | ||
| this.showError(`Upload failed: ${error}`) | ||
| } | ||
|
|
||
| onUploadEnd(event) { | ||
| this.clearError() | ||
| } | ||
|
|
||
| showError(message) { | ||
| this.clearError() | ||
| const errorEl = document.createElement("p") | ||
| errorEl.className = "text-red-500 text-sm mt-1" | ||
| errorEl.dataset.filePreviewTarget = "error" | ||
| errorEl.textContent = message | ||
| this.element.appendChild(errorEl) | ||
| } | ||
|
|
||
| clearError() { | ||
| const existing = this.element.querySelector('[data-file-preview-target="error"]') | ||
| if (existing) existing.remove() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,9 @@ class Asset < ApplicationRecord | |
| "image/jpeg", | ||
| "image/png", | ||
| "image/gif", | ||
| "image/webp", | ||
| "image/heic", | ||
| "image/heif", | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add new filetypes |
||
| "application/pdf", | ||
| "application/zip", | ||
| "application/msword", # Word .doc | ||
|
|
@@ -39,6 +42,29 @@ def self.allowed_types_for_owner(owner) | |
| end | ||
| end | ||
|
|
||
| CONTENT_TYPE_LABELS = { | ||
| "image/jpeg" => "JPG", | ||
| "image/png" => "PNG", | ||
| "image/gif" => "GIF", | ||
| "image/webp" => "WebP", | ||
| "image/heic" => "HEIC", | ||
| "image/heif" => "HEIF", | ||
| "application/pdf" => "PDF", | ||
| "application/zip" => "ZIP", | ||
| "application/msword" => "DOC", | ||
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => "DOCX", | ||
| "application/vnd.oasis.opendocument.text" => "ODT", | ||
| "text/html" => "HTML" | ||
| }.freeze | ||
|
|
||
| def self.accept_attribute | ||
| self::ACCEPTED_CONTENT_TYPES.join(",") | ||
| end | ||
|
|
||
| def self.accepted_types_label | ||
| self::ACCEPTED_CONTENT_TYPES.map { |ct| CONTENT_TYPE_LABELS[ct] || ct }.join(", ") | ||
| end | ||
|
|
||
| belongs_to :owner, polymorphic: true, optional: true, touch: true | ||
| belongs_to :report, optional: true | ||
|
|
||
|
|
@@ -55,16 +81,10 @@ def self.allowed_types_for_owner(owner) | |
| def file_type | ||
| return unless file.attached? | ||
|
|
||
| allowed_types = | ||
| case type | ||
| when "PrimaryAsset" | ||
| PrimaryAsset::ACCEPTED_CONTENT_TYPES | ||
| else | ||
| ACCEPTED_CONTENT_TYPES | ||
| end | ||
| allowed_types = self.class::ACCEPTED_CONTENT_TYPES | ||
|
|
||
| unless allowed_types.include?(file.content_type) | ||
| errors.add(:file, "type is not allowed for #{type.underscore.humanize}") | ||
| errors.add(:file, "type not accepted") | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,9 @@ | ||
| class PrimaryAsset < Asset | ||
| ACCEPTED_CONTENT_TYPES = [ "image/jpeg", "image/png" ].freeze | ||
| ACCEPTED_CONTENT_TYPES = [ | ||
| "image/jpeg", | ||
| "image/png", | ||
| "image/webp", | ||
| "image/heic", | ||
| "image/heif" | ||
| ].freeze | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| <div class="assets"> | ||
| <%= render "assets/display_image", resource: resource, file: (defined?(file) ? file : nil), variant: (defined?(variant) ? variant : :hero) %> | ||
| <%= render "assets/display_gallery_media", resource: resource %> | ||
| <%= render "assets/display_image", resource: resource, file: (defined?(file) ? file : nil), variant: (defined?(variant) ? variant : :hero), link: (defined?(link) ? link : nil) %> | ||
| <%= render "assets/display_gallery_media", resource: resource, link: (defined?(link) ? link : nil) %> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,7 +52,11 @@ | |
| <% end %> | ||
| </div> | ||
| <!-- ACTION BUTTONS --> | ||
| <div class="absolute bottom-3 right-3 flex items-center space-x-2 z-30"> | ||
| <div class="absolute bottom-3 left-4 right-3 flex items-center z-30"> | ||
| <% if allowed_to?(:edit?, resource) %> | ||
| <p class="admin-only text-[10px] text-blue-600 bg-blue-50 rounded px-1.5 py-0.5 whitespace-nowrap"><%= resource.created_at.strftime("%b %-d, %Y") %></p> | ||
| <% end %> | ||
| <div class="ml-auto flex items-center space-x-2"> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. show created at in admin styling on resource card (this pr is wrt resource editing, mainly and this was a stakeholder request) |
||
| <% if resource.downloadable_asset&.file&.attached? %> | ||
| <%= link_to( | ||
| content_tag(:span, "", class: "fa fa-download"), | ||
|
|
@@ -69,6 +73,7 @@ | |
| data: {turbo_frame: "_top", turbo_prefetch: false } | ||
| ) %> | ||
| <% end %> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,8 @@ | |
| <% use_profile_placeholder ||= false %> | ||
| <% show_file_field = show_file_field.to_s.present? ? show_file_field : true %> | ||
| <% show_existing = show_existing.to_s.present? ? show_existing : true %> | ||
| <% accept_attr = asset.class.respond_to?(:accept_attribute) ? asset.class.accept_attribute : nil %> | ||
| <% accepted_label = asset.class.respond_to?(:accepted_types_label) ? asset.class.accepted_types_label : nil %> | ||
| <% file = | ||
| if file.present? | ||
| file | ||
|
|
@@ -92,6 +94,7 @@ | |
|
|
||
| <div class="relative w-28"> | ||
| <%= f.file_field field_name, | ||
| accept: accept_attr, | ||
| data: { | ||
| file_preview_target: "input", | ||
| action: "change->file-preview#update" | ||
|
|
@@ -118,9 +121,20 @@ | |
| <% end %> | ||
|
|
||
| </div> | ||
|
|
||
| <% if accepted_label.present? %> | ||
| <p class="text-gray-400 text-xs mt-1">Accepts: <%= accepted_label %></p> | ||
| <% end %> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. show accepted file types on form |
||
| <% end %> | ||
|
|
||
| </div> | ||
| </div> | ||
|
|
||
| <%= f.error :file, wrap_with: { tag: :p, class: "text-red-500 text-sm mt-1" } %> | ||
| <% if asset.errors[:file].any? %> | ||
| <div class="mt-2 mx-auto w-fit rounded-md border border-red-200 bg-red-50 px-3 py-2 text-center"> | ||
| <p class="text-red-600 text-sm"> | ||
| <i class="fa-solid fa-circle-exclamation mr-1"></i> | ||
| File type not accepted | ||
| </p> | ||
| </div> | ||
| <% end %> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. show pretty error msg if wrong filetype |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,7 +56,7 @@ | |
| <div id="upload-progress-bar" class="h-full w-0 bg-blue-500 transition-[width] duration-200 ease-out"></div> | ||
| </div> | ||
| <div id="hero-image"> | ||
| <%= render "assets/display_assets", resource: @story %> | ||
| <%= render "assets/display_assets", resource: @story, link: true %> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we want show page images to open in new window |
||
| </div> | ||
|
|
||
| <!-- Story Body Text --> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,7 +35,7 @@ | |
| </p> | ||
| </div> | ||
|
|
||
| <%= render "assets/display_assets", resource: @story_idea %> | ||
| <%= render "assets/display_assets", resource: @story_idea, link: true %> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we want show page images to open in new window |
||
|
|
||
| <!-- Story Body --> | ||
| <div class="mb-10"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,8 @@ | |
| <div class="aspect-[16/6] bg-gray-200"> | ||
| <%= render "assets/display_image", | ||
| resource: @story, | ||
| variant: :hero %> | ||
| variant: :hero, | ||
| link: true %> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we want show page images to open in new window |
||
| </div> | ||
|
|
||
| <!-- Blue Sector Box on Top Left --> | ||
|
|
@@ -81,7 +82,7 @@ | |
| <!-- Additional Story Assets (if any beyond hero) --> | ||
| <% if @story.assets.count > 1 %> | ||
| <div class="mb-10"> | ||
| <%= render "assets/display_assets", resource: @story %> | ||
| <%= render "assets/display_assets", resource: @story, link: true %> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we want show page images to open in new window |
||
| </div> | ||
| <% end %> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,7 +36,7 @@ | |
| </div> | ||
| <% end %> | ||
|
|
||
| <%= render "assets/display_assets", resource: @tutorial %> | ||
| <%= render "assets/display_assets", resource: @tutorial, link: true %> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we want show page images to open in new window |
||
|
|
||
|
|
||
| <!-- Main Text --> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,6 +101,6 @@ | |
| </div> | ||
| </div> | ||
| <div class="media"> | ||
| <%= render "assets/display_assets", resource: @workshop_idea %> | ||
| <%= render "assets/display_assets", resource: @workshop_idea, link: true %> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we want show page images to open in new window |
||
| </div> | ||
| </div> | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
show an icon as in a form on save