diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0664abd2f..e8542e68c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,10 @@ jobs: - name: Install Dependencies run: yarn install --frozen-lockfile - run: bin/ci + - name: Fail when generated npm changes are not checked-in + run: | + git update-index --refresh && git diff-index --quiet HEAD -- + rails-tests: name: Downstream Rails integration tests runs-on: ubuntu-latest @@ -88,6 +92,10 @@ jobs: rails_branch: main experimental: true steps: + - uses: ruby/setup-ruby-pkgs@v1 + with: + ruby-version: ${{ matrix.ruby }} + apt-get: libvips-tools - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: diff --git a/action_text-trix/Gemfile b/action_text-trix/Gemfile index 676ce4900..90db7a505 100644 --- a/action_text-trix/Gemfile +++ b/action_text-trix/Gemfile @@ -5,6 +5,7 @@ gemspec branch = ENV.fetch("RAILS_BRANCH", "main") gem "rails", github: "rails/rails", branch: branch +gem "image_processing" gem "importmap-rails" gem "propshaft" gem "puma" diff --git a/action_text-trix/app/assets/javascripts/trix.js b/action_text-trix/app/assets/javascripts/trix.js index 0a56d27e2..bfcc8a2f5 100644 --- a/action_text-trix/app/assets/javascripts/trix.js +++ b/action_text-trix/app/assets/javascripts/trix.js @@ -89,6 +89,9 @@ Copyright © 2025 37signals, LLC var engines = { node: ">= 18" }; + var peerDependencies = { + "@rails/actiontext": "^8.1.100" + }; var _package = { name: name, version: version, @@ -107,7 +110,8 @@ Copyright © 2025 37signals, LLC resolutions: resolutions, scripts: scripts, dependencies: dependencies, - engines: engines + engines: engines, + peerDependencies: peerDependencies }; const attachmentSelector = "[data-trix-attachment]"; @@ -3451,12 +3455,16 @@ $\ } const width = this.attachment.getWidth(); const height = this.attachment.getHeight(); + const alt = this.attachment.getAttribute("alt"); if (width != null) { image.width = width; } if (height != null) { image.height = height; } + if (alt != null) { + image.alt = alt; + } const storeKey = ["imageElement", this.attachment.id, image.src, image.width, image.height].join("/"); image.dataset.trixStoreKey = storeKey; } @@ -6616,6 +6624,11 @@ $\ this.attributes = Hash.box(attributes); this.didChangeAttributes(); } + setAttribute(attribute, value) { + this.setAttributes({ + [attribute]: value + }); + } getAttribute(attribute) { return this.attributes.get(attribute); } @@ -8855,6 +8868,8 @@ $\ ManagedAttachment.proxyMethod("attachment.isPending"); ManagedAttachment.proxyMethod("attachment.isPreviewable"); ManagedAttachment.proxyMethod("attachment.getURL"); + ManagedAttachment.proxyMethod("attachment.getPreviewURL"); + ManagedAttachment.proxyMethod("attachment.setPreviewURL"); ManagedAttachment.proxyMethod("attachment.getHref"); ManagedAttachment.proxyMethod("attachment.getFilename"); ManagedAttachment.proxyMethod("attachment.getFilesize"); @@ -12465,12 +12480,12 @@ $\ this.attributes = {}; this.actions = {}; this.resetDialogInputs(); - handleEvent("mousedown", { + handleEvent("click", { onElement: this.element, matchingSelector: actionButtonSelector, withCallback: this.didClickActionButton }); - handleEvent("mousedown", { + handleEvent("click", { onElement: this.element, matchingSelector: attributeButtonSelector, withCallback: this.didClickAttributeButton @@ -13248,6 +13263,22 @@ $\ this.innerHTML = toolbar.getDefaultHTML(); } } + + // Properties + + get editorElements() { + if (this.id) { + var _this$ownerDocument; + const nodeList = (_this$ownerDocument = this.ownerDocument) === null || _this$ownerDocument === void 0 ? void 0 : _this$ownerDocument.querySelectorAll("trix-editor[toolbar=\"".concat(this.id, "\"]")); + return Array.from(nodeList); + } else { + return []; + } + } + get editorElement() { + const [editorElement] = this.editorElements; + return editorElement; + } } let id = 0; diff --git a/action_text-trix/app/assets/javascripts/trix/actiontext.esm.js b/action_text-trix/app/assets/javascripts/trix/actiontext.esm.js new file mode 100644 index 000000000..fb3eb6d2e --- /dev/null +++ b/action_text-trix/app/assets/javascripts/trix/actiontext.esm.js @@ -0,0 +1,18 @@ +/* +trix/actiontext 2.1.15 +Copyright © 2025 37signals, LLC + */ +import { AttachmentUpload } from '@rails/actiontext'; + +addEventListener("trix-attachment-add", event => { + const { + attachment, + target + } = event; + if (attachment.file) { + const upload = new AttachmentUpload(attachment, target, attachment.file); + const onProgress = event => attachment.setUploadProgress(event.detail.progress); + target.addEventListener("direct-upload:progress", onProgress); + upload.start().then(attributes => attachment.setAttributes(attributes)).catch(error => alert(error)).finally(() => target.removeEventListener("direct-upload:progress", onProgress)); + } +}); diff --git a/action_text-trix/app/assets/javascripts/trix/actiontext.esm.min.js b/action_text-trix/app/assets/javascripts/trix/actiontext.esm.min.js new file mode 100644 index 000000000..b7aa2e9a8 --- /dev/null +++ b/action_text-trix/app/assets/javascripts/trix/actiontext.esm.min.js @@ -0,0 +1,5 @@ +/* +trix/actiontext 2.1.15 +Copyright © 2025 37signals, LLC + */ +import{AttachmentUpload as t}from"@rails/actiontext";addEventListener("trix-attachment-add",(e=>{const{attachment:r,target:a}=e;if(r.file){const e=new t(r,a,r.file),s=t=>r.setUploadProgress(t.detail.progress);a.addEventListener("direct-upload:progress",s),e.start().then((t=>r.setAttributes(t))).catch((t=>alert(t))).finally((()=>a.removeEventListener("direct-upload:progress",s)))}})); diff --git a/action_text-trix/lib/action_text/trix/engine.rb b/action_text-trix/lib/action_text/trix/engine.rb index f8853bfa6..c1de9f473 100644 --- a/action_text-trix/lib/action_text/trix/engine.rb +++ b/action_text-trix/lib/action_text/trix/engine.rb @@ -2,7 +2,7 @@ module Trix class Engine < ::Rails::Engine initializer "trix.asset" do |app| if app.config.respond_to?(:assets) - app.config.assets.precompile += %w[ trix.js trix.css ] + app.config.assets.precompile += %w[ trix.js trix.css trix/actiontext.esm.js trix/actiontext.esm.min.js ] end end end diff --git a/action_text-trix/test/application_system_test_case.rb b/action_text-trix/test/application_system_test_case.rb index 5d400471d..e02f7c342 100644 --- a/action_text-trix/test/application_system_test_case.rb +++ b/action_text-trix/test/application_system_test_case.rb @@ -7,6 +7,34 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase js_errors: true, headless: ENV["HEADLESS"] != "0" } + + def capture_events(event_names) + execute_script <<~JS, *event_names + window.capturedEvents = [] + + function capture({ target: { id }, type, detail }) { + for (const name in detail) { + detail[name] = detail[name].constructor.name + } + + capturedEvents.push({ id, type, detail }) + } + + for (const eventName of arguments) { + addEventListener(eventName, capture, { once: true }) + } + JS + + yield + + evaluate_script("window.capturedEvents").each do |event| + event["target"] = find(id: event.delete("id")) + end + end + + def go_offline! + page.driver.browser.network.emulate_network_conditions(offline: true) + end end Capybara.server = :puma, { Silent: true } diff --git a/action_text-trix/test/dummy/app/javascript/application.js b/action_text-trix/test/dummy/app/javascript/application.js index bf0fca607..beff742ec 100644 --- a/action_text-trix/test/dummy/app/javascript/application.js +++ b/action_text-trix/test/dummy/app/javascript/application.js @@ -1,2 +1 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails -import "trix" diff --git a/action_text-trix/test/dummy/app/views/layouts/application.html.erb b/action_text-trix/test/dummy/app/views/layouts/application.html.erb index 2c72634e1..75a4cc27d 100644 --- a/action_text-trix/test/dummy/app/views/layouts/application.html.erb +++ b/action_text-trix/test/dummy/app/views/layouts/application.html.erb @@ -20,6 +20,15 @@ <%# Includes all stylesheet files in app/assets/stylesheets %> <%= stylesheet_link_tag :app %> <%= javascript_importmap_tags %> + diff --git a/action_text-trix/test/dummy/config/application.rb b/action_text-trix/test/dummy/config/application.rb index 14f2a0e92..98bf19fee 100644 --- a/action_text-trix/test/dummy/config/application.rb +++ b/action_text-trix/test/dummy/config/application.rb @@ -26,6 +26,6 @@ class Application < Rails::Application # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") - config.active_storage.variant_processor = :disabled + config.active_storage.variant_processor = :vips end end diff --git a/action_text-trix/test/dummy/config/importmap.rb b/action_text-trix/test/dummy/config/importmap.rb index f19ec3c49..1dea619d1 100644 --- a/action_text-trix/test/dummy/config/importmap.rb +++ b/action_text-trix/test/dummy/config/importmap.rb @@ -3,3 +3,6 @@ pin "application" pin "trix" +pin "trix/actiontext", to: "trix/actiontext.esm.js" +pin "@rails/activestorage", to: "activestorage.esm.js" +pin "@rails/actiontext", to: "actiontext.esm.js" diff --git a/action_text-trix/test/system/action_text_test.rb b/action_text-trix/test/system/action_text_test.rb index 1f33f2a1a..549947e81 100644 --- a/action_text-trix/test/system/action_text_test.rb +++ b/action_text-trix/test/system/action_text_test.rb @@ -10,4 +10,73 @@ class ActionTextTest < ApplicationSystemTestCase assert_element class: "trix-content", text: "Hello, world!" end + + test "attaches and uploads image file" do + visit new_message_url + attach_fixture_file "racecar.jpg" + + within :rich_text_area do + assert_selector :element, "img", src: %r{/rails/active_storage/blobs/redirect/.*/racecar.jpg\Z} + end + + click_button "Create Message" + + within class: "trix-content" do + assert_selector :element, "img", src: %r{/rails/active_storage/representations/redirect/.*/racecar.jpg\Z} + end + end + + if ActionText.version >= "8.0.0" + test "dispatches direct-upload:-prefixed events when uploading a File" do + visit new_message_url + events = capture_direct_upload_events do + attach_fixture_file "racecar.jpg" + + assert_selector :element, "img", src: %r{/rails/active_storage/blobs/redirect/.*/racecar.jpg\Z} + end + + assert_equal 1, ActiveStorage::Blob.where(filename: "racecar.jpg").count + assert_equal direct_upload_event("start"), events[0] + assert_equal direct_upload_event("progress", progress: "Number"), events[1] + assert_equal direct_upload_event("end"), events[2] + end + + test "dispatches direct-upload:error event when uploading fails" do + visit new_message_url + events = capture_direct_upload_events do + accept_alert 'Error creating Blob for "racecar.jpg". Status: 0' do + go_offline! + attach_fixture_file "racecar.jpg" + end + + assert_no_selector :element, "img", src: /racecar.jpg\Z/ + end + + assert_empty ActiveStorage::Blob.where(filename: "racecar.jpg") + assert_equal direct_upload_event("error", error: "String"), events.last + end + end + + def attach_fixture_file(path) + attach_file(file_fixture(path)) { click_button "Attach Files" } + end + + def capture_direct_upload_events(&block) + capture_events %w[ + direct-upload:start + direct-upload:progress + direct-upload:error + direct-upload:end + ], &block + end + + def direct_upload_event(name, target: find(:rich_text_area), **detail) + ActiveSupport::HashWithIndifferentAccess.new( + type: "direct-upload:#{name}", + target: target, + detail: detail.with_defaults( + attachment: "ManagedAttachment" + ) + ) + end end diff --git a/package.json b/package.json index 36ab0ee84..641c50dbd 100644 --- a/package.json +++ b/package.json @@ -79,5 +79,8 @@ }, "engines": { "node": ">= 18" + }, + "peerDependencies": { + "@rails/actiontext": "^8.1.100" } } diff --git a/rollup.config.js b/rollup.config.js index 573870b82..4e14775de 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -9,6 +9,7 @@ import { version } from "./package.json" const year = new Date().getFullYear() const banner = `/*\nTrix ${version}\nCopyright © ${year} 37signals, LLC\n */` +const actionTextBanner = `/*\ntrix/actiontext ${version}\nCopyright © ${year} 37signals, LLC\n */` const plugins = [ json(), @@ -92,6 +93,38 @@ export default [ ], ...compressedConfig, }, + { + input: "src/trix/actiontext.js", + output: [ + { + file: "dist/trix/actiontext.esm.js", + format: "es", + banner: actionTextBanner + }, + { + file: "action_text-trix/app/assets/javascripts/trix/actiontext.esm.js", + format: "es", + banner: actionTextBanner + } + ], + ...defaultConfig, + }, + { + input: "src/trix/actiontext.js", + output: [ + { + file: "dist/trix/actiontext.esm.js", + format: "es", + banner: actionTextBanner + }, + { + file: "action_text-trix/app/assets/javascripts/trix/actiontext.esm.min.js", + format: "es", + banner: actionTextBanner + } + ], + ...compressedConfig, + }, { input: "src/test/test.js", output: { diff --git a/src/trix/actiontext.js b/src/trix/actiontext.js new file mode 100644 index 000000000..d4d62368f --- /dev/null +++ b/src/trix/actiontext.js @@ -0,0 +1,17 @@ +import { AttachmentUpload } from "@rails/actiontext" + +addEventListener("trix-attachment-add", event => { + const { attachment, target } = event + + if (attachment.file) { + const upload = new AttachmentUpload(attachment, target, attachment.file) + const onProgress = event => attachment.setUploadProgress(event.detail.progress) + + target.addEventListener("direct-upload:progress", onProgress) + + upload.start() + .then(attributes => attachment.setAttributes(attributes)) + .catch(error => alert(error)) + .finally(() => target.removeEventListener("direct-upload:progress", onProgress)) + } +})