diff --git a/Gemfile b/Gemfile index 64a32d01a..85052847e 100644 --- a/Gemfile +++ b/Gemfile @@ -24,9 +24,10 @@ gem "thruster", require: false gem "bcrypt", "~> 3.1.7" gem "geared_pagination", "~> 1.2" gem "rqrcode" -gem "redcarpet" gem "rouge" gem "jbuilder" +gem "actiontext-lexical", path: "/Users/jorge/Work/jorgemanrubia/actiontext-lexical" +gem "image_processing", "~> 1.14" # Telemetry and logging gem "sentry-ruby" @@ -51,3 +52,4 @@ group :test do gem "webmock" gem "vcr" end + diff --git a/Gemfile.lock b/Gemfile.lock index b8dd7eebb..a0a44f206 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/active_record-tenanted - revision: 612f5e8b7f77b7076a7eb21521920630161461aa + revision: 173048c2af29e9a723811e552bc59e3154e82e2a specs: active_record-tenanted (0.1.0) activerecord (>= 8.1.alpha) @@ -128,6 +128,16 @@ GIT thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) +PATH + remote: /Users/jorge/Work/jorgemanrubia/actiontext-lexical + specs: + actiontext-lexical (0.1.0) + capybara + cuprite + image_processing + rails (>= 8.0.2) + selenium-webdriver + GEM remote: https://rubygems.org/ specs: @@ -163,13 +173,17 @@ GEM bigdecimal rexml crass (1.0.6) + cuprite (0.17) + capybara (~> 3.0) + ferrum (~> 0.17.0) date (3.4.1) debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) dotenv (3.1.8) - drb (2.2.1) + drb (2.2.3) ed25519 (1.4.0) + erb (5.0.1) erubi (1.13.1) et-orbi (1.2.11) tzinfo @@ -184,6 +198,16 @@ GEM net-http (>= 0.5.0) faraday-retry (2.3.1) faraday (~> 2.0) + ferrum (0.17.1) + addressable (~> 2.5) + base64 (~> 0.2) + concurrent-ruby (~> 1.1) + webrick (~> 1.7) + websocket-driver (~> 0.7) + ffi (1.17.2) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -195,6 +219,9 @@ GEM hashdiff (1.1.2) i18n (1.14.7) concurrent-ruby (~> 1.0) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) importmap-rails (2.1.0) actionpack (>= 6.0.0) activesupport (>= 6.0.0) @@ -222,7 +249,7 @@ GEM language_server-protocol (3.17.0.4) lint_roller (1.1.0) logger (1.7.0) - loofah (2.24.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -232,8 +259,11 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + mini_magick (5.2.0) + benchmark + logger mini_mime (1.1.5) - mini_portile2 (2.8.8) + mini_portile2 (2.8.9) minitest (5.25.5) msgpack (1.8.0) multipart-post (2.4.1) @@ -277,7 +307,7 @@ GEM activesupport (>= 7.0.0) rack railties (>= 7.0.0) - psych (5.2.3) + psych (5.2.6) date stringio public_suffix (6.0.1) @@ -285,7 +315,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.1.14) + rack (3.1.15) rack-contrib (2.5.0) rack (< 4) rack-session (2.1.1) @@ -295,7 +325,7 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) - rails-dom-testing (2.2.0) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) @@ -305,9 +335,9 @@ GEM rainbow (3.1.1) rake (13.2.1) rb_sys (0.9.106) - rdoc (6.13.1) + rdoc (6.14.0) + erb psych (>= 4.0.0) - redcarpet (3.6.1) regexp_parser (2.10.0) reline (0.6.1) io-console (~> 0.5) @@ -346,6 +376,9 @@ GEM rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) + ruby-vips (2.2.3) + ffi (~> 1.12) + logger rubyzip (2.4.1) securerandom (0.4.1) selenium-webdriver (4.31.0) @@ -423,6 +456,7 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) + webrick (1.9.1) websocket (1.2.11) websocket-driver (0.7.7) base64 @@ -430,7 +464,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.2) + zeitwerk (2.7.3) PLATFORMS arm64-darwin @@ -440,6 +474,7 @@ PLATFORMS x86_64-mingw32 DEPENDENCIES + actiontext-lexical! active_record-tenanted! bcrypt (~> 3.1.7) bootsnap @@ -448,6 +483,7 @@ DEPENDENCIES capybara debug geared_pagination (~> 1.2) + image_processing (~> 1.14) importmap-rails jbuilder kamal @@ -455,7 +491,6 @@ DEPENDENCIES puma (>= 5.0) rails! rails_structured_logging! - redcarpet rouge rqrcode rubocop-rails-omakase diff --git a/app/assets/stylesheets/actiontext.css b/app/assets/stylesheets/actiontext.css new file mode 100644 index 000000000..9b6bcb064 --- /dev/null +++ b/app/assets/stylesheets/actiontext.css @@ -0,0 +1,440 @@ +/* + * Default Trix editor styles. See Action Text overwrites below. +*/ + +trix-editor { + border: 1px solid #bbb; + border-radius: 3px; + margin: 0; + padding: 0.4em 0.6em; + min-height: 5em; + outline: none; } + +trix-toolbar * { + box-sizing: border-box; } + +trix-toolbar .trix-button-row { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + overflow-x: auto; } + +trix-toolbar .trix-button-group { + display: flex; + margin-bottom: 10px; + border: 1px solid #bbb; + border-top-color: #ccc; + border-bottom-color: #888; + border-radius: 3px; } + trix-toolbar .trix-button-group:not(:first-child) { + margin-left: 1.5vw; } + @media (max-width: 768px) { + trix-toolbar .trix-button-group:not(:first-child) { + margin-left: 0; } } + +trix-toolbar .trix-button-group-spacer { + flex-grow: 1; } + @media (max-width: 768px) { + trix-toolbar .trix-button-group-spacer { + display: none; } } + +trix-toolbar .trix-button { + position: relative; + float: left; + color: rgba(0, 0, 0, 0.6); + font-size: 0.75em; + font-weight: 600; + white-space: nowrap; + padding: 0 0.5em; + margin: 0; + outline: none; + border: none; + border-bottom: 1px solid #ddd; + border-radius: 0; + background: transparent; } + trix-toolbar .trix-button:not(:first-child) { + border-left: 1px solid #ccc; } + trix-toolbar .trix-button.trix-active { + background: #cbeefa; + color: black; } + trix-toolbar .trix-button:not(:disabled) { + cursor: pointer; } + trix-toolbar .trix-button:disabled { + color: rgba(0, 0, 0, 0.125); } + @media (max-width: 768px) { + trix-toolbar .trix-button { + letter-spacing: -0.01em; + padding: 0 0.3em; } } + +trix-toolbar .trix-button--icon { + font-size: inherit; + width: 2.6em; + height: 1.6em; + max-width: calc(0.8em + 4vw); + text-indent: -9999px; } + @media (max-width: 768px) { + trix-toolbar .trix-button--icon { + height: 2em; + max-width: calc(0.8em + 3.5vw); } } + trix-toolbar .trix-button--icon::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.6; + content: ""; + background-position: center; + background-repeat: no-repeat; + background-size: contain; } + @media (max-width: 768px) { + trix-toolbar .trix-button--icon::before { + right: 6%; + left: 6%; } } + trix-toolbar .trix-button--icon.trix-active::before { + opacity: 1; } + trix-toolbar .trix-button--icon:disabled::before { + opacity: 0.125; } + +trix-toolbar .trix-button--icon-attach::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M10.5%2018V7.5c0-2.25%203-2.25%203%200V18c0%204.125-6%204.125-6%200V7.5c0-6.375%209-6.375%209%200V18%22%20stroke%3D%22%23000%22%20stroke-width%3D%222%22%20stroke-miterlimit%3D%2210%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E"); + top: 8%; + bottom: 4%; } + +trix-toolbar .trix-button--icon-bold::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M6.522%2019.242a.5.5%200%200%201-.5-.5V5.35a.5.5%200%200%201%20.5-.5h5.783c1.347%200%202.46.345%203.24.982.783.64%201.216%201.562%201.216%202.683%200%201.13-.587%202.129-1.476%202.71a.35.35%200%200%200%20.049.613c1.259.56%202.101%201.742%202.101%203.22%200%201.282-.483%202.334-1.363%203.063-.876.726-2.132%201.12-3.66%201.12h-5.89ZM9.27%207.347v3.362h1.97c.766%200%201.347-.17%201.733-.464.38-.291.587-.716.587-1.27%200-.53-.183-.928-.513-1.198-.334-.273-.838-.43-1.505-.43H9.27Zm0%205.606v3.791h2.389c.832%200%201.448-.177%201.853-.497.399-.315.614-.786.614-1.423%200-.62-.22-1.077-.63-1.385-.418-.313-1.053-.486-1.905-.486H9.27Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-italic::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M9%205h6.5v2h-2.23l-2.31%2010H13v2H6v-2h2.461l2.306-10H9V5Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-link::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M18.948%205.258a4.337%204.337%200%200%200-6.108%200L11.217%206.87a.993.993%200%200%200%200%201.41c.392.39%201.027.39%201.418%200l1.623-1.613a2.323%202.323%200%200%201%203.271%200%202.29%202.29%200%200%201%200%203.251l-2.393%202.38a3.021%203.021%200%200%201-4.255%200l-.05-.049a1.007%201.007%200%200%200-1.418%200%20.993.993%200%200%200%200%201.41l.05.049a5.036%205.036%200%200%200%207.091%200l2.394-2.38a4.275%204.275%200%200%200%200-6.072Zm-13.683%2013.6a4.337%204.337%200%200%200%206.108%200l1.262-1.255a.993.993%200%200%200%200-1.41%201.007%201.007%200%200%200-1.418%200L9.954%2017.45a2.323%202.323%200%200%201-3.27%200%202.29%202.29%200%200%201%200-3.251l2.344-2.331a2.579%202.579%200%200%201%203.631%200c.392.39%201.027.39%201.419%200a.993.993%200%200%200%200-1.41%204.593%204.593%200%200%200-6.468%200l-2.345%202.33a4.275%204.275%200%200%200%200%206.072Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-strike::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M6%2014.986c.088%202.647%202.246%204.258%205.635%204.258%203.496%200%205.713-1.728%205.713-4.463%200-.275-.02-.536-.062-.781h-3.461c.398.293.573.654.573%201.123%200%201.035-1.074%201.787-2.646%201.787-1.563%200-2.773-.762-2.91-1.924H6ZM6.432%2010h3.763c-.632-.314-.914-.715-.914-1.273%200-1.045.977-1.739%202.432-1.739%201.475%200%202.52.723%202.617%201.914h2.764c-.05-2.548-2.11-4.238-5.39-4.238-3.145%200-5.392%201.719-5.392%204.316%200%20.363.04.703.12%201.02ZM4%2011a1%201%200%201%200%200%202h15a1%201%200%201%200%200-2H4Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-quote::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M4.581%208.471c.44-.5%201.056-.834%201.758-.995C8.074%207.17%209.201%207.822%2010%208.752c1.354%201.578%201.33%203.555.394%205.277-.941%201.731-2.788%203.163-4.988%203.56a.622.622%200%200%201-.653-.317c-.113-.205-.121-.49.16-.764.294-.286.567-.566.791-.835.222-.266.413-.54.524-.815.113-.28.156-.597.026-.908-.128-.303-.39-.524-.72-.69a3.02%203.02%200%200%201-1.674-2.7c0-.905.283-1.59.72-2.088Zm9.419%200c.44-.5%201.055-.834%201.758-.995%201.734-.306%202.862.346%203.66%201.276%201.355%201.578%201.33%203.555.395%205.277-.941%201.731-2.789%203.163-4.988%203.56a.622.622%200%200%201-.653-.317c-.113-.205-.122-.49.16-.764.294-.286.567-.566.791-.835.222-.266.412-.54.523-.815.114-.28.157-.597.026-.908-.127-.303-.39-.524-.72-.69a3.02%203.02%200%200%201-1.672-2.701c0-.905.283-1.59.72-2.088Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-heading-1::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21.5%207.5v-3h-12v3H14v13h3v-13h4.5ZM9%2013.5h3.5v-3h-10v3H6v7h3v-7Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-code::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3.293%2011.293a1%201%200%200%200%200%201.414l4%204a1%201%200%201%200%201.414-1.414L5.414%2012l3.293-3.293a1%201%200%200%200-1.414-1.414l-4%204Zm13.414%205.414%204-4a1%201%200%200%200%200-1.414l-4-4a1%201%200%201%200-1.414%201.414L18.586%2012l-3.293%203.293a1%201%200%200%200%201.414%201.414Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-bullet-list::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%207.5a1.5%201.5%200%201%200%200-3%201.5%201.5%200%200%200%200%203ZM8%206a1%201%200%200%201%201-1h11a1%201%200%201%201%200%202H9a1%201%200%200%201-1-1Zm1%205a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm0%206a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm-2.5-5a1.5%201.5%200%201%201-3%200%201.5%201.5%200%200%201%203%200ZM5%2019.5a1.5%201.5%200%201%200%200-3%201.5%201.5%200%200%200%200%203Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-number-list::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3%204h2v4H4V5H3V4Zm5%202a1%201%200%200%201%201-1h11a1%201%200%201%201%200%202H9a1%201%200%200%201-1-1Zm1%205a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm0%206a1%201%200%201%200%200%202h11a1%201%200%201%200%200-2H9Zm-3.5-7H6v1l-1.5%202H6v1H3v-1l1.667-2H3v-1h2.5ZM3%2017v-1h3v4H3v-1h2v-.5H4v-1h1V17H3Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-undo::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M3%2014a1%201%200%200%200%201%201h6a1%201%200%201%200%200-2H6.257c2.247-2.764%205.151-3.668%207.579-3.264%202.589.432%204.739%202.356%205.174%205.405a1%201%200%200%200%201.98-.283c-.564-3.95-3.415-6.526-6.825-7.095C11.084%207.25%207.63%208.377%205%2011.39V8a1%201%200%200%200-2%200v6Zm2-1Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-redo::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21%2014a1%201%200%200%201-1%201h-6a1%201%200%201%201%200-2h3.743c-2.247-2.764-5.151-3.668-7.579-3.264-2.589.432-4.739%202.356-5.174%205.405a1%201%200%200%201-1.98-.283c.564-3.95%203.415-6.526%206.826-7.095%203.08-.513%206.534.614%209.164%203.626V8a1%201%200%201%201%202%200v6Zm-2-1Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-decrease-nesting-level::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%206a1%201%200%200%201%201-1h12a1%201%200%201%201%200%202H6a1%201%200%200%201-1-1Zm4%205a1%201%200%201%200%200%202h9a1%201%200%201%200%200-2H9Zm-3%206a1%201%200%201%200%200%202h12a1%201%200%201%200%200-2H6Zm-3.707-5.707a1%201%200%200%200%200%201.414l2%202a1%201%200%201%200%201.414-1.414L4.414%2012l1.293-1.293a1%201%200%200%200-1.414-1.414l-2%202Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-button--icon-increase-nesting-level::before { + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5%206a1%201%200%200%201%201-1h12a1%201%200%201%201%200%202H6a1%201%200%200%201-1-1Zm4%205a1%201%200%201%200%200%202h9a1%201%200%201%200%200-2H9Zm-3%206a1%201%200%201%200%200%202h12a1%201%200%201%200%200-2H6Zm-2.293-2.293%202-2a1%201%200%200%200%200-1.414l-2-2a1%201%200%201%200-1.414%201.414L3.586%2012l-1.293%201.293a1%201%200%201%200%201.414%201.414Z%22%20fill%3D%22%23000%22%2F%3E%3C%2Fsvg%3E"); } + +trix-toolbar .trix-dialogs { + position: relative; } + +trix-toolbar .trix-dialog { + position: absolute; + top: 0; + left: 0; + right: 0; + font-size: 0.75em; + padding: 15px 10px; + background: #fff; + box-shadow: 0 0.3em 1em #ccc; + border-top: 2px solid #888; + border-radius: 5px; + z-index: 5; } + +trix-toolbar .trix-input--dialog { + font-size: inherit; + font-weight: normal; + padding: 0.5em 0.8em; + margin: 0 10px 0 0; + border-radius: 3px; + border: 1px solid #bbb; + background-color: #fff; + box-shadow: none; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; } + trix-toolbar .trix-input--dialog.validate:invalid { + box-shadow: #F00 0px 0px 1.5px 1px; } + +trix-toolbar .trix-button--dialog { + font-size: inherit; + padding: 0.5em; + border-bottom: none; } + +trix-toolbar .trix-dialog--link { + max-width: 600px; } + +trix-toolbar .trix-dialog__link-fields { + display: flex; + align-items: baseline; } + trix-toolbar .trix-dialog__link-fields .trix-input { + flex: 1; } + trix-toolbar .trix-dialog__link-fields .trix-button-group { + flex: 0 0 content; + margin: 0; } + +trix-editor [data-trix-mutable]:not(.attachment__caption-editor) { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + +trix-editor [data-trix-mutable]::-moz-selection, +trix-editor [data-trix-cursor-target]::-moz-selection, trix-editor [data-trix-mutable] ::-moz-selection { + background: none; } + +trix-editor [data-trix-mutable]::selection, +trix-editor [data-trix-cursor-target]::selection, trix-editor [data-trix-mutable] ::selection { + background: none; } + +trix-editor .attachment__caption-editor:focus[data-trix-mutable]::-moz-selection { + background: highlight; } + +trix-editor .attachment__caption-editor:focus[data-trix-mutable]::selection { + background: highlight; } + +trix-editor [data-trix-mutable].attachment.attachment--file { + box-shadow: 0 0 0 2px highlight; + border-color: transparent; } + +trix-editor [data-trix-mutable].attachment img { + box-shadow: 0 0 0 2px highlight; } + +trix-editor .attachment { + position: relative; } + trix-editor .attachment:hover { + cursor: default; } + +trix-editor .attachment--preview .attachment__caption:hover { + cursor: text; } + +trix-editor .attachment__progress { + position: absolute; + z-index: 1; + height: 20px; + top: calc(50% - 10px); + left: 5%; + width: 90%; + opacity: 0.9; + transition: opacity 200ms ease-in; } + trix-editor .attachment__progress[value="100"] { + opacity: 0; } + +trix-editor .attachment__caption-editor { + display: inline-block; + width: 100%; + margin: 0; + padding: 0; + font-size: inherit; + font-family: inherit; + line-height: inherit; + color: inherit; + text-align: center; + vertical-align: top; + border: none; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; } + +trix-editor .attachment__toolbar { + position: absolute; + z-index: 1; + top: -0.9em; + left: 0; + width: 100%; + text-align: center; } + +trix-editor .trix-button-group { + display: inline-flex; } + +trix-editor .trix-button { + position: relative; + float: left; + color: #666; + white-space: nowrap; + font-size: 80%; + padding: 0 0.8em; + margin: 0; + outline: none; + border: none; + border-radius: 0; + background: transparent; } + trix-editor .trix-button:not(:first-child) { + border-left: 1px solid #ccc; } + trix-editor .trix-button.trix-active { + background: #cbeefa; } + trix-editor .trix-button:not(:disabled) { + cursor: pointer; } + +trix-editor .trix-button--remove { + text-indent: -9999px; + display: inline-block; + padding: 0; + outline: none; + width: 1.8em; + height: 1.8em; + line-height: 1.8em; + border-radius: 50%; + background-color: #fff; + border: 2px solid highlight; + box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.25); } + trix-editor .trix-button--remove::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.7; + content: ""; + background-image: url("data:image/svg+xml,%3Csvg%20height%3D%2224%22%20width%3D%2224%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M19%206.41%2017.59%205%2012%2010.59%206.41%205%205%206.41%2010.59%2012%205%2017.59%206.41%2019%2012%2013.41%2017.59%2019%2019%2017.59%2013.41%2012z%22%2F%3E%3Cpath%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E"); + background-position: center; + background-repeat: no-repeat; + background-size: 90%; } + trix-editor .trix-button--remove:hover { + border-color: #333; } + trix-editor .trix-button--remove:hover::before { + opacity: 1; } + +trix-editor .attachment__metadata-container { + position: relative; } + +trix-editor .attachment__metadata { + position: absolute; + left: 50%; + top: 2em; + transform: translate(-50%, 0); + max-width: 90%; + padding: 0.1em 0.6em; + font-size: 0.8em; + color: #fff; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 3px; } + trix-editor .attachment__metadata .attachment__name { + display: inline-block; + max-width: 100%; + vertical-align: bottom; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + trix-editor .attachment__metadata .attachment__size { + margin-left: 0.2em; + white-space: nowrap; } + +.trix-content { + line-height: 1.5; + overflow-wrap: break-word; + word-break: break-word; } + .trix-content * { + box-sizing: border-box; + margin: 0; + padding: 0; } + .trix-content h1 { + font-size: 1.2em; + line-height: 1.2; } + .trix-content blockquote { + border: 0 solid #ccc; + border-left-width: 0.3em; + margin-left: 0.3em; + padding-left: 0.6em; } + .trix-content [dir=rtl] blockquote, + .trix-content blockquote[dir=rtl] { + border-width: 0; + border-right-width: 0.3em; + margin-right: 0.3em; + padding-right: 0.6em; } + .trix-content li { + margin-left: 1em; } + .trix-content [dir=rtl] li { + margin-right: 1em; } + .trix-content pre { + display: inline-block; + width: 100%; + vertical-align: top; + font-family: monospace; + font-size: 0.9em; + padding: 0.5em; + white-space: pre; + background-color: #eee; + overflow-x: auto; } + .trix-content img { + max-width: 100%; + height: auto; } + .trix-content .attachment { + display: inline-block; + position: relative; + max-width: 100%; } + .trix-content .attachment a { + color: inherit; + text-decoration: none; } + .trix-content .attachment a:hover, .trix-content .attachment a:visited:hover { + color: inherit; } + .trix-content .attachment__caption { + text-align: center; } + .trix-content .attachment__caption .attachment__name + .attachment__size::before { + content: ' \2022 '; } + .trix-content .attachment--preview { + width: 100%; + text-align: center; } + .trix-content .attachment--preview .attachment__caption { + color: #666; + font-size: 0.9em; + line-height: 1.2; } + .trix-content .attachment--file { + color: #333; + line-height: 1; + margin: 0 2px 2px 2px; + padding: 0.4em 1em; + border: 1px solid #bbb; + border-radius: 5px; } + .trix-content .attachment-gallery { + display: flex; + flex-wrap: wrap; + position: relative; } + .trix-content .attachment-gallery .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; } + .trix-content .attachment-gallery.attachment-gallery--2 .attachment, .trix-content .attachment-gallery.attachment-gallery--4 .attachment { + flex-basis: 50%; + max-width: 50%; } + +/* + * We need to override trix.css’s image gallery styles to accommodate the + * element we wrap around attachments. Otherwise, + * images in galleries will be squished by the max-width: 33%; rule. +*/ +.trix-content .attachment-gallery > action-text-attachment, +.trix-content .attachment-gallery > .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; +} + +.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--4 > .attachment { + flex-basis: 50%; + max-width: 50%; +} + +.trix-content action-text-attachment .attachment { + padding: 0 !important; + max-width: 100% !important; +} diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 57fd63eff..8dab7c110 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -58,7 +58,7 @@ module NotificationsHelper def comment_notification_body(event) comment = event.eventable - "#{strip_tags(comment.body_html).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body_html).truncate(200)}" + "#{strip_tags(comment.body.to_s).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body.to_s).truncate(200)}" end def card_notification_title(card) diff --git a/app/helpers/rich_textarea_helper.rb b/app/helpers/rich_textarea_helper.rb new file mode 100644 index 000000000..c6651ac2e --- /dev/null +++ b/app/helpers/rich_textarea_helper.rb @@ -0,0 +1,29 @@ +module RichTextareaHelper + def rich_textarea_toolbar(id = "editor-toolbar") + content_tag("lexical-toolbar", id: id) do + safe_join([ + # Inline formatting + rich_textarea_toolbar_button("Bold", "bold"), + rich_textarea_toolbar_button("Italic", "italic"), + rich_textarea_toolbar_button("Link", "link"), + + # Block-level formatting + rich_textarea_toolbar_button("Heading", "formatElement", data: { payload: "h2" }), + rich_textarea_toolbar_button("Subheading", "formatElement", data: { payload: "h3" }), + rich_textarea_toolbar_button("Code Block", "insertCodeBlock"), + + # Lists + rich_textarea_toolbar_button("• Bullet List", "insertUnorderedList"), + rich_textarea_toolbar_button("1. Numbered List", "insertOrderedList"), + + # Attachments + rich_textarea_toolbar_button("Upload Attachment", "uploadAttachments") + ]) + end + end + + def rich_textarea_toolbar_button(label, command, data: {}, **properties) + data[:command] = command + content_tag(:button, label, type: "button", data: data, **properties) + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index c2917aa3e..df3dda654 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -3,3 +3,6 @@ import "@hotwired/turbo-rails" import "initializers" import "controllers" import "house" + +import "actiontext-lexical" +import "@rails/actiontext" diff --git a/app/javascript/controllers/local_save_controller.js b/app/javascript/controllers/local_save_controller.js index 5d9b89a72..26da25f43 100644 --- a/app/javascript/controllers/local_save_controller.js +++ b/app/javascript/controllers/local_save_controller.js @@ -30,9 +30,11 @@ export default class extends Controller { async restoreContent() { await nextFrame() - const savedContent = localStorage.getItem(this.keyValue) + let savedContent = localStorage.getItem(this.keyValue) if (savedContent) { + savedContent = `
${savedContent}
` // temporary for old markdown saves + console.debug("Es", savedContent); this.inputTarget.value = savedContent this.#triggerChangeEvent(savedContent) } diff --git a/app/models/card.rb b/app/models/card.rb index 8707b2bf2..69588ea88 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -9,7 +9,7 @@ class Card < ApplicationRecord has_many :comments, dependent: :destroy has_one_attached :image, dependent: :purge_later - has_markdown :description + has_rich_text :description before_save :set_default_title, if: :published? diff --git a/app/models/comment.rb b/app/models/comment.rb index ebb1229a4..f62f457a0 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -5,7 +5,7 @@ class Comment < ApplicationRecord belongs_to :creator, class_name: "User", default: -> { Current.user } has_many :reactions, dependent: :delete_all - has_markdown :body + has_rich_text :body searchable_by :body_plain_text, using: :comments_search_index, as: :body scope :chronologically, -> { order created_at: :asc, id: :desc } @@ -19,6 +19,10 @@ class Comment < ApplicationRecord end private + def body_plain_text + body.to_plain_text + end + def watch_card_by_creator card.watch_by creator end diff --git a/app/models/concerns/mentions.rb b/app/models/concerns/mentions.rb index 08e0362cb..e8efb550c 100644 --- a/app/models/concerns/mentions.rb +++ b/app/models/concerns/mentions.rb @@ -14,7 +14,7 @@ module Mentions end def mentionable_content - markdown_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ") + rich_text_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ") end private @@ -32,12 +32,12 @@ module Mentions collection.users end - def markdown_associations - self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::Markdown } + def rich_text_associations + self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::RichText } end def mentionable_content_changed? - markdown_associations.any? { send(it.name).content_previously_changed? } + rich_text_associations.any? { send(it.name).body_previously_changed? } end def create_mentions_later diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb new file mode 100644 index 000000000..49ba357dd --- /dev/null +++ b/app/views/active_storage/blobs/_blob.html.erb @@ -0,0 +1,14 @@ +
attachment--<%= blob.filename.extension %>"> + <% if blob.representable? %> + <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %> + <% end %> + +
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <%= number_to_human_size blob.byte_size %> + <% end %> +
+
diff --git a/app/views/cards/comments/_comment.html.erb b/app/views/cards/comments/_comment.html.erb index 9c41c81d6..02787e49b 100644 --- a/app/views/cards/comments/_comment.html.erb +++ b/app/views/cards/comments/_comment.html.erb @@ -29,7 +29,7 @@
- <%= sanitize comment.body_html %> + <%= comment.body %>
<%= render "cards/comments/reactions/reactions", comment: comment %> diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb index c4645fe86..0a41b43cf 100644 --- a/app/views/cards/comments/_new.html.erb +++ b/app/views/cards/comments/_new.html.erb @@ -9,7 +9,8 @@ data: { controller: "form paste local-save", local_save_key_value: "comment-#{card.id}", action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> - <%= form.markdown_area :body, class: "input comment__input", required: true, placeholder: new_comment_placeholder(card), + <%= rich_textarea_toolbar "editor-toolbar" %> + <%= form.rich_textarea :body, class: "input comment__input", toolbar: "editor-toolbar", required: true, placeholder: new_comment_placeholder(card), data: { local_save_target: "input", action: "house-md:change->local-save#save turbo:morph-element->local-save#restoreContent" } %> <%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %> Post this comment diff --git a/app/views/cards/comments/edit.html.erb b/app/views/cards/comments/edit.html.erb index 342afc590..7fa50ec44 100644 --- a/app/views/cards/comments/edit.html.erb +++ b/app/views/cards/comments/edit.html.erb @@ -8,7 +8,8 @@
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width", data: { controller: "form paste", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> - <%= form.markdown_area :body, class: "input comment__input", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %> + <%= rich_textarea_toolbar "editor-toolbar" %> + <%= form.rich_textarea :body, class: "input comment__input", toolbar: "editor-toolbar", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %>
<%= form.button class: "btn btn--reversed", type: :submit do %> Save changes diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index 4e4c2c9f5..ed3e8f489 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -5,7 +5,7 @@ <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>
- <%= sanitize card.description_html %> + <%= card.description %>
<% end %> @@ -18,7 +18,9 @@ data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
- <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + <%= rich_textarea_toolbar "editor-toolbar" %> + <%= form.rich_textarea :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + toolbar: "the-toolbar", placeholder: "Add some notes, context, pictures, or video about this…", data: { action: "house-md:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index 499563bb4..cd955a621 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -8,10 +8,13 @@ data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> <% end %> -
- <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", - placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> + + <%= rich_textarea_toolbar "editor-toolbar" %> + + <%= form.rich_textarea :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + toolbar: "editor-toolbar", + placeholder: "Add some notes, context, pictures, or video about this…", + data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
<%= form.button "Save changes", type: :submit, class: "btn" %> <%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %> diff --git a/app/views/events/event/_comment.html.erb b/app/views/events/event/_comment.html.erb index 22f4596e8..b437349bb 100644 --- a/app/views/events/event/_comment.html.erb +++ b/app/views/events/event/_comment.html.erb @@ -1,5 +1,5 @@ <%= render "events/event/layout", card: event.eventable.card, event: event do %> - <%= strip_tags(event&.eventable.body_html).truncate(200) -%> + <%= strip_tags(event&.eventable.body.to_s).truncate(200) -%> <% end %> diff --git a/app/views/layouts/action_text/contents/_content.html.erb b/app/views/layouts/action_text/contents/_content.html.erb new file mode 100644 index 000000000..0dec72775 --- /dev/null +++ b/app/views/layouts/action_text/contents/_content.html.erb @@ -0,0 +1,3 @@ +
+ <%= yield -%> +
diff --git a/config/importmap.rb b/config/importmap.rb index 65dc7629c..ead746d7e 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -11,3 +11,6 @@ pin_all_from "app/javascript/controllers", under: "controllers" pin_all_from "app/javascript/helpers", under: "helpers" pin_all_from "app/javascript/initializers", under: "initializers" pin "marked" # @15.0.11 +pin "actiontext-lexical" +pin "@rails/activestorage", to: "activestorage.esm.js" +pin "@rails/actiontext", to: "actiontext.esm.js" diff --git a/config/initializers/markdown.rb b/config/initializers/markdown.rb deleted file mode 100644 index 04d5c8e78..000000000 --- a/config/initializers/markdown.rb +++ /dev/null @@ -1,7 +0,0 @@ -ActiveSupport.on_load :action_text_markdown do - require "markdown_renderer" - require "redcarpet/render_strip" - - ActionText::Markdown.html_renderer = ->(content) { MarkdownRenderer.build.render(content) } - ActionText::Markdown.plain_text_renderer = Redcarpet::Markdown.new(Redcarpet::Render::StripDown) -end diff --git a/db/migrate/20250523084734_create_action_text_tables.action_text.rb b/db/migrate/20250523084734_create_action_text_tables.action_text.rb new file mode 100644 index 000000000..c1c0a9d8c --- /dev/null +++ b/db/migrate/20250523084734_create_action_text_tables.action_text.rb @@ -0,0 +1,26 @@ +# This migration comes from action_text (originally 20180528164100) +class CreateActionTextTables < ActiveRecord::Migration[6.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :action_text_rich_texts, id: primary_key_type do |t| + t.string :name, null: false + t.text :body, size: :long + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + + t.timestamps + + t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [ primary_key_type, foreign_key_type ] + end +end diff --git a/db/schema.rb b/db/schema.rb index 834b2bcc9..098674f3a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_05_15_125505) do +ActiveRecord::Schema[8.1].define(version: 2025_05_23_084734) do create_table "accesses", force: :cascade do |t| t.integer "collection_id", null: false t.datetime "created_at", null: false @@ -39,6 +39,16 @@ ActiveRecord::Schema[8.1].define(version: 2025_05_15_125505) do t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record" end + create_table "action_text_rich_texts", force: :cascade do |t| + t.text "body" + t.datetime "created_at", null: false + t.string "name", null: false + t.bigint "record_id", null: false + t.string "record_type", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + create_table "active_storage_attachments", force: :cascade do |t| t.bigint "blob_id", null: false t.datetime "created_at", null: false diff --git a/db/schema_cache.yml b/db/schema_cache.yml index fbaacdc9c..6cd19a67c 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -1,7 +1,7 @@ --- !ruby/object:ActiveRecord::ConnectionAdapters::SchemaCache columns: accesses: - - &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: collection_id cast_type: &1 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer @@ -80,7 +80,7 @@ columns: default_function: collation: comment: - - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &25 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: user_id cast_type: *1 @@ -118,13 +118,13 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: content - cast_type: &14 !ruby/object:ActiveRecord::Type::Text + cast_type: &11 !ruby/object:ActiveRecord::Type::Text true: t false: f precision: scale: limit: - sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: TEXT type: :text limit: @@ -159,17 +159,30 @@ columns: collation: comment: - *9 - active_storage_attachments: + action_text_rich_texts: + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: body + cast_type: *11 + sql_type_metadata: *12 + 'null': true + default: + default_function: + collation: + comment: + - *5 + - *6 + - *10 - &16 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: - name: blob_id - cast_type: &11 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer + name: record_id + cast_type: &14 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer precision: scale: limit: max: 9223372036854775808 min: -9223372036854775808 - sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: bigint type: :integer limit: @@ -180,19 +193,23 @@ columns: default_function: collation: comment: - - *5 - - *6 - - *10 - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - *13 + - *9 + active_storage_attachments: + - &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: - name: record_id - cast_type: *11 - sql_type_metadata: *12 + name: blob_id + cast_type: *14 + sql_type_metadata: *15 'null': false default: default_function: collation: comment: + - *5 + - *6 + - *10 + - *16 - *13 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -208,8 +225,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: byte_size - cast_type: *11 - sql_type_metadata: *12 + cast_type: *14 + sql_type_metadata: *15 'null': false default: default_function: @@ -247,7 +264,7 @@ columns: collation: comment: - *6 - - &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: key cast_type: *7 @@ -260,8 +277,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: metadata - cast_type: *14 - sql_type_metadata: *15 + cast_type: *11 + sql_type_metadata: *12 'null': true default: default_function: @@ -278,7 +295,7 @@ columns: collation: comment: active_storage_variant_records: - - *16 + - *17 - *6 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -292,7 +309,7 @@ columns: comment: ar_internal_metadata: - *5 - - *17 + - *18 - *9 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -305,7 +322,7 @@ columns: collation: comment: assignees_filters: - - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: assignee_id cast_type: *1 @@ -315,7 +332,7 @@ columns: default_function: collation: comment: - - &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: filter_id cast_type: *1 @@ -326,7 +343,7 @@ columns: collation: comment: assigners_filters: - - &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: assigner_id cast_type: *1 @@ -336,11 +353,11 @@ columns: default_function: collation: comment: - - *18 - assignments: - *19 + assignments: - *20 - - &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - *21 + - &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: card_id cast_type: *1 @@ -368,14 +385,14 @@ columns: - *6 - *9 card_goldnesses: - - *21 + - *22 - *5 - *6 - *9 cards: - - *22 + - *23 - *5 - - &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: creator_id cast_type: *1 @@ -428,14 +445,14 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: status - cast_type: *14 - sql_type_metadata: *15 + cast_type: *11 + sql_type_metadata: *12 'null': false default: creating default_function: collation: comment: - - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: title cast_type: *7 @@ -461,7 +478,7 @@ columns: comment: - *9 closures: - - *21 + - *22 - *5 - *6 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column @@ -489,11 +506,11 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: all_access - cast_type: &31 !ruby/object:ActiveModel::Type::Boolean + cast_type: &32 !ruby/object:ActiveModel::Type::Boolean precision: scale: limit: - sql_type_metadata: &32 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &33 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: boolean type: :boolean limit: @@ -507,15 +524,15 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: auto_close_period - cast_type: *11 - sql_type_metadata: *12 + cast_type: *14 + sql_type_metadata: *15 'null': true default: default_function: collation: comment: - *5 - - *23 + - *24 - *6 - *10 - *9 @@ -530,18 +547,18 @@ columns: collation: comment: collections_filters: - - *22 - - *18 + - *23 + - *19 commands: - *5 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: data - cast_type: &25 !ruby/object:ActiveRecord::Type::Json + cast_type: &26 !ruby/object:ActiveRecord::Type::Json precision: scale: limit: - sql_type_metadata: &26 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: json type: :json limit: @@ -556,8 +573,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: line - cast_type: *14 - sql_type_metadata: *15 + cast_type: *11 + sql_type_metadata: *12 'null': true default: default_function: @@ -584,16 +601,16 @@ columns: collation: comment: - *9 - - *24 + - *25 comments: - - *21 + - *22 - *5 - - *23 + - *24 - *6 - *9 creators_filters: - - *23 - - *18 + - *24 + - *19 events: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -605,9 +622,9 @@ columns: default_function: collation: comment: - - *22 - - *5 - *23 + - *5 + - *24 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: eventable_id @@ -632,8 +649,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: particulars - cast_type: *25 - sql_type_metadata: *26 + cast_type: *26 + sql_type_metadata: *27 'null': true default: "{}" default_function: @@ -642,12 +659,12 @@ columns: - *9 filters: - *5 - - *23 + - *24 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: fields - cast_type: *25 - sql_type_metadata: *26 + cast_type: *26 + sql_type_metadata: *27 'null': false default: "{}" default_function: @@ -666,7 +683,7 @@ columns: comment: - *9 filters_stages: - - *18 + - *19 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: stage_id @@ -678,8 +695,8 @@ columns: collation: comment: filters_tags: - - *18 - - &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - *19 + - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: tag_id cast_type: *1 @@ -712,7 +729,7 @@ columns: default_function: collation: comment: - - &27 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: source_id cast_type: *1 @@ -722,7 +739,7 @@ columns: default_function: collation: comment: - - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: source_type cast_type: *7 @@ -756,16 +773,16 @@ columns: default_function: collation: comment: - - *27 - *28 + - *29 - *9 - - *24 + - *25 pins: - - *21 + - *22 - *5 - *6 - *9 - - *24 + - *25 reactions: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -882,24 +899,24 @@ columns: default_function: collation: comment: - - *24 + - *25 taggings: - - *21 + - *22 - *5 - *6 - - *29 + - *30 - *9 tags: - *5 - *6 - - *30 + - *31 - *9 users: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: active - cast_type: *31 - sql_type_metadata: *32 + cast_type: *32 + sql_type_metadata: *33 'null': false default: true default_function: @@ -940,16 +957,16 @@ columns: comment: - *9 watches: - - *21 + - *22 - *5 - *6 - *9 - - *24 + - *25 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: watching - cast_type: *31 - sql_type_metadata: *32 + cast_type: *32 + sql_type_metadata: *33 'null': false default: true default_function: @@ -989,6 +1006,7 @@ primary_keys: accesses: id accounts: id action_text_markdowns: id + action_text_rich_texts: id active_storage_attachments: id active_storage_blobs: id active_storage_variant_records: id @@ -1027,6 +1045,7 @@ data_sources: accesses: true accounts: true action_text_markdowns: true + action_text_rich_texts: true active_storage_attachments: true active_storage_blobs: true active_storage_variant_records: true @@ -1131,6 +1150,25 @@ indexes: nulls_not_distinct: comment: valid: true + action_text_rich_texts: + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: action_text_rich_texts + name: index_action_text_rich_texts_uniqueness + unique: true + columns: + - record_type + - record_id + - name + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true active_storage_attachments: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: active_storage_attachments @@ -2161,4 +2199,4 @@ indexes: comment: valid: true workflows: [] -version: 20250515125505 +version: 20250523084734 diff --git a/lib/markdown_renderer.rb b/lib/markdown_renderer.rb deleted file mode 100644 index e90e86fb4..000000000 --- a/lib/markdown_renderer.rb +++ /dev/null @@ -1,81 +0,0 @@ -require "rouge/plugins/redcarpet" - -class MarkdownRenderer < Redcarpet::Render::HTML - include Rouge::Plugins::Redcarpet - - def self.build - Redcarpet::Markdown.new MarkdownRenderer.new(filter_html: false), - autolink: true, highlight: true, no_intra_emphasis: true, - fenced_code_blocks: true, lax_spacing: true, strikethrough: true, tables: true - end - - def initialize(...) - super - @id_counts = Hash.new 0 - end - - def header(text, header_level) - unique_id(text).then do |id| - <<~HTML.chomp - - #{text} - - HTML - end - end - - def image(url, title, alt_text) - <<~HTML.chomp - - #{alt_text} - - HTML - end - - def code_block(code, language) - block_code(code, language) # call Rouge Redcarpet plugin - end - - def link(url, title, content) - return video_tag(url, title) if video_url?(url) - - attributes = { href: url } - attributes[:title] = title if title - attributes["data-turbo-frame"] = "_top" - - "#{content}" - end - - def autolink(url, link_type) - attributes = { href: url } - attributes["data-turbo-frame"] = "_top" - - "#{url}" - end - - private - attr_reader :id_counts - - def html_attributes(attributes) - attributes.map { |key, value| %Q(#{key}="#{value}") }.join(" ") - end - - def unique_id(text) - text.parameterize.then do |base_id| - id_counts[base_id] += 1 - id_counts[base_id] > 1 ? "#{base_id}-#{id_counts[base_id]}" : base_id - end - end - - def video_url?(url) - File.extname(url).downcase.match?(/\.(mp4|webm|ogg|mov|avi|mkv)/) - end - - def video_tag(url, title) - <<~HTML.chomp - - HTML - end -end diff --git a/lib/rails_ext/action_text_has_markdown.rb b/lib/rails_ext/action_text_has_markdown.rb deleted file mode 100644 index b792657d5..000000000 --- a/lib/rails_ext/action_text_has_markdown.rb +++ /dev/null @@ -1,47 +0,0 @@ -module ActionText - module HasMarkdown - extend ActiveSupport::Concern - - class_methods do - def has_markdown(name, strict_loading: strict_loading_by_default) - class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name} - markdown_#{name} || build_markdown_#{name} - end - - def #{name}_html - #{name}.to_html - end - - def #{name}_plain_text - #{name}.to_plain_text - end - - def #{name}? - markdown_#{name}.present? - end - - def #{name}=(content) - self.#{name}.content = content - end - CODE - - has_one :"markdown_#{name}", -> { where(name: name) }, - class_name: "ActionText::Markdown", as: :record, inverse_of: :record, autosave: true, dependent: :destroy, - strict_loading: strict_loading - - scope :"with_markdown_#{name}", -> { includes("markdown_#{name}") } - end - end - - def safe_markdown_attribute(name) - if self.class.reflect_on_association("markdown_#{name}")&.klass == ActionText::Markdown - public_send(name) - end - end - end -end - -ActiveSupport.on_load :active_record do - include ActionText::HasMarkdown -end diff --git a/lib/rails_ext/action_text_markdown.rb b/lib/rails_ext/action_text_markdown.rb deleted file mode 100644 index cd19fa001..000000000 --- a/lib/rails_ext/action_text_markdown.rb +++ /dev/null @@ -1,44 +0,0 @@ -module ActionText - class Markdown < Record - mattr_accessor :html_renderer - mattr_accessor :plain_text_renderer - - belongs_to :record, polymorphic: true, touch: true - - def to_html - to_unsafe_html.html_safe - end - - def to_unsafe_html - if html_renderer.respond_to? :call - html_renderer.call content - else - html_renderer.render content - end - end - - def to_plain_text - if plain_text_renderer.respond_to? :call - plain_text_renderer.call content - else - plain_text_renderer.render content - end - end - end -end - -module ActionText::Markdown::Uploads - extend ActiveSupport::Concern - - included do - has_many_attached :uploads, dependent: :destroy - end -end - -ActiveSupport.on_load :active_storage_attachment do - class ActionText::Markdown - include ActionText::Markdown::Uploads - end -end - -ActiveSupport.run_load_hooks :action_text_markdown, ActionText::Markdown diff --git a/lib/rails_ext/action_text_tag_helper.rb b/lib/rails_ext/action_text_tag_helper.rb deleted file mode 100644 index d662da7b2..000000000 --- a/lib/rails_ext/action_text_tag_helper.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActionText - module TagHelper - def markdown_area(record, name, value: nil, data: {}, **options) - field_name = "#{record.class.model_name.param_key}[#{name}]" - value = record.safe_markdown_attribute(name).content.to_s if value.nil? - data = data.reverse_merge! uploads_url: uploads_url(format: "json") - - tag.house_md value, name: field_name, data: data, **options - end - - def house_toolbar(**options, &block) - tag.house_md_toolbar(**options, &block) - end - - def house_toolbar_button(action, **options, &block) - tag.button title: action.to_s.humanize, data: { "house-md-action": action }, **options, &block - end - - def house_toolbar_file_upload_button(name: "upload", title: "Upload File", **options, &block) - tag.label title: title, **options do - safe_join [ - file_field_tag(name, data: { "house-md-toolbar-file-picker": true }, style: "display: none;"), - capture(&block) - ] - end - end - end -end - -module ActionView::Helpers - class FormBuilder - def markdown_area(method, **options) - @template.markdown_area(@object, method, **options) - end - end -end diff --git a/lib/rails_ext/active_storage_sluggable.rb b/lib/rails_ext/active_storage_sluggable.rb deleted file mode 100644 index d4d4be335..000000000 --- a/lib/rails_ext/active_storage_sluggable.rb +++ /dev/null @@ -1,32 +0,0 @@ -module ActiveStorage::Sluggable - extend ActiveSupport::Concern - - included do - before_create :set_slug - end - - def slug_url(host: ActiveStorage::Current.url_options[:host], port: ActiveStorage::Current.url_options[:port]) - Rails.application.routes.url_helpers.upload_url(slug, host: host, port: port) - end - - private - def set_slug - self.slug = "#{slug_basename}-#{SecureRandom.alphanumeric(6)}.#{slug_extension}" - end - - def slug_basename - File.basename(slug_filename, ".*").parameterize - end - - def slug_extension - File.extname(slug_filename).delete(".").parameterize - end - - def slug_filename - slug.presence || filename.to_s - end -end - -ActiveSupport.on_load :active_storage_attachment do - ActiveStorage::Attachment.include ActiveStorage::Sluggable -end diff --git a/script/migrate-to-multi-tenant b/script/migrate-to-multi-tenant index 8d035f37e..79db5c0e0 100755 --- a/script/migrate-to-multi-tenant +++ b/script/migrate-to-multi-tenant @@ -214,9 +214,6 @@ class EnactTenanting assert_count Tagging, Tagging.count assert_count Watch, Watch.count - # we're only copying the markdown records that are still accessible. - assert_count ActionText::Markdown, Comment.count - assert_count Event, Event.count assert_count Notification, Notification.count @@ -228,22 +225,7 @@ class EnactTenanting end def update_action_text_urls - update_counter["ActionText::Markdown"] = 0 - TENANTS.each do |tenant| - tenanted_domain = sprintf(TENANTED_DOMAIN, tenant: tenant) - - ApplicationRecord.with_tenant(tenant) do - ActionText::Markdown.all.each do |markdown| - content = markdown.content - next unless content =~ %r{#{CURRENT_DOMAIN}/u/} - - content.gsub!(%r{#{CURRENT_DOMAIN}/u/}, "#{tenanted_domain}/u/") - - markdown.update_column :content, content - update_counter["ActionText::Markdown"] += 1 - end - end - end + raise "No more markdown" end def hardlink_active_storage diff --git a/script/reset_cards_ids.rb b/script/reset_cards_ids.rb index 19305b5f2..65c556283 100644 --- a/script/reset_cards_ids.rb +++ b/script/reset_cards_ids.rb @@ -100,9 +100,9 @@ ApplicationRecord.with_each_tenant do |tenant| end # Update action_text_markdowns table (polymorphic relationship) - ActionText::Markdown.where(record_type: "Card").find_each do |markdown| - if id_mapping[markdown.record_id] - markdown.update_column(:record_id, id_mapping[markdown.record_id]) + ActionText::RichText.where(record_type: "Card").find_each do |rich_text| + if id_mapping[rich_text.record_id] + rich_text.update_column(:record_id, id_mapping[rich_text.record_id]) end end diff --git a/script/reset_collections_ids.rb b/script/reset_collections_ids.rb index 142e9e2a9..cf1d84028 100644 --- a/script/reset_collections_ids.rb +++ b/script/reset_collections_ids.rb @@ -73,9 +73,9 @@ ApplicationRecord.with_each_tenant do |tenant| end # Update action_text_markdowns table (polymorphic relationship) - ActionText::Markdown.where(record_type: "Collection").find_each do |markdown| - if id_mapping[markdown.record_id] - markdown.update_column(:record_id, id_mapping[markdown.record_id]) + ActionText::RichText.where(record_type: "Collection").find_each do |rich_text| + if id_mapping[rich_text.record_id] + rich_text.update_column(:record_id, id_mapping[rich_text.record_id]) end end diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index 5cb3a6582..75ba45147 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -46,7 +46,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest assert_equal "moon.jpg", card.image.filename.to_s assert_equal [ tags(:mobile) ], card.tags - assert_equal "Something more in-depth", card.description_plain_text.strip + assert_equal "Something more in-depth", card.description.to_plain_text.strip end test "users can only see cards in collections they have access to" do diff --git a/test/fixtures/action_text/rich_texts.yml b/test/fixtures/action_text/rich_texts.yml new file mode 100644 index 000000000..8b371ea60 --- /dev/null +++ b/test/fixtures/action_text/rich_texts.yml @@ -0,0 +1,4 @@ +# one: +# record: name_of_fixture (ClassOfFixture) +# name: content +# body:

In a million stars!