Merge branch 'main' into mobile/bridge-components
* main: (22 commits) Disable card column buttons when active Prevent comment content from overlapping with reaction button Delete pins belonging to cards in a board with revoked access Update development dependencies in SAAS lockfile Update runtime dependencies Update development dependencies Balance magic link instructions Add `.txt-balance` utility Move handleDesktop inside restoreColumnsDisablingTransitions Adjust border radius Clean up blank slates Add dialog manager to events page More sturated mini bubbles in light mode Brighten mini bubbles in dark mode Use separate cache namespaces for each beta instance Bump boost size up a tick on mobile Move Undo form inside closure message element Phrasing tweak for exceeding storage limit Keep strong tags words on same line Allow ActionText embed reuse and safe purge (#2346) ...
This commit is contained in:
+31
-32
@@ -123,8 +123,8 @@ GEM
|
||||
ast (2.4.3)
|
||||
autotuner (1.1.0)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1197.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-partitions (1.1203.0)
|
||||
aws-sdk-core (3.241.3)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -132,28 +132,28 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sdk-kms (1.120.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-s3 (1.211.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
bcrypt (3.1.21)
|
||||
bcrypt_pbkdf (1.1.2)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.0.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.19.0)
|
||||
bootsnap (1.20.1)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (7.1.1)
|
||||
brakeman (7.1.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bundler-audit (0.9.2)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
bundler-audit (0.9.3)
|
||||
bundler (>= 1.2.0)
|
||||
thor (~> 1.0)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
@@ -168,23 +168,23 @@ GEM
|
||||
logger (~> 1.5)
|
||||
chunky_png (1.4.0)
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (2.5.5)
|
||||
connection_pool (3.0.2)
|
||||
crack (1.0.1)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
date (3.5.1)
|
||||
debug (1.11.0)
|
||||
debug (1.11.1)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
dotenv (3.1.8)
|
||||
dotenv (3.2.0)
|
||||
drb (2.2.3)
|
||||
ed25519 (1.4.0)
|
||||
erb (6.0.1)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
faker (3.5.2)
|
||||
faker (3.5.3)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
ffi (1.17.2-aarch64-linux-gnu)
|
||||
ffi (1.17.2-aarch64-linux-musl)
|
||||
@@ -224,7 +224,7 @@ GEM
|
||||
json (2.18.0)
|
||||
jwt (3.1.2)
|
||||
base64
|
||||
kamal (2.9.0)
|
||||
kamal (2.10.1)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
@@ -271,11 +271,11 @@ GEM
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
mittens (0.3.1)
|
||||
mocha (2.8.2)
|
||||
mocha (3.0.1)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.8.0)
|
||||
net-http-persistent (4.0.6)
|
||||
connection_pool (~> 2.2, >= 2.2.4)
|
||||
net-http-persistent (4.0.8)
|
||||
connection_pool (>= 2.2.4, < 4)
|
||||
net-imap (0.6.2)
|
||||
date
|
||||
net-protocol
|
||||
@@ -307,7 +307,7 @@ GEM
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
openssl (3.3.2)
|
||||
openssl (4.0.0)
|
||||
ostruct (0.6.3)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.0)
|
||||
@@ -319,7 +319,7 @@ GEM
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.6.0)
|
||||
prism (1.8.0)
|
||||
propshaft (1.3.1)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
@@ -360,11 +360,11 @@ GEM
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.4.4)
|
||||
rouge (4.6.1)
|
||||
rqrcode (3.1.0)
|
||||
rouge (4.7.0)
|
||||
rqrcode (3.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.0.0)
|
||||
rqrcode_core (2.1.0)
|
||||
rubocop (1.81.7)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
@@ -400,7 +400,7 @@ GEM
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (3.2.2)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.38.0)
|
||||
selenium-webdriver (4.39.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
@@ -430,7 +430,7 @@ GEM
|
||||
sqlite3 (2.8.0-x86_64-darwin)
|
||||
sqlite3 (2.8.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.8.0-x86_64-linux-musl)
|
||||
sshkit (1.24.0)
|
||||
sshkit (1.25.0)
|
||||
base64
|
||||
logger
|
||||
net-scp (>= 1.1.2)
|
||||
@@ -440,7 +440,7 @@ GEM
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.2.0)
|
||||
thor (1.4.0)
|
||||
thor (1.5.0)
|
||||
thruster (0.1.17)
|
||||
thruster (0.1.17-aarch64-linux)
|
||||
thruster (0.1.17-arm64-darwin)
|
||||
@@ -458,16 +458,15 @@ GEM
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
uri (1.1.1)
|
||||
vcr (6.3.1)
|
||||
base64
|
||||
vcr (6.4.0)
|
||||
web-console (4.2.1)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
web-push (3.0.2)
|
||||
web-push (3.1.0)
|
||||
jwt (~> 3.0)
|
||||
openssl (~> 3.0)
|
||||
openssl (>= 3.0)
|
||||
webmock (3.26.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
|
||||
+31
-32
@@ -201,8 +201,8 @@ GEM
|
||||
ast (2.4.3)
|
||||
autotuner (1.1.0)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1197.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-partitions (1.1203.0)
|
||||
aws-sdk-core (3.241.3)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -210,28 +210,28 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sdk-kms (1.120.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-s3 (1.211.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
bcrypt (3.1.21)
|
||||
bcrypt_pbkdf (1.1.2)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.0.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.19.0)
|
||||
bootsnap (1.20.1)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (7.1.1)
|
||||
brakeman (7.1.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bundler-audit (0.9.2)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
bundler-audit (0.9.3)
|
||||
bundler (>= 1.2.0)
|
||||
thor (~> 1.0)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
@@ -246,16 +246,16 @@ GEM
|
||||
logger (~> 1.5)
|
||||
chunky_png (1.4.0)
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (2.5.5)
|
||||
connection_pool (3.0.2)
|
||||
crack (1.0.1)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
date (3.5.1)
|
||||
debug (1.11.0)
|
||||
debug (1.11.1)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
dotenv (3.1.8)
|
||||
dotenv (3.2.0)
|
||||
drb (2.2.3)
|
||||
dry-initializer (3.2.0)
|
||||
ed25519 (1.4.0)
|
||||
@@ -263,7 +263,7 @@ GEM
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
faker (3.5.2)
|
||||
faker (3.5.3)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
ffi (1.17.2-aarch64-linux-gnu)
|
||||
ffi (1.17.2-aarch64-linux-musl)
|
||||
@@ -302,7 +302,7 @@ GEM
|
||||
json (2.18.0)
|
||||
jwt (3.1.2)
|
||||
base64
|
||||
kamal (2.9.0)
|
||||
kamal (2.10.1)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
@@ -349,11 +349,11 @@ GEM
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
mittens (0.3.1)
|
||||
mocha (2.8.2)
|
||||
mocha (3.0.1)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.8.0)
|
||||
net-http-persistent (4.0.6)
|
||||
connection_pool (~> 2.2, >= 2.2.4)
|
||||
net-http-persistent (4.0.8)
|
||||
connection_pool (>= 2.2.4, < 4)
|
||||
net-imap (0.6.2)
|
||||
date
|
||||
net-protocol
|
||||
@@ -383,7 +383,7 @@ GEM
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
openssl (3.3.2)
|
||||
openssl (4.0.0)
|
||||
ostruct (0.6.3)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.0)
|
||||
@@ -395,7 +395,7 @@ GEM
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.6.0)
|
||||
prism (1.8.0)
|
||||
prometheus-client-mmap (1.4.0)
|
||||
base64
|
||||
bigdecimal
|
||||
@@ -470,11 +470,11 @@ GEM
|
||||
io-console (~> 0.5)
|
||||
rexml (3.4.4)
|
||||
rinku (2.0.6)
|
||||
rouge (4.6.1)
|
||||
rqrcode (3.1.0)
|
||||
rouge (4.7.0)
|
||||
rqrcode (3.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.0.0)
|
||||
rqrcode_core (2.1.0)
|
||||
rubocop (1.81.7)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
@@ -511,7 +511,7 @@ GEM
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (3.2.2)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.38.0)
|
||||
selenium-webdriver (4.39.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
@@ -549,7 +549,7 @@ GEM
|
||||
sqlite3 (2.8.0-arm64-darwin)
|
||||
sqlite3 (2.8.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.8.0-x86_64-linux-musl)
|
||||
sshkit (1.24.0)
|
||||
sshkit (1.25.0)
|
||||
base64
|
||||
logger
|
||||
net-scp (>= 1.1.2)
|
||||
@@ -560,7 +560,7 @@ GEM
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.2.0)
|
||||
stripe (18.0.1)
|
||||
thor (1.4.0)
|
||||
thor (1.5.0)
|
||||
thruster (0.1.17)
|
||||
thruster (0.1.17-aarch64-linux)
|
||||
thruster (0.1.17-arm64-darwin)
|
||||
@@ -577,16 +577,15 @@ GEM
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
uri (1.1.1)
|
||||
vcr (6.3.1)
|
||||
base64
|
||||
vcr (6.4.0)
|
||||
web-console (4.2.1)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
web-push (3.0.2)
|
||||
web-push (3.1.0)
|
||||
jwt (~> 3.0)
|
||||
openssl (~> 3.0)
|
||||
openssl (>= 3.0)
|
||||
webmock (3.26.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
|
||||
@@ -1,46 +1,21 @@
|
||||
/* Styles for the blank slate. To manage when they are shown/hidden, do so in context */
|
||||
|
||||
@layer components {
|
||||
.blank-slate--drag {
|
||||
box-shadow: none !important;
|
||||
color: color-mix(in srgb, var(--card-color) 70%, var(--color-canvas));
|
||||
|
||||
p {
|
||||
font-size: var(--text-small);
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cards--maybe & {
|
||||
background-color: var(--card-bg-color) !important;
|
||||
}
|
||||
|
||||
.cards--grid & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.blank-slate--empty {
|
||||
border: 2px dashed var(--color-ink-light);
|
||||
border-radius: 1ch;
|
||||
.blank-slate {
|
||||
border-radius: 0.5ch;
|
||||
border: 2px dashed var(--color-ink-lighter);
|
||||
color: var(--color-ink-dark);
|
||||
margin-block-start: 2dvh;
|
||||
padding: 1ch 2ch;
|
||||
rotate: -3deg;
|
||||
font-weight: 500;
|
||||
|
||||
.cards:has(.card) & {
|
||||
display: none;
|
||||
}
|
||||
margin-block-start: 2dvh;
|
||||
padding: 1.5ch 2ch;
|
||||
rotate: -3deg;
|
||||
}
|
||||
|
||||
.blank-slate--filters {
|
||||
.cards--grid:has(.card) & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cards__list:has(> :not(.blank-slate)) .card--hide-unless-empty {
|
||||
display: none;
|
||||
.blank-slate--drag {
|
||||
background-color: color-mix(in srgb, transparent, var(--card-color) 5%);
|
||||
border-color: color-mix(in srgb, transparent, var(--card-color) 10%);
|
||||
color: color-mix(in srgb, transparent, var(--card-color) 75%);
|
||||
margin-block-start: 0;
|
||||
rotate: 0deg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +255,27 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.card) {
|
||||
.blank-slate {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Use the default blank-slate on small viewports since drag-and-drop isn't available */
|
||||
[data-controller~="drag-and-drop"] & {
|
||||
@media (max-width: 639px) {
|
||||
.blank-slate--drag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.blank-slate--default {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cards__new-column {
|
||||
@@ -315,6 +336,10 @@
|
||||
inline-size: fit-content;
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
|
||||
.blank-slate--drag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Column Elements
|
||||
@@ -731,7 +756,8 @@
|
||||
.cards--on-deck {
|
||||
--card-color: var(--color-ink-light) !important;
|
||||
|
||||
.card {
|
||||
.card,
|
||||
.blank-slate {
|
||||
--card-color: var(--color-card-complete) !important;
|
||||
}
|
||||
|
||||
@@ -748,12 +774,13 @@
|
||||
.cards--maybe.is-collapsed:has(.bubble:not([hidden])) .cards__transition-container,
|
||||
.cards--doing.is-collapsed:has(.bubble:not([hidden])) .cards__transition-container {
|
||||
--bubble-color: var(--card-color, oklch(var(--lch-blue-medium)));
|
||||
--bubble-opacity: 75%;
|
||||
--bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%;
|
||||
|
||||
&:before {
|
||||
background: radial-gradient(
|
||||
color-mix(in srgb, var(--bubble-color) 8%, var(--color-canvas)) 50%,
|
||||
color-mix(in srgb, var(--bubble-color) 48%, var(--color-canvas)) 100%
|
||||
color-mix(in srgb, var(--bubble-color) calc(var(--bubble-opacity) / 5), var(--color-canvas)) 50%,
|
||||
color-mix(in srgb, var(--bubble-color) var(--bubble-opacity), var(--color-canvas)) 100%
|
||||
);
|
||||
block-size: 1em;
|
||||
border-radius: var(--bubble-shape);
|
||||
@@ -777,6 +804,22 @@
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
&.cards__expander-title:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="dark"] & {
|
||||
--bubble-opacity: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html:not([data-theme]) & {
|
||||
--bubble-opacity: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Card column indicators
|
||||
@@ -806,6 +849,7 @@
|
||||
--btn-background: var(--card-color);
|
||||
|
||||
color: var(--color-ink-inverted);
|
||||
opacity: 1 !important;
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
--comment-padding-block: var(--block-space-half);
|
||||
--comment-padding-inline: var(--inline-space-double);
|
||||
--comment-max: 70ch;
|
||||
--reaction-size: 2rem;
|
||||
--reaction-size: 2.25rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -109,6 +109,13 @@
|
||||
&:not:has(lexxy-editor) {
|
||||
padding-inline-end: var(--reaction-size);
|
||||
}
|
||||
|
||||
/* Add an empty space so the last line of text doesn't overlap with the reaction button */
|
||||
.action-text-content > p:last-child::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
inline-size: var(--reaction-size);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__content {
|
||||
@@ -130,6 +137,10 @@
|
||||
|
||||
.comment__edit {
|
||||
background-color: var(--color-ink-lightest);
|
||||
|
||||
&:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__permalink-title {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
&:has(.card--notification) {
|
||||
.notifications-list__empty-message {
|
||||
.notifications-list__blank-slate {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ summary {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.search__empty {
|
||||
.search__blank-slate {
|
||||
margin-block: 3em;
|
||||
margin-inline: auto;
|
||||
inline-size: fit-content;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
.txt-underline { text-decoration: underline; }
|
||||
.txt-tight-lines { line-height: 1.2; }
|
||||
.txt-nowrap { white-space: nowrap; }
|
||||
.txt-balance { text-wrap: balance; }
|
||||
.txt-break { word-break: break-word; }
|
||||
.txt-uppercase { text-transform: uppercase; }
|
||||
.txt-capitalize { text-transform: capitalize; }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Notifications::UnsubscribesController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
skip_before_action :verify_authenticity_token
|
||||
skip_forgery_protection
|
||||
|
||||
before_action :set_user
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ module ColumnsHelper
|
||||
card_triage_path(card, column_id: column),
|
||||
method: :post,
|
||||
class: [ "card__column-name btn", { "card__column-name--current": column == card.column && card.open? } ],
|
||||
disabled: column == card.column,
|
||||
style: "--column-color: #{column.color}",
|
||||
form_class: "flex gap-half",
|
||||
data: { turbo_frame: "_top", scroll_to_target: column == card.column && card.open? ? "target" : nil }
|
||||
|
||||
@@ -14,13 +14,12 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
async connect() {
|
||||
await this.#restoreColumnsDisablingTransitions()
|
||||
this.#setupIntersectionObserver()
|
||||
|
||||
this.mediaQuery = window.matchMedia(this.desktopBreakpointValue)
|
||||
this.handleDesktop = this.#handleDesktop.bind(this)
|
||||
this.mediaQuery.addEventListener("change", this.handleDesktop)
|
||||
this.handleDesktop(this.mediaQuery)
|
||||
|
||||
await this.#restoreColumnsDisablingTransitions()
|
||||
this.#setupIntersectionObserver()
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
@@ -57,6 +56,7 @@ export default class extends Controller {
|
||||
async #restoreColumnsDisablingTransitions() {
|
||||
this.#disableTransitions()
|
||||
this.#restoreColumns()
|
||||
this.#handleDesktop()
|
||||
|
||||
await nextFrame()
|
||||
this.#enableTransitions()
|
||||
|
||||
@@ -46,6 +46,7 @@ module Board::Accessible
|
||||
mentions_for_user(user).destroy_all
|
||||
notifications_for_user(user).destroy_all
|
||||
watches_for(user).destroy_all
|
||||
pins_for(user).destroy_all
|
||||
end
|
||||
|
||||
def watchers
|
||||
@@ -99,4 +100,8 @@ module Board::Accessible
|
||||
def watches_for(user)
|
||||
Watch.where(card: cards, user: user)
|
||||
end
|
||||
|
||||
def pins_for(user)
|
||||
Pin.where(card: cards, user: user)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
<div class="blank-slate blank-slate--drag card card--hide-unless-empty">
|
||||
<p>Drag cards here</p>
|
||||
</div>
|
||||
<div class="blank-slate blank-slate--empty card--hide-unless-empty">No cards here</div>
|
||||
<div class="blank-slate blank-slate--default">No cards here</div>
|
||||
<div class="blank-slate blank-slate--drag overflow-ellipsis">Drag cards here</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%= turbo_frame_tag @board, :entropy do %>
|
||||
<%= turbo_frame_tag board, :entropy do %>
|
||||
<div class="margin-block-end">
|
||||
<h2 class="divider txt-large">Auto close</h2>
|
||||
<p class="margin-none-block-start">Fizzy doesn’t let stale cards stick around forever. Cards automatically move to “Not now” if no one updates, comments, or moves a card for…</p>
|
||||
<%= render "entropy/auto_close", model: board, url: board_entropy_path(board), disabled: !Current.user.can_administer_board?(@board) %>
|
||||
<%= render "entropy/auto_close", model: board, url: board_entropy_path(board), disabled: !Current.user.can_administer_board?(board) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<%= with_automatic_pagination :filtered_cards_paginated_container, page do %>
|
||||
<%= render "cards/display/previews", cards: page.records, draggable: true %>
|
||||
<% end %>
|
||||
<div class="blank-slate blank-slate--empty blank-slate--filters">No cards match this filter</div>
|
||||
<div class="blank-slate">No cards match this filter</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<%= button_to "Not now", card_not_now_path(@card),
|
||||
class: [ "card__column-name btn", { "card__column-name--current": @card.postponed? } ],
|
||||
style: "--column-color: var(--color-card-complete)",
|
||||
disabled: @card.postponed?,
|
||||
role: "radio",
|
||||
aria: { checked: @card.postponed? },
|
||||
data: { scroll_to_target: @card.postponed? ? "target" : nil },
|
||||
@@ -12,7 +13,8 @@
|
||||
|
||||
<%= button_to "Maybe?", card_triage_path(@card), method: :delete,
|
||||
class: [ "card__column-name card__column-name--stream btn", { "card__column-name--current": @card.awaiting_triage? } ],
|
||||
style: "--column-color: var(--color-card-default)",
|
||||
style: "--column-color: var(--color-card-default)",
|
||||
disabled: @card.awaiting_triage?,
|
||||
role: "radio",
|
||||
aria: { checked: @card.awaiting_triage? },
|
||||
data: { scroll_to_target: @card.awaiting_triage? ? "target" : nil },
|
||||
@@ -25,6 +27,7 @@
|
||||
<%= button_to "Done", card_closure_path(@card),
|
||||
class: [ "card__column-name btn", { "card__column-name--current": @card.closed? } ],
|
||||
style: "--column-color: var(--color-card-complete)",
|
||||
disabled: @card.closed?,
|
||||
role: "radio",
|
||||
aria: { checked: @card.closed? },
|
||||
data: { scroll_to_target: @card.closed? ? "target" : nil },
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<%= with_automatic_pagination :cards_paginated_container, @page do %>
|
||||
<%= render "cards/display/previews", cards: @page.records, draggable: true %>
|
||||
<% end %>
|
||||
<div class="blank-slate blank-slate--empty blank-slate--filters">No cards match this filter</div>
|
||||
<div class="blank-slate">No cards match this filter</div>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<% content_for :header do %>
|
||||
<%= render "events/index/add_card_button", user_filtering: @user_filtering %>
|
||||
|
||||
<h1 class="header__title" data-bridge--title-target="header">
|
||||
<h1 class="header__title" data-controller="dialog-manager" data-bridge--title-target="header">
|
||||
<% if @user_filtering.boards.many? %>
|
||||
<span>Activity <%= @user_filtering.filter.boards.any? ? "in" : "across" %></span>
|
||||
<% else %>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<%= yield %>
|
||||
|
||||
<div class="nav__blank-slate blank-slate blank-slate--empty">
|
||||
<div class="nav__blank-slate blank-slate">
|
||||
Nothing matches that filter
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<%= button_to "Mark all as read", bulk_reading_path, class: "btn txt-small", form: { data: { turbo: false } }, data: { action: "badge#clear" } %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="notifications-list__empty-message blank-slate blank-slate--empty align-self-center">
|
||||
<div class="notifications-list__blank-slate blank-slate align-self-center">
|
||||
Nothing new for you
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<%= render "cards/display/public_previews", cards: @page.records %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="blank-slate blank-slate--empty">No cards here</div>
|
||||
<div class="blank-slate">No cards here</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<%= render "cards/display/public_previews", cards: @page.records %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="blank-slate blank-slate--empty">No cards here</div>
|
||||
<div class="blank-slate">No cards here</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<%= render "cards/display/public_previews", cards: @page.records %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="blank-slate blank-slate--empty">No cards here</div>
|
||||
<div class="blank-slate">No cards here</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<%= render "cards/display/public_previews", cards: @page.records %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="blank-slate blank-slate--empty">No cards here</div>
|
||||
<div class="blank-slate">No cards here</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<section class="search">
|
||||
<div class="search__results">
|
||||
<% unless page.used? %>
|
||||
<div class="search__empty blank-slate blank-slate--empty">
|
||||
<div class="search__blank-slate blank-slate">
|
||||
No matches
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] || flash[:shake] %>">
|
||||
<header>
|
||||
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
|
||||
<p class="margin-none-block-start txt-medium">
|
||||
<p class="margin-none-block-start txt-medium txt-balance">
|
||||
Then enter the verification code included in the email below:
|
||||
</p>
|
||||
</header>
|
||||
@@ -15,7 +15,7 @@
|
||||
data: { magic_link_target: "input", action: "keydown.enter->magic-link#submitOnEnter paste->magic-link#submitOnPaste" } %>
|
||||
<% end %>
|
||||
|
||||
<p class="txt-small">The code sent to <strong><%= email_address_pending_authentication %></strong> will work for <%= distance_of_time_in_words(MagicLink::EXPIRATION_TIME) %>.</p>
|
||||
<p class="txt-small txt-balance">The code sent to <strong><%= email_address_pending_authentication %></strong> will work for <%= distance_of_time_in_words(MagicLink::EXPIRATION_TIME) %>.</p>
|
||||
</div>
|
||||
|
||||
<% if Rails.env.development? && flash[:magic_link_code].present? %>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
default_options: &default_options
|
||||
store_options:
|
||||
max_age: <%= 60.days.to_i %>
|
||||
namespace: <%= Rails.env %>
|
||||
namespace: <%= "#{Rails.env}#{"-#{ENV["CACHE_NAMESPACE"]}" if ENV["CACHE_NAMESPACE"]}" %>
|
||||
|
||||
default_connection: &default_connection
|
||||
database: cache
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
# attached to an untracked type (avatar/export) could theoretically be reused in a tracked
|
||||
# context. This is acceptable - user-accessible blob IDs from untracked contexts are
|
||||
# basically nonexistent in practice.
|
||||
#
|
||||
# Exception: ActionText embeds are allowed to reuse blobs to support copy/paste.
|
||||
|
||||
ActiveSupport.on_load(:active_storage_attachment) do
|
||||
validate :blob_account_matches_record, on: :create
|
||||
@@ -29,30 +31,31 @@ ActiveSupport.on_load(:active_storage_attachment) do
|
||||
# bypass tenancy checks via try(:account) returning nil - this is intentional as
|
||||
# these classes don't participate in storage tracking.
|
||||
def blob_account_matches_record
|
||||
return unless record&.try(:account).present?
|
||||
return if whitelisted_for_cross_account?
|
||||
|
||||
unless blob&.account_id == record.account.id
|
||||
errors.add(:blob_id, "blob account must match record account")
|
||||
if record&.try(:account).present? && !whitelisted_for_cross_account?
|
||||
unless blob&.account_id == record.account.id
|
||||
errors.add(:blob_id, "blob account must match record account")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Ledger integrity: blob can only have one tracked attachment
|
||||
def no_tracked_blob_reuse
|
||||
tracked_record = record&.try(:storage_tracked_record)
|
||||
return unless tracked_record.present?
|
||||
return if whitelisted_for_cross_account?
|
||||
if tracked_record.present? &&
|
||||
!whitelisted_for_cross_account? &&
|
||||
!(record_type == "ActionText::RichText" && name == "embeds")
|
||||
|
||||
# Check for existing attachment of this blob in tracked contexts
|
||||
# Uses Storage::TRACKED_RECORD_TYPES constant to stay generic
|
||||
existing = ActiveStorage::Attachment
|
||||
.where(blob_id: blob_id)
|
||||
.where(record_type: Storage::TRACKED_RECORD_TYPES)
|
||||
.where.not(id: id)
|
||||
.exists?
|
||||
# Check for existing attachment of this blob in tracked contexts
|
||||
# Uses Storage::TRACKED_RECORD_TYPES constant to stay generic
|
||||
existing = ActiveStorage::Attachment
|
||||
.where(blob_id: blob_id)
|
||||
.where(record_type: Storage::TRACKED_RECORD_TYPES)
|
||||
.where.not(id: id)
|
||||
.exists?
|
||||
|
||||
if existing
|
||||
errors.add(:blob_id, "cannot reuse blob in tracked storage context")
|
||||
if existing
|
||||
errors.add(:blob_id, "cannot reuse blob in tracked storage context")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Fizzy-specific override: ActiveStorage's default purge path uses `delete`,
|
||||
# which skips attachment callbacks. We need `destroy` so storage ledger detaches
|
||||
# are recorded and reused blobs (ActionText embeds) aren't purged until the
|
||||
# last attachment is gone. Keep this local to Fizzy; it's not a Rails default.
|
||||
module ActiveStorage
|
||||
module PurgeOnLastAttachment
|
||||
def purge
|
||||
@purge_mode = :purge
|
||||
destroy
|
||||
purge_blob_if_last(:purge) if destroyed?
|
||||
ensure
|
||||
@purge_mode = nil
|
||||
end
|
||||
|
||||
def purge_later
|
||||
@purge_mode = :purge_later
|
||||
destroy
|
||||
purge_blob_if_last(:purge_later) if destroyed?
|
||||
ensure
|
||||
@purge_mode = nil
|
||||
end
|
||||
|
||||
private
|
||||
def purge_dependent_blob_later
|
||||
if dependent == :purge_later && !@purge_mode
|
||||
purge_blob_if_last(:purge_later)
|
||||
end
|
||||
end
|
||||
|
||||
def purge_blob_if_last(mode)
|
||||
if blob && !blob.attachments.exists?
|
||||
mode == :purge ? blob.purge : blob.purge_later
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ActiveSupport.on_load(:active_storage_attachment) do
|
||||
prepend ActiveStorage::PurgeOnLastAttachment
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
class Stripe::WebhooksController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
skip_before_action :require_account
|
||||
skip_before_action :verify_authenticity_token
|
||||
skip_forgery_protection
|
||||
|
||||
def create
|
||||
if event = verify_webhook_signature
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<% elsif Current.account.exceeding_card_limit? %>
|
||||
You’ve used up your <u><%= number_with_delimiter(Plan.free.card_limit) %></u> free cards
|
||||
<% elsif Current.account.exceeding_storage_limit? %>
|
||||
You’ve used up all <u><%= storage_to_human_size(Plan.free.storage_limit) %></u> free storage
|
||||
You’ve used up all <u><%= storage_to_human_size(Plan.free.storage_limit) %></u> of free storage
|
||||
<% else %>
|
||||
You’ve used <u><%= Current.account.billed_cards_count %></u> free cards out of <%= number_with_delimiter(Plan.free.card_limit) %>
|
||||
<% end %>
|
||||
</h3>
|
||||
|
||||
<p class="margin-block-start-half">To keep using Fizzy <%= "past #{ number_with_delimiter(Plan.free.card_limit) } cards," unless Current.account.exceeding_storage_limit? %> it’s only <strong><%= format_currency(Plan.paid.price) %>/month</strong> for <strong>unlimited cards</strong>, <strong>unlimited users</strong>, and <strong><%= storage_to_human_size(Plan.paid.storage_limit) %></strong> of storage.</p>
|
||||
<p class="margin-block-start-half">To keep using Fizzy <%= "past #{ number_with_delimiter(Plan.free.card_limit) } cards," unless Current.account.exceeding_storage_limit? %> it’s only <strong><%= format_currency(Plan.paid.price) %>/month</strong> for <strong class="txt-nowrap">unlimited cards</strong>, <strong class="txt-nowrap">unlimited users</strong>, and <strong><%= storage_to_human_size(Plan.paid.storage_limit) %></strong> of storage.</p>
|
||||
|
||||
<%= button_to "Upgrade to #{Plan.paid.name} for #{ format_currency(Plan.paid.price) }/month", account_subscription_path, class: "btn settings-subscription__button txt-medium", form: { data: { turbo: false } } %>
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ ssh:
|
||||
env:
|
||||
clear:
|
||||
APP_FQDN: beta<%= @beta_number %>.fizzy-beta.com
|
||||
CACHE_NAMESPACE: <%= @beta_number %>
|
||||
RAILS_ENV: beta
|
||||
RAILS_LOG_LEVEL: fatal # suppress unstructured log lines
|
||||
MYSQL_DATABASE_HOST: fizzy-mysql-primary
|
||||
|
||||
@@ -10,17 +10,19 @@
|
||||
#
|
||||
# Safe to re-run: skips attachments that already have entries (by blob_id + recordable).
|
||||
#
|
||||
# IMPORTANT: Before running in production, verify zero historic blob reuse in tracked contexts:
|
||||
# OPTIONAL: If you want to enforce no-reuse for direct attachments, verify there are
|
||||
# no existing violations (ActionText embeds may legitimately reuse blobs):
|
||||
#
|
||||
# ActiveStorage::Attachment
|
||||
# .joins(:blob)
|
||||
# .where(record_type: Storage::TRACKED_RECORD_TYPES)
|
||||
# .where.not(record_type: "ActionText::RichText")
|
||||
# .where.not(active_storage_blobs: { account_id: Storage::TEMPLATE_ACCOUNT_ID })
|
||||
# .select(:blob_id)
|
||||
# .group(:blob_id)
|
||||
# .having("COUNT(*) > 1")
|
||||
# .count
|
||||
# # Should return empty hash if no reuse exists in tracked contexts
|
||||
# # Should return empty hash if no direct-attachment reuse exists
|
||||
#
|
||||
# If reuse exists (excluding template blobs), fix the data first.
|
||||
class BackfillStorageLedger
|
||||
|
||||
@@ -39,7 +39,7 @@ class SearchesControllerTest < ActionDispatch::IntegrationTest
|
||||
# Searching with non-existent card id
|
||||
get search_path(q: "999999", script_name: "/#{@account.external_account_id}")
|
||||
assert_select "form[data-controller='auto-submit']", count: 0
|
||||
assert_select ".search__empty", text: "No matches"
|
||||
assert_select ".search__blank-slate", text: "No matches"
|
||||
end
|
||||
|
||||
test "search highlights matched terms with proper HTML marks" do
|
||||
|
||||
@@ -80,4 +80,27 @@ class AccessTest < ActiveSupport::TestCase
|
||||
|
||||
assert_not card.watched_by?(kevin)
|
||||
end
|
||||
|
||||
test "pins are destroyed when access is lost" do
|
||||
kevin = users(:kevin)
|
||||
board = boards(:writebook)
|
||||
card = cards(:logo) # Kevin has pinned this card
|
||||
|
||||
other_board = boards(:miltons_wish_list)
|
||||
other_card = cards(:radio)
|
||||
other_board.accesses.grant_to(kevin)
|
||||
other_card.pin_by(kevin)
|
||||
|
||||
assert card.pinned_by?(kevin)
|
||||
assert other_card.pinned_by?(kevin)
|
||||
|
||||
kevin_access = accesses(:writebook_kevin)
|
||||
|
||||
perform_enqueued_jobs only: Board::CleanInaccessibleDataJob do
|
||||
kevin_access.destroy
|
||||
end
|
||||
|
||||
assert_not card.pinned_by?(kevin)
|
||||
assert other_card.pinned_by?(kevin), "Pin on other board should not be destroyed"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,6 +33,83 @@ class Storage::NoReuseTest < ActiveSupport::TestCase
|
||||
assert_equal 1, ActiveStorage::Attachment.where(blob_id: blob.id).count
|
||||
end
|
||||
|
||||
test "allows reuse for ActionText embeds" do
|
||||
file = file_fixture("moon.jpg")
|
||||
blob = ActiveStorage::Blob.create_and_upload! \
|
||||
io: file.open,
|
||||
filename: "embed.jpg",
|
||||
content_type: "image/jpeg"
|
||||
|
||||
embed_html = ActionText::Attachment.from_attachable(blob).to_html
|
||||
|
||||
card1 = @board.cards.create!(title: "Card 1", creator: users(:david))
|
||||
card1.update!(description: "<p>#{embed_html}</p>")
|
||||
card1.reload
|
||||
|
||||
card2 = @board.cards.create!(title: "Card 2", creator: users(:david))
|
||||
card2.update!(description: "<p>#{embed_html}</p>")
|
||||
card2.reload
|
||||
|
||||
assert_equal 2, ActiveStorage::Attachment.where(
|
||||
record_type: "ActionText::RichText",
|
||||
name: "embeds",
|
||||
blob_id: blob.id
|
||||
).count
|
||||
end
|
||||
|
||||
test "purge_later does not purge blob when still attached elsewhere" do
|
||||
file = file_fixture("moon.jpg")
|
||||
blob = ActiveStorage::Blob.create_and_upload! \
|
||||
io: file.open,
|
||||
filename: "embed.jpg",
|
||||
content_type: "image/jpeg"
|
||||
|
||||
embed_html = ActionText::Attachment.from_attachable(blob).to_html
|
||||
|
||||
card1 = @board.cards.create!(title: "Card 1", creator: users(:david))
|
||||
card1.update!(description: "<p>#{embed_html}</p>")
|
||||
|
||||
card2 = @board.cards.create!(title: "Card 2", creator: users(:david))
|
||||
card2.update!(description: "<p>#{embed_html}</p>")
|
||||
|
||||
attachment = ActiveStorage::Attachment.find_by(
|
||||
record: card1.rich_text_description,
|
||||
name: "embeds",
|
||||
blob_id: blob.id
|
||||
)
|
||||
|
||||
assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do
|
||||
attachment.purge_later
|
||||
end
|
||||
|
||||
assert ActiveStorage::Blob.exists?(blob.id)
|
||||
assert_equal 1, ActiveStorage::Attachment.where(blob_id: blob.id).count
|
||||
end
|
||||
|
||||
test "purge_later enqueues purge when last attachment is removed" do
|
||||
file = file_fixture("moon.jpg")
|
||||
blob = ActiveStorage::Blob.create_and_upload! \
|
||||
io: file.open,
|
||||
filename: "embed.jpg",
|
||||
content_type: "image/jpeg"
|
||||
|
||||
embed_html = ActionText::Attachment.from_attachable(blob).to_html
|
||||
|
||||
card = @board.cards.create!(title: "Card", creator: users(:david))
|
||||
card.update!(description: "<p>#{embed_html}</p>")
|
||||
card.reload
|
||||
|
||||
attachment = ActiveStorage::Attachment.find_by(
|
||||
record: card.rich_text_description,
|
||||
name: "embeds",
|
||||
blob_id: blob.id
|
||||
)
|
||||
|
||||
assert_enqueued_with job: ActiveStorage::PurgeJob, args: [ blob ] do
|
||||
attachment.purge_later
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects cross-account blob attachment" do
|
||||
other_account = Account.create!(name: "Other")
|
||||
other_board = other_account.boards.create!(name: "Other Board", creator: users(:david))
|
||||
|
||||
Reference in New Issue
Block a user