Merge branch 'main' into mobile/bridge-components
* main: Add rollback script to convert relative URLs back to absolute Add a script to migrate existing rich texts to relative URLs Use relative URLs where possible across all the app Use relative URLs for avatars and rich text attachments
This commit is contained in:
@@ -8,7 +8,7 @@ class Users::AvatarsController < ApplicationController
|
||||
if @user.system?
|
||||
redirect_to view_context.image_path("system_user.png")
|
||||
elsif @user.avatar.attached?
|
||||
redirect_to rails_blob_url(@user.avatar_thumbnail, disposition: "inline")
|
||||
redirect_to rails_blob_path(@user.avatar_thumbnail, disposition: "inline")
|
||||
elsif stale? @user, cache_control: cache_control
|
||||
render_initials
|
||||
end
|
||||
|
||||
@@ -42,6 +42,6 @@ module AvatarsHelper
|
||||
end
|
||||
|
||||
def avatar_image_tag(user, **options)
|
||||
image_tag user_avatar_url(user, script_name: user.account.slug), aria: { hidden: "true" }, size: 48, title: user.name, **options
|
||||
image_tag user_avatar_path(user, script_name: user.account.slug), aria: { hidden: "true" }, size: 48, title: user.name, **options
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
height: height %>
|
||||
<% elsif blob.audio? %>
|
||||
<audio controls="true" width="100%" preload="metadata">
|
||||
<source src="<%= rails_blob_url(blob) %>" type="<%= blob.content_type %>">
|
||||
<source src="<%= rails_blob_path(blob) %>" type="<%= blob.content_type %>">
|
||||
</audio>
|
||||
<% elsif blob.variable? %>
|
||||
<%= link_to url_for(blob.variant(variant)), data: { lightbox_target: "image", lightbox_caption_value: blob.filename.to_s } do %>
|
||||
<%= image_tag url_for(blob.variant(variant)), width: width, height: height %>
|
||||
<%= link_to rails_representation_path(blob.variant(variant)), data: { lightbox_target: "image", lightbox_caption_value: blob.filename.to_s } do %>
|
||||
<%= image_tag rails_representation_path(blob.variant(variant)), width: width, height: height %>
|
||||
<% end %>
|
||||
<% elsif blob.previewable? %>
|
||||
<%= image_tag url_for(blob.preview(variant)), width: width, height: height %>
|
||||
<%= image_tag rails_representation_path(blob.preview(variant)), width: width, height: height %>
|
||||
<% else %>
|
||||
<span class="attachment__icon"><%= blob.filename.extension&.downcase.presence || "unknown" %></span>
|
||||
<% end %>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<% height = attachment.metadata["height"] %>
|
||||
|
||||
<% if attachment.previewable? %>
|
||||
<%= image_tag url_for(attachment.preview(variant)), class: "attachment attachment--image", width: width, height: height %>
|
||||
<%= image_tag rails_representation_path(attachment.preview(variant)), class: "attachment attachment--image", width: width, height: height %>
|
||||
<% elsif attachment.variable? %>
|
||||
<%= image_tag url_for(attachment.variant(variant)), class: "attachment attachment--image", width: width, height: height %>
|
||||
<%= image_tag rails_representation_path(attachment.variant(variant)), class: "attachment attachment--image", width: width, height: height %>
|
||||
<% else %>
|
||||
<div class="attachment attachment--file attachment--<%= attachment.filename.extension -%>">
|
||||
<span class="attachment__icon"><%= attachment.filename.extension&.downcase.presence || "unknown" %></span>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<%= collapsible_nav_section "Accounts" do %>
|
||||
<%# Bust cache 1 Dec 2025 %>
|
||||
<% accounts.each do |account| %>
|
||||
<%= filter_place_menu_item landing_url(script_name: account.slug), account.name, "marker", current: account == Current.account, turbo: false %>
|
||||
<%= filter_place_menu_item landing_path(script_name: account.slug), account.name, "marker", current: account == Current.account, turbo: false %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<%= tag.li class: "popup__item", data: { filter_target: "item", navigable_list_target: "item" } do %>
|
||||
<%= icon_tag "logout", class: "popup__icon" %>
|
||||
<%= button_to session_url(script_name: nil), method: :delete, class: "popup__btn btn", data: { turbo: false } do %>
|
||||
<%= button_to session_path(script_name: nil), method: :delete, class: "popup__btn btn", data: { turbo: false } do %>
|
||||
<span>Sign out</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
#<%= card.number %> <%= card.title %>
|
||||
</template>
|
||||
<template type="editor">
|
||||
<%= link_to "##{card.number} #{card.title}", card_url(card) %>
|
||||
<%= link_to "##{card.number} #{card.title}", card_path(card) %>
|
||||
</template>
|
||||
</lexxy-prompt-item>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<% @accounts.each do |account| %>
|
||||
<li class="popup__item txt-medium">
|
||||
<%= icon_tag "marker", class: "popup__icon" %>
|
||||
<%= link_to landing_url(script_name: account.slug), class: "btn overflow-ellipsis fill-transparent popup__btn" do %>
|
||||
<%= link_to landing_path(script_name: account.slug), class: "btn overflow-ellipsis fill-transparent popup__btn" do %>
|
||||
<strong><%= account.name %></strong>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<% if Current.user == @user %>
|
||||
<hr class="separator--horizontal full-width flex-item-grow margin-block-start-double" style="--border-color: var(--color-ink-light);" aria-hidden="true">
|
||||
|
||||
<%= button_to session_url(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %>
|
||||
<%= button_to session_path(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %>
|
||||
<span>Sign out of Fizzy on this device</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= @event.description_for(Current.user).to_plain_text %>
|
||||
<% if @event.eventable %>
|
||||
<%= link_to "↗︎", polymorphic_url(@event.eventable) %>
|
||||
<%= link_to "↗︎", polymorphic_url(@event.eventable, only_path: true) %>
|
||||
<% end %>
|
||||
|
||||
@@ -8,6 +8,17 @@ ActiveSupport.on_load(:active_storage_blob) do
|
||||
end
|
||||
end
|
||||
|
||||
ActiveSupport.on_load(:action_text_content) do
|
||||
# Install our extensions after ActionText::Engine's
|
||||
ActiveSupport.on_load(:active_storage_blob) do
|
||||
# Ensure all <action-text-attachment>s have a "url" attribute that's a relative
|
||||
# path (for portability across host name changes, beta environments, etc).
|
||||
def to_rich_text_attributes(*)
|
||||
super.merge url: Rails.application.routes.url_helpers.polymorphic_url(self, only_path: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Don't configure replica connections for ActiveStorage::Record.
|
||||
# When ActiveStorage uses `connects_to`, it creates a separate connection pool
|
||||
# from ApplicationRecord. This causes after_commit callbacks to fire in
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# Convert absolute attachment URLs in rich text content to relative paths.
|
||||
# This fixes URLs that were stored with full hostnames (e.g., https://app.fizzy.do/...)
|
||||
# making them portable across beta environments and host changes.
|
||||
#
|
||||
# MUST BE RUN AFTER `decrypt!` when using ActiveRecord Encryption
|
||||
#
|
||||
# Run locally:
|
||||
# bin/rails runner script/migrations/convert-absolute-attachment-urls-to-relative.rb --help
|
||||
#
|
||||
# Run via Kamal:
|
||||
# kamal app exec -d <stage> -p --reuse "bin/rails runner script/migrations/convert-absolute-attachment-urls-to-relative.rb --help"
|
||||
#
|
||||
# Safe to re-run: won't modify already-relative URLs
|
||||
|
||||
class ConvertAbsoluteAttachmentUrlsToRelative
|
||||
# Match absolute URLs pointing to Active Storage routes, keeping the account slug
|
||||
ABSOLUTE_URL_PATTERN = %r{https?://[^/]+(/\d+/rails/active_storage/[^"']+)}
|
||||
|
||||
attr_reader :account, :dry_run
|
||||
|
||||
def initialize(account_id: nil, dry_run: false)
|
||||
@account = Account.find_by(external_account_id: account_id)
|
||||
@dry_run = dry_run
|
||||
end
|
||||
|
||||
def run
|
||||
puts "Converting absolute attachment URLs to relative paths"
|
||||
puts dry_run ? "DRY RUN MODE - no changes will be saved" : "LIVE MODE - changes will be saved"
|
||||
puts account ? "Only account: #{account.external_account_id} - #{account.name}" : "For **ALL ACCOUNTS**"
|
||||
|
||||
puts "\nPress ENTER to continue running or CTRL-C to bail..."
|
||||
gets
|
||||
|
||||
puts "\nRunning..."
|
||||
|
||||
# Suppress SQL logs
|
||||
Rails.event.debug_mode = false
|
||||
|
||||
seconds = Benchmark.realtime do
|
||||
suppressing_turbo_broadcasts do
|
||||
convert_urls
|
||||
end
|
||||
end
|
||||
|
||||
puts "\n\n"
|
||||
puts "Finished in %.2f seconds." % seconds
|
||||
end
|
||||
|
||||
private
|
||||
def suppressing_turbo_broadcasts
|
||||
Board.suppressing_turbo_broadcasts do
|
||||
Card.suppressing_turbo_broadcasts do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def convert_urls
|
||||
scanned = 0
|
||||
fixed = 0
|
||||
urls_converted = 0
|
||||
|
||||
action_texts_scope.find_each do |rich_text|
|
||||
scanned += 1
|
||||
|
||||
body = rich_text.body
|
||||
|
||||
edited = false
|
||||
conversions = 0
|
||||
|
||||
body.send(:attachment_nodes).each do |node|
|
||||
url = node["url"]
|
||||
next unless url
|
||||
|
||||
if url.match?(ABSOLUTE_URL_PATTERN)
|
||||
node["url"] = url.gsub(ABSOLUTE_URL_PATTERN, '\1')
|
||||
edited = true
|
||||
conversions += 1
|
||||
end
|
||||
end
|
||||
|
||||
if edited
|
||||
record = rich_text.record
|
||||
puts " - modifying #{record.class.name} #{record.to_param} (account: #{record.account&.external_account_id}) - #{conversions} URL(s)"
|
||||
|
||||
unless dry_run
|
||||
rich_text.update! body: body.fragment.to_html
|
||||
end
|
||||
|
||||
fixed += 1
|
||||
urls_converted += conversions
|
||||
end
|
||||
end
|
||||
|
||||
puts "\n\nConversion complete!"
|
||||
puts " Rich texts examined: #{scanned}"
|
||||
puts " Rich texts modified: #{fixed}"
|
||||
puts " URLs converted: #{urls_converted}"
|
||||
end
|
||||
|
||||
def action_texts_scope
|
||||
# Only examine rich texts that have embedded attachments
|
||||
scope = ActionText::RichText.joins(:embeds_attachments)
|
||||
scope = scope.where(account: account) if account
|
||||
scope
|
||||
end
|
||||
end
|
||||
|
||||
require "optparse"
|
||||
|
||||
options = { account_id: nil, dry_run: true }
|
||||
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Usage: bin/rails runner #{__FILE__} [options]"
|
||||
|
||||
opts.on("-a", "--account ACCOUNT_ID", "Restrict to a specific account (external_account_id)") do |id|
|
||||
options[:account_id] = id
|
||||
end
|
||||
|
||||
opts.on("--[no-]dry-run", "Run in dry-run mode (default: --dry-run)") do |v|
|
||||
options[:dry_run] = v
|
||||
end
|
||||
|
||||
opts.on("-h", "--help", "Show this help message") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
end.parse!
|
||||
|
||||
ConvertAbsoluteAttachmentUrlsToRelative.new(**options).run
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# Rollback script: Convert relative attachment URLs back to absolute URLs.
|
||||
# Use this if you need to rollback the relative URL changes and want to
|
||||
# convert rich texts created during the rollout back to absolute URLs.
|
||||
#
|
||||
# Run locally:
|
||||
# bin/rails runner script/migrations/convert-relative-attachment-urls-to-absolute.rb --help
|
||||
#
|
||||
# Run via Kamal:
|
||||
# kamal app exec -d <stage> -p --reuse "bin/rails runner script/migrations/convert-relative-attachment-urls-to-absolute.rb --help"
|
||||
|
||||
class ConvertRelativeAttachmentUrlsToAbsolute
|
||||
# Match relative URLs pointing to Active Storage routes (with account slug)
|
||||
RELATIVE_URL_PATTERN = %r{\A(/\d+/rails/active_storage/[^"']+)\z}
|
||||
|
||||
attr_reader :host, :since
|
||||
|
||||
def initialize(host:, since:)
|
||||
@host = host
|
||||
@since = since
|
||||
end
|
||||
|
||||
def run
|
||||
puts "Converting relative attachment URLs to absolute URLs"
|
||||
puts "Host: #{host}"
|
||||
puts "Processing rich texts created since: #{since}"
|
||||
|
||||
puts "\nPress ENTER to continue running or CTRL-C to bail..."
|
||||
gets
|
||||
|
||||
puts "\nRunning..."
|
||||
|
||||
seconds = Benchmark.realtime do
|
||||
suppressing_turbo_broadcasts do
|
||||
convert_urls
|
||||
end
|
||||
end
|
||||
|
||||
puts "\n\n"
|
||||
puts "Finished in %.2f seconds." % seconds
|
||||
end
|
||||
|
||||
private
|
||||
def suppressing_turbo_broadcasts
|
||||
Board.suppressing_turbo_broadcasts do
|
||||
Card.suppressing_turbo_broadcasts do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def convert_urls
|
||||
scanned = 0
|
||||
fixed = 0
|
||||
urls_converted = 0
|
||||
|
||||
action_texts_scope.find_each do |rich_text|
|
||||
scanned += 1
|
||||
|
||||
body = rich_text.body
|
||||
|
||||
edited = false
|
||||
conversions = 0
|
||||
|
||||
body.send(:attachment_nodes).each do |node|
|
||||
url = node["url"]
|
||||
next unless url
|
||||
|
||||
if url.match?(RELATIVE_URL_PATTERN)
|
||||
node["url"] = "#{host}#{url}"
|
||||
edited = true
|
||||
conversions += 1
|
||||
end
|
||||
end
|
||||
|
||||
if edited
|
||||
record = rich_text.record
|
||||
puts " - modifying #{record.class.name} #{record.to_param} (account: #{record.account&.external_account_id}) - #{conversions} URL(s)"
|
||||
|
||||
rich_text.update! body: body.fragment.to_html
|
||||
|
||||
fixed += 1
|
||||
urls_converted += conversions
|
||||
end
|
||||
end
|
||||
|
||||
puts "\n\nConversion complete!"
|
||||
puts " Rich texts examined: #{scanned}"
|
||||
puts " Rich texts modified: #{fixed}"
|
||||
puts " URLs converted: #{urls_converted}"
|
||||
end
|
||||
|
||||
def action_texts_scope
|
||||
ActionText::RichText.joins(:embeds_attachments).where("action_text_rich_texts.created_at >= ?", since)
|
||||
end
|
||||
end
|
||||
|
||||
require "optparse"
|
||||
require "time"
|
||||
|
||||
options = {}
|
||||
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Usage: bin/rails runner #{__FILE__} [options]"
|
||||
|
||||
opts.on("--host HOST", "Host to prepend (e.g., https://app.fizzy.do)") do |host|
|
||||
options[:host] = host
|
||||
end
|
||||
|
||||
opts.on("--since TIME", "Process rich texts created since this time (ISO 8601 format)") do |time|
|
||||
options[:since] = Time.parse(time)
|
||||
end
|
||||
|
||||
opts.on("-h", "--help", "Show this help message") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
end.parse!
|
||||
|
||||
if options[:host].nil? || options[:since].nil?
|
||||
puts "Error: --host and --since are required"
|
||||
puts "Example: bin/rails runner #{__FILE__} --host https://app.fizzy.do --since 2026-01-14T10:00:00Z"
|
||||
exit 1
|
||||
end
|
||||
|
||||
ConvertRelativeAttachmentUrlsToAbsolute.new(**options).run
|
||||
Reference in New Issue
Block a user