Merge branch 'main' into mobile-columns-pt-iii
* main: (318 commits) Document the new sign in method Replace handle_ naming Use same constant for fake magic links Replace FakeMagicLink with a temporary object Tidy up session_token Clean up interfaces Split tests by controller or responsibility Simplify auth logic Fix due to unit test when creating with invalid emails Restore sessions_controller test on creating invalid email address Move magic link api tests to their own files Rename test to clarify what they're about Cleanup session creation Update to always return a pending auth token for JSON responses. Update API test for cross code Change test expectation on single tenant mode account creation Add unit tests for the new endpoints Pass a server token when creating a magic link via API Simplify code a bit Simplify session create logic for both html and json ...
This commit is contained in:
@@ -5,7 +5,7 @@ registries:
|
||||
type: git
|
||||
url: https://github.com
|
||||
username: x-access-token
|
||||
password: ${{secrets.GH_TOKEN}}
|
||||
password: ${{secrets.FIZZY_GH_TOKEN}}
|
||||
|
||||
updates:
|
||||
- package-ecosystem: bundler
|
||||
|
||||
@@ -18,4 +18,4 @@ jobs:
|
||||
with:
|
||||
saas: true
|
||||
secrets:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
FIZZY_GH_TOKEN: ${{ secrets.FIZZY_GH_TOKEN }}
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
type: boolean
|
||||
required: true
|
||||
secrets:
|
||||
GH_TOKEN:
|
||||
FIZZY_GH_TOKEN:
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
MYSQL_USER: root
|
||||
FIZZY_DB_HOST: 127.0.0.1
|
||||
FIZZY_DB_PORT: 3306
|
||||
BUNDLE_GITHUB__COM: ${{ inputs.saas && format('x-access-token:{0}', secrets.GH_TOKEN) || '' }}
|
||||
BUNDLE_GITHUB__COM: ${{ inputs.saas && format('x-access-token:{0}', secrets.FIZZY_GH_TOKEN) || '' }}
|
||||
|
||||
steps:
|
||||
- name: Install system packages
|
||||
|
||||
@@ -9,3 +9,11 @@ paths = [
|
||||
'''docs/''',
|
||||
'''test/''',
|
||||
]
|
||||
|
||||
[[rules]]
|
||||
id = "basecamp-integration-url"
|
||||
description = "Basecamp Integration URL"
|
||||
regex = '''https://[^\s]*?([0-9a-fA-F]{16,})'''
|
||||
[rules.allowlist]
|
||||
regexTarget = "match"
|
||||
regexes = ['''github\.com''']
|
||||
|
||||
@@ -11,3 +11,5 @@ AllCops:
|
||||
Exclude:
|
||||
- 'db/migrate/**/*'
|
||||
- 'db/schema*.rb'
|
||||
- 'saas/db/migrate/**/*'
|
||||
- 'saas/db/saas_schema.rb'
|
||||
|
||||
+17
-11
@@ -1,8 +1,8 @@
|
||||
GIT
|
||||
remote: https://github.com/basecamp/lexxy
|
||||
revision: d292280b6d2c5d8a924327adf8bb9f0b25953539
|
||||
revision: 7c197c0afc7095c89df9cb6e24484df9e7212ac8
|
||||
specs:
|
||||
lexxy (0.1.23.beta)
|
||||
lexxy (0.1.24.beta)
|
||||
rails (>= 8.0.2)
|
||||
|
||||
GIT
|
||||
@@ -122,8 +122,8 @@ GEM
|
||||
ast (2.4.3)
|
||||
autotuner (1.1.0)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1187.0)
|
||||
aws-sdk-core (3.239.1)
|
||||
aws-partitions (1.1197.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -134,7 +134,7 @@ GEM
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.205.0)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -144,7 +144,7 @@ GEM
|
||||
bcrypt (3.1.20)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (3.3.1)
|
||||
bigdecimal (4.0.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.19.0)
|
||||
msgpack (~> 1.2)
|
||||
@@ -190,6 +190,7 @@ GEM
|
||||
ffi (1.17.2-arm-linux-gnu)
|
||||
ffi (1.17.2-arm-linux-musl)
|
||||
ffi (1.17.2-arm64-darwin)
|
||||
ffi (1.17.2-x86_64-darwin)
|
||||
ffi (1.17.2-x86_64-linux-gnu)
|
||||
ffi (1.17.2-x86_64-linux-musl)
|
||||
fugit (1.12.1)
|
||||
@@ -267,7 +268,7 @@ GEM
|
||||
railties (>= 7.1)
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
mittens (0.3.0)
|
||||
mittens (0.3.1)
|
||||
mocha (2.8.2)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.8.0)
|
||||
@@ -298,6 +299,8 @@ GEM
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-x86_64-linux-musl)
|
||||
@@ -422,6 +425,7 @@ GEM
|
||||
sqlite3 (2.8.0-arm-linux-gnu)
|
||||
sqlite3 (2.8.0-arm-linux-musl)
|
||||
sqlite3 (2.8.0-arm64-darwin)
|
||||
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)
|
||||
@@ -435,10 +439,11 @@ GEM
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.9)
|
||||
thor (1.4.0)
|
||||
thruster (0.1.16)
|
||||
thruster (0.1.16-aarch64-linux)
|
||||
thruster (0.1.16-arm64-darwin)
|
||||
thruster (0.1.16-x86_64-linux)
|
||||
thruster (0.1.17)
|
||||
thruster (0.1.17-aarch64-linux)
|
||||
thruster (0.1.17-arm64-darwin)
|
||||
thruster (0.1.17-x86_64-darwin)
|
||||
thruster (0.1.17-x86_64-linux)
|
||||
timeout (0.4.4)
|
||||
trilogy (2.9.0)
|
||||
tsort (0.2.0)
|
||||
@@ -481,6 +486,7 @@ PLATFORMS
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
arm64-darwin
|
||||
x86_64-darwin-25
|
||||
x86_64-linux
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
+2
-2
@@ -3,10 +3,10 @@ eval_gemfile "Gemfile"
|
||||
|
||||
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
|
||||
|
||||
# SaaS-only functionality
|
||||
gem "activeresource", require: "active_resource"
|
||||
gem "stripe", "~> 18.0"
|
||||
gem "queenbee", bc: "queenbee-plugin"
|
||||
gem "fizzy-saas", bc: "fizzy-saas"
|
||||
gem "fizzy-saas", path: "saas"
|
||||
gem "console1984", bc: "console1984"
|
||||
gem "audits1984", bc: "audits1984"
|
||||
|
||||
|
||||
+35
-34
@@ -19,38 +19,16 @@ GIT
|
||||
rails (>= 7.0)
|
||||
rainbow
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/fizzy-saas
|
||||
revision: 9c356dfeedbf5309f1a36ee3371f49604c9be188
|
||||
specs:
|
||||
fizzy-saas (0.1.0)
|
||||
audits1984
|
||||
console1984
|
||||
prometheus-client-mmap (~> 1.4.0)
|
||||
queenbee
|
||||
rails (>= 8.1.0.beta1)
|
||||
rails_structured_logging
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
yabeda
|
||||
yabeda-actioncable
|
||||
yabeda-activejob
|
||||
yabeda-gc
|
||||
yabeda-http_requests
|
||||
yabeda-prometheus-mmap
|
||||
yabeda-puma-plugin
|
||||
yabeda-rails (>= 0.10)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/lexxy
|
||||
revision: d292280b6d2c5d8a924327adf8bb9f0b25953539
|
||||
revision: 7c197c0afc7095c89df9cb6e24484df9e7212ac8
|
||||
specs:
|
||||
lexxy (0.1.23.beta)
|
||||
lexxy (0.1.24.beta)
|
||||
rails (>= 8.0.2)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/queenbee-plugin
|
||||
revision: 15faf03a876c5e66b67753d2e1ddb24f1eb5abb2
|
||||
revision: 14312a940471e20617b38cdec7c092a01567d18b
|
||||
specs:
|
||||
queenbee (3.2.0)
|
||||
activeresource
|
||||
@@ -181,6 +159,27 @@ GIT
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
|
||||
PATH
|
||||
remote: saas
|
||||
specs:
|
||||
fizzy-saas (0.1.0)
|
||||
audits1984
|
||||
console1984
|
||||
prometheus-client-mmap (~> 1.4.0)
|
||||
queenbee
|
||||
rails (>= 8.1.0.beta1)
|
||||
rails_structured_logging
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
yabeda
|
||||
yabeda-actioncable
|
||||
yabeda-activejob
|
||||
yabeda-gc
|
||||
yabeda-http_requests
|
||||
yabeda-prometheus-mmap
|
||||
yabeda-puma-plugin
|
||||
yabeda-rails (>= 0.10)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
@@ -201,8 +200,8 @@ GEM
|
||||
ast (2.4.3)
|
||||
autotuner (1.1.0)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1187.0)
|
||||
aws-sdk-core (3.239.1)
|
||||
aws-partitions (1.1197.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -213,7 +212,7 @@ GEM
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.205.0)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -223,7 +222,7 @@ GEM
|
||||
bcrypt (3.1.20)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (3.3.1)
|
||||
bigdecimal (4.0.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.19.0)
|
||||
msgpack (~> 1.2)
|
||||
@@ -347,7 +346,7 @@ GEM
|
||||
railties (>= 7.1)
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
mittens (0.3.0)
|
||||
mittens (0.3.1)
|
||||
mocha (2.8.2)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.8.0)
|
||||
@@ -558,11 +557,12 @@ GEM
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.9)
|
||||
stripe (18.0.1)
|
||||
thor (1.4.0)
|
||||
thruster (0.1.16)
|
||||
thruster (0.1.16-aarch64-linux)
|
||||
thruster (0.1.16-arm64-darwin)
|
||||
thruster (0.1.16-x86_64-linux)
|
||||
thruster (0.1.17)
|
||||
thruster (0.1.17-aarch64-linux)
|
||||
thruster (0.1.17-arm64-darwin)
|
||||
thruster (0.1.17-x86_64-linux)
|
||||
timeout (0.4.4)
|
||||
trilogy (2.9.0)
|
||||
tsort (0.2.0)
|
||||
@@ -684,6 +684,7 @@ DEPENDENCIES
|
||||
solid_queue (~> 1.2)
|
||||
sqlite3 (>= 2.0)
|
||||
stimulus-rails
|
||||
stripe (~> 18.0)
|
||||
thruster
|
||||
trilogy (~> 2.9)
|
||||
turbo-rails
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" /></svg>
|
||||
|
After Width: | Height: | Size: 308 B |
@@ -1 +0,0 @@
|
||||
<svg enable-background="new 0 0 24 24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m20.3 16.9c0 .1-.1.2-.2.2h-16.1c-.1 0-.2-.1-.2-.2v-9.8c-.1-.1 0-.2.2-.2h5.8c0-.1 0-.2 0-.3 0-.6.1-1.1.2-1.7h-6.3c-1.1 0-2 .9-2 2v11.7c0 1.1.9 2 2 2h7.1c.1 0 .2.1.2.2v1c0 .1-.1.2-.2.2h-4.9c-.5 0-1 .4-1 1 0 .5.4 1 1 1h12.2c.5 0 1-.4 1-1 0-.5-.4-1-1-1h-4.9c-.1 0-.2-.1-.2-.2v-1c0-.1.1-.2.2-.2h7.1c1.1 0 2-.9 2-2v-5.8c-.6.5-1.2.8-2 1.1z"/><path d="m17.5.1c-3 0-5.6 2.1-6.3 4.9-.1.4-.2 1-.2 1.6v.3c.2 3.4 3 6.2 6.5 6.2 1 0 2-.2 2.8-.6.7-.4 1.4-.8 2-1.4 1-1.3 1.7-2.8 1.7-4.5 0-3.6-2.9-6.5-6.5-6.5zm2.4 7.3-2 2.5c-.2.2-.5.3-.7.1 0 0-.1 0-.1-.1l-2-2.5c-.2-.2-.1-.5.1-.7.1-.1.2-.1.3-.1h.8c.1 0 .2-.1.2-.2v-2.8c0-.6.4-1 1-1s1 .4 1 1v2.8c0 .1.1.2.2.2h.8c.3 0 .5.2.5.5 0 .1 0 .2-.1.3z"/></svg>
|
||||
|
Before Width: | Height: | Size: 785 B |
@@ -1,4 +1,4 @@
|
||||
@layer reset, base, components, modules, utilities;
|
||||
@layer reset, base, components, modules, utilities, native, platform;
|
||||
|
||||
:root {
|
||||
/* Spacing */
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@layer platform {
|
||||
[data-platform~=android] {
|
||||
.hide-on-android {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,15 @@
|
||||
33% { background-color: var(--color-border-darker); scale: 1; }
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0% { transform: rotate(0deg); }
|
||||
20% { transform: rotate(3deg); }
|
||||
40% { transform: rotate(-3deg); }
|
||||
60% { transform: rotate(3deg); }
|
||||
80% { transform: rotate(-3deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes wobble {
|
||||
0% { transform: rotate(calc(var(--bubble-rotate) + 30deg)); }
|
||||
15% { border-radius: 66% 34% 72% 28% / 39% 63% 37% 61%; }
|
||||
|
||||
@@ -481,7 +481,23 @@
|
||||
font-size: clamp(0.6rem, 0.85cqi, 100px);
|
||||
}
|
||||
|
||||
.card__comments {
|
||||
--column-gap: 0.8ch;
|
||||
|
||||
display: flex;
|
||||
margin-block-end: calc(var(--block-space) * -1.7);
|
||||
margin-inline-end: calc(var(--card-padding-inline) * -0.5);
|
||||
|
||||
.icon {
|
||||
--icon-size: 1.6em;
|
||||
|
||||
color: var(--card-color);
|
||||
}
|
||||
}
|
||||
|
||||
.card__steps {
|
||||
--column-gap: 0.8ch;
|
||||
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
border-radius: 99rem;
|
||||
}
|
||||
|
||||
.btn:not(.popup__btn, .btn--plain, .btn--reversed) {
|
||||
.btn:not(.popup__btn, .btn--plain, .btn--reversed, .settings-subscription__button) {
|
||||
--btn-background: var(--card-color);
|
||||
--btn-color: var(--color-ink-inverted);
|
||||
}
|
||||
@@ -299,6 +299,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card-perma__notch-new-card-buttons {
|
||||
display: flex;
|
||||
gap: var(--inline-space-half);
|
||||
|
||||
@media (max-width: 479px) {
|
||||
flex-direction: column;
|
||||
|
||||
.btn {
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-perma__closure-message {
|
||||
color: var(--card-color);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
}
|
||||
|
||||
.comment {
|
||||
display: flex;
|
||||
margin-inline: auto;
|
||||
max-inline-size: var(--comment-max);
|
||||
position: relative;
|
||||
@@ -34,6 +35,56 @@
|
||||
.house-md-content {
|
||||
padding: var(--comment-padding-block) 0 0;
|
||||
}
|
||||
|
||||
.comment--system & {
|
||||
--comment-padding-block: var(--block-space-half);
|
||||
|
||||
text-align: center;
|
||||
|
||||
&::before {
|
||||
/* Make up space for lack of avatar */
|
||||
content: "";
|
||||
display: flex;
|
||||
inline-size: calc(var(--comment-padding-inline) * 0.9);
|
||||
}
|
||||
|
||||
.comment__avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment__author {
|
||||
a { margin: 0 auto; }
|
||||
h3 { margin-inline: auto; }
|
||||
strong { display: none; }
|
||||
}
|
||||
|
||||
.comment__body {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comment__content {
|
||||
--stripe-color: var(--color-ink-lightest);
|
||||
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg in srgb,
|
||||
var(--color-canvas) 0 1px,
|
||||
var(--stripe-color) 1px 10px);
|
||||
padding-inline: var(--comment-padding-inline);
|
||||
|
||||
.comments--system-expanded .comment--system & {
|
||||
--stripe-color: color-mix(in srgb, var(--card-color) 10%, var(--color-canvas));
|
||||
}
|
||||
|
||||
.comment__history {
|
||||
background-color: var(--stripe-color);
|
||||
}
|
||||
}
|
||||
|
||||
.reactions {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment__author {
|
||||
@@ -48,7 +99,6 @@
|
||||
}
|
||||
|
||||
.comment__body {
|
||||
padding-inline-end: var(--reaction-size);
|
||||
text-align: start;
|
||||
|
||||
.action-text-content {
|
||||
@@ -60,6 +110,10 @@
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:not:has(lexxy-editor) {
|
||||
padding-inline-end: var(--reaction-size);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__content {
|
||||
@@ -70,6 +124,7 @@
|
||||
|
||||
background-color: var(--comment-bg-color);
|
||||
border-radius: 0.2em;
|
||||
max-inline-size: calc(100% - calc(var(--comment-padding-inline) * 0.75));
|
||||
padding:
|
||||
var(--comment-padding-block)
|
||||
calc(var(--comment-padding-inline) / 2)
|
||||
@@ -82,53 +137,36 @@
|
||||
background-color: var(--color-ink-lightest);
|
||||
}
|
||||
|
||||
.comment__history {
|
||||
background-color: var(--color-ink-lightest);
|
||||
display: none;
|
||||
inset: calc(var(--comment-padding-inline) / 2.5) calc(var(--comment-padding-inline) / 1.5) auto auto; margin-inline-end: calc(var(--comment-padding-inline) / -2.5);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.comment--system {
|
||||
--comment-padding-block: var(--block-space-half);
|
||||
display: none;
|
||||
transition: var(--dialog-duration) allow-discrete;
|
||||
transition-property: display;
|
||||
|
||||
max-inline-size: var(--comment-max);
|
||||
text-align: center;
|
||||
|
||||
&::before {
|
||||
/* Make up space for lack of avatar */
|
||||
content: "";
|
||||
display: flex;
|
||||
inline-size: calc(var(--comment-padding-inline) * 0.75);
|
||||
.comments--system-expanded & {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__avatar {
|
||||
/* Show the last system comment */
|
||||
:nth-last-child(1 of .comment--system) {
|
||||
display: contents;
|
||||
|
||||
.comment__history {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide the "Show history" button if there's only one system comment */
|
||||
:nth-child(1 of .comment--system) {
|
||||
.comment__history {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment__author {
|
||||
a {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__body {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comment__content {
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg in srgb,
|
||||
var(--color-canvas) 0 1px,
|
||||
var(--color-ink-lightest) 1px 10px
|
||||
);
|
||||
padding-inline: var(--comment-padding-inline);
|
||||
}
|
||||
|
||||
.reactions {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,8 @@
|
||||
.icon--fizzy { --svg: url("fizzy.svg"); }
|
||||
.icon--globe { --svg: url("globe.svg "); }
|
||||
.icon--golden-ticket { --svg: url("golden-ticket.svg "); }
|
||||
.icon--history { --svg: url("history.svg "); }
|
||||
.icon--home { --svg: url("home.svg "); }
|
||||
.icon--install { --svg: url("install.svg "); }
|
||||
.icon--install { --svg: url("install.svg "); }
|
||||
.icon--install-edge { --svg: url("install-edge.svg "); }
|
||||
.icon--lifebuoy { --svg: url("lifebuoy.svg "); }
|
||||
.icon--lock { --svg: url("lock.svg "); }
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@layer platform {
|
||||
[data-platform~=ios] {
|
||||
.hide-on-ios {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
}
|
||||
|
||||
.lexxy-editor__content {
|
||||
margin: var(--block-space-half) 0;
|
||||
margin-block-start: var(--block-space-half);
|
||||
min-block-size: calc(7lh + var(--block-space));
|
||||
outline: 0;
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
::selection {
|
||||
background: oklch(var(--lch-blue-light) / 0.5);
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lexxy-editor--drag-over {
|
||||
@@ -227,12 +231,15 @@
|
||||
[data-button-group] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap);
|
||||
|
||||
+ & {
|
||||
margin-block-start: var(--gap);
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,17 +304,12 @@
|
||||
font-size: var(--text-small);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
max-height: 200px;
|
||||
min-inline-size: var(--lexxy-prompt-min-width);
|
||||
overflow: auto;
|
||||
padding: var(--lexxy-prompt-padding);
|
||||
visibility: hidden;
|
||||
z-index: var(--z-popup);
|
||||
|
||||
max-height: 200px;
|
||||
overflow: scroll;
|
||||
|
||||
&:is(.lexxy-prompt-menu--visible) {
|
||||
padding: var(--lexxy-prompt-padding);
|
||||
}
|
||||
}
|
||||
|
||||
.lexxy-prompt-menu--visible {
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
.lightbox__caption {
|
||||
color: var(--color-ink-inverted);
|
||||
color: var(--color-white);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
@layer native {
|
||||
[data-platform~=native] {
|
||||
/* The mobile apps may inject their own custom insets based on native elements on screen, like a floating navigation */
|
||||
--custom-safe-inset-top: var(--injected-safe-inset-top, env(safe-area-inset-top, 0px));
|
||||
--custom-safe-inset-right: var(--injected-safe-inset-right, env(safe-area-inset-right, 0px));
|
||||
--custom-safe-inset-bottom: var(--injected-safe-inset-bottom, env(safe-area-inset-bottom, 0px));
|
||||
--custom-safe-inset-left: var(--injected-safe-inset-left, env(safe-area-inset-left, 0px));
|
||||
|
||||
.hide-on-native {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.popup__footer {
|
||||
border-block-start: 1px solid var(--color-ink-lightest);
|
||||
color: var(--card-color);
|
||||
margin-block-start: var(--popup-item-padding-inline);
|
||||
padding: var(--popup-item-padding-inline) var(--popup-item-padding-inline) 0;
|
||||
text-align: center;
|
||||
text-transform: initial;
|
||||
}
|
||||
|
||||
.popup__title {
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
inset-inline-end: calc(var(--comment-padding-inline) / 3);
|
||||
}
|
||||
|
||||
.reactions__list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@layer components {
|
||||
.rich-text-content {
|
||||
:where(.rich-text-content) {
|
||||
--block-margin: 0.5lh;
|
||||
|
||||
:where(h1, h2, h3, h4, h5, h6) {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
display: block;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02ch;
|
||||
@@ -12,14 +12,14 @@
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
:where(h1) { font-size: 2em; }
|
||||
:where(h2) { font-size: 1.5em; }
|
||||
:where(h3) { font-size: 1.17em; }
|
||||
:where(h4) { font-size: 1em; }
|
||||
:where(h5) { font-size: 0.83em; }
|
||||
:where(h6) { font-size: 0.67em; }
|
||||
h1 { font-size: 2em; }
|
||||
h2 { font-size: 1.5em; }
|
||||
h3 { font-size: 1.17em; }
|
||||
h4 { font-size: 1em; }
|
||||
h5 { font-size: 0.83em; }
|
||||
h6 { font-size: 0.67em; }
|
||||
|
||||
:where(p, ul, ol, dl, blockquote, figure, .attachment) {
|
||||
p, ul, ol, dl, blockquote, figure, .attachment {
|
||||
margin-block: 0 var(--block-margin);
|
||||
|
||||
&:not(lexxy-editor &) {
|
||||
@@ -28,49 +28,65 @@
|
||||
}
|
||||
}
|
||||
|
||||
:where(ol, ul) {
|
||||
list-style: disc;
|
||||
p:has(+ p) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
padding-inline-start: 3ch;
|
||||
}
|
||||
|
||||
:where(li:has(li)) {
|
||||
ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
li:has(li) {
|
||||
list-style: none;
|
||||
|
||||
:where(ol, ul) {
|
||||
ol, ul {
|
||||
margin: 0;
|
||||
padding-inline-start: 2ch;
|
||||
}
|
||||
}
|
||||
|
||||
:where(b, strong, .lexxy-content__bold) {
|
||||
b, strong, .lexxy-content__bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:where(i, em, .lexxy-content__italic) {
|
||||
i, em, .lexxy-content__italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
:where(s, .lexxy-content__strikethrough) {
|
||||
s, .lexxy-content__strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
:where(mark, .lexxy-content__highlight) {
|
||||
mark, .lexxy-content__highlight {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:where(p, blockquote) {
|
||||
p, blockquote {
|
||||
letter-spacing: -0.005ch;
|
||||
}
|
||||
|
||||
:where(blockquote) {
|
||||
/* Avoid extra space due to empty paragraphs */
|
||||
p:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-inline-start: 0.25em solid var(--color-ink-lighter);
|
||||
font-style: italic;
|
||||
margin: var(--block-margin) 0;
|
||||
padding-inline-start: 2ch;
|
||||
}
|
||||
|
||||
:where(img, video, embed, object) {
|
||||
img, video, embed, object {
|
||||
inline-size: auto;
|
||||
margin-inline: auto;
|
||||
max-block-size: 32rem;
|
||||
@@ -81,14 +97,10 @@
|
||||
a:has(img),
|
||||
a:has(video) {
|
||||
inline-size: fit-content;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* Avoid extra space due to empty paragraphs */
|
||||
p:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:where(hr) {
|
||||
hr {
|
||||
color: currentColor;
|
||||
block-size: 0;
|
||||
border: none;
|
||||
@@ -103,7 +115,7 @@
|
||||
}
|
||||
|
||||
/* Code */
|
||||
:where(code, pre) {
|
||||
code, pre {
|
||||
background-color: var(--color-canvas);
|
||||
border: 1px solid var(--color-ink-lighter);
|
||||
border-radius: 0.3em;
|
||||
@@ -209,7 +221,7 @@
|
||||
position: relative;
|
||||
max-inline-size: 100%;
|
||||
|
||||
:where(progress) {
|
||||
progress {
|
||||
inline-size: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -93,17 +93,14 @@
|
||||
}
|
||||
|
||||
.steps__icon {
|
||||
--icon-size: 0.875em;
|
||||
|
||||
background-color: var(--card-color);
|
||||
block-size: 1.3em;
|
||||
border-radius: 50%;
|
||||
color: var(--color-ink-inverted);
|
||||
display: grid;
|
||||
inline-size: 1.3em;
|
||||
place-content: center;
|
||||
|
||||
.icon {
|
||||
background-color: var(--color-ink-inverted);
|
||||
block-size: 0.8em;
|
||||
inline-size: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,6 +424,7 @@
|
||||
.card__meta-text:not(.card__meta-text--updated),
|
||||
.card__stages,
|
||||
.card__steps,
|
||||
.card__comments,
|
||||
.card__closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -178,6 +178,23 @@
|
||||
.fill-highlight { background-color: var(--color-highlight); }
|
||||
.fill-transparent { background-color: transparent; }
|
||||
|
||||
.fill-highlighter {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-highlight);
|
||||
border-radius: 0.2em;
|
||||
content: "";
|
||||
inset-block: 0;
|
||||
inset-inline: -0.1em;
|
||||
position: absolute;
|
||||
transform: skewX(-10deg) rotate(1deg);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.translucent { opacity: var(--opacity, 0.66); }
|
||||
|
||||
/* Borders */
|
||||
@@ -246,6 +263,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.show-on-native {
|
||||
body:not([data-platform~=native]) & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none; /* Edge */
|
||||
scrollbar-width: none; /* FF */
|
||||
|
||||
@@ -35,7 +35,7 @@ class Boards::ColumnsController < ApplicationController
|
||||
@column.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.html { redirect_back_or_to @board }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -83,7 +83,7 @@ class BoardsController < ApplicationController
|
||||
def show_columns
|
||||
cards = @board.cards.awaiting_triage.latest.with_golden_first.preloaded
|
||||
set_page_and_extract_portion_from cards
|
||||
fresh_when etag: [ @board, @page.records, @user_filtering ]
|
||||
fresh_when etag: [ @board, @page.records, @user_filtering, Current.account ]
|
||||
end
|
||||
|
||||
def board_params
|
||||
|
||||
@@ -8,11 +8,16 @@ class Cards::AssignmentsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
@card.toggle_assignment @board.users.active.find(params[:assignee_id])
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
if @card.toggle_assignment @board.users.active.find(params[:assignee_id])
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,6 +61,6 @@ class CardsController < ApplicationController
|
||||
end
|
||||
|
||||
def card_params
|
||||
params.expect(card: [ :status, :title, :description, :image, :created_at, :last_active_at, tag_ids: [] ])
|
||||
params.expect(card: [ :status, :title, :description, :image, :created_at, :last_active_at ])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,13 +4,12 @@ module Authentication
|
||||
included do
|
||||
before_action :require_account # Checking and setting account must happen first
|
||||
before_action :require_authentication
|
||||
after_action :ensure_development_magic_link_not_leaked
|
||||
helper_method :authenticated?
|
||||
helper_method :email_address_pending_authentication
|
||||
|
||||
etag { Current.identity.id if authenticated? }
|
||||
|
||||
include LoginHelper
|
||||
include Authentication::ViaMagicLink, LoginHelper
|
||||
end
|
||||
|
||||
class_methods do
|
||||
@@ -38,7 +37,7 @@ module Authentication
|
||||
|
||||
def require_account
|
||||
unless Current.account.present?
|
||||
redirect_to main_app.session_menu_url(script_name: nil)
|
||||
redirect_to main_app.session_menu_path(script_name: nil)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,35 +101,7 @@ module Authentication
|
||||
cookies.delete(:session_token)
|
||||
end
|
||||
|
||||
def ensure_development_magic_link_not_leaked
|
||||
unless Rails.env.development?
|
||||
raise "Leaking magic link via flash in #{Rails.env}?" if flash[:magic_link_code].present?
|
||||
end
|
||||
end
|
||||
|
||||
def email_address_pending_authentication_matches?(email_address)
|
||||
if ActiveSupport::SecurityUtils.secure_compare(email_address, email_address_pending_authentication || "")
|
||||
session.delete(:email_address_pending_authentication)
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def email_address_pending_authentication
|
||||
session[:email_address_pending_authentication]
|
||||
end
|
||||
|
||||
def redirect_to_session_magic_link(magic_link, return_to: nil)
|
||||
serve_development_magic_link(magic_link)
|
||||
session[:email_address_pending_authentication] = magic_link.identity.email_address if magic_link
|
||||
session[:return_to_after_authenticating] = return_to if return_to
|
||||
redirect_to main_app.session_magic_link_url(script_name: nil)
|
||||
end
|
||||
|
||||
def serve_development_magic_link(magic_link)
|
||||
if Rails.env.development?
|
||||
flash[:magic_link_code] = magic_link&.code
|
||||
end
|
||||
def session_token
|
||||
cookies[:session_token]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
module Authentication::ViaMagicLink
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_action :ensure_development_magic_link_not_leaked
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_development_magic_link_not_leaked
|
||||
unless Rails.env.development?
|
||||
raise "Leaking magic link via flash in #{Rails.env}?" if flash[:magic_link_code].present?
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_to_fake_session_magic_link(email_address, **options)
|
||||
fake_magic_link = MagicLink.new(
|
||||
identity: Identity.new(email_address: email_address),
|
||||
code: SecureRandom.base32(6),
|
||||
expires_at: MagicLink::EXPIRATION_TIME.from_now
|
||||
)
|
||||
|
||||
redirect_to_session_magic_link fake_magic_link, **options
|
||||
end
|
||||
|
||||
def redirect_to_session_magic_link(magic_link, return_to: nil)
|
||||
serve_development_magic_link(magic_link)
|
||||
set_pending_authentication_token(magic_link)
|
||||
session[:return_to_after_authenticating] = return_to if return_to
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to main_app.session_magic_link_url(script_name: nil) }
|
||||
format.json { render json: { pending_authentication_token: pending_authentication_token }, status: :created }
|
||||
end
|
||||
end
|
||||
|
||||
def serve_development_magic_link(magic_link)
|
||||
if Rails.env.development? && magic_link.present?
|
||||
flash[:magic_link_code] = magic_link.code
|
||||
response.set_header("X-Magic-Link-Code", magic_link.code)
|
||||
end
|
||||
end
|
||||
|
||||
def set_pending_authentication_token(magic_link)
|
||||
cookies[:pending_authentication_token] = {
|
||||
value: pending_authentication_token_verifier.generate(magic_link.identity.email_address, expires_at: magic_link.expires_at),
|
||||
httponly: true,
|
||||
same_site: :lax,
|
||||
expires: magic_link.expires_at
|
||||
}
|
||||
end
|
||||
|
||||
def email_address_pending_authentication
|
||||
pending_authentication_token_verifier.verified(pending_authentication_token)
|
||||
end
|
||||
|
||||
def pending_authentication_token_verifier
|
||||
Rails.application.message_verifier(:pending_authentication)
|
||||
end
|
||||
|
||||
def pending_authentication_token
|
||||
cookies[:pending_authentication_token]
|
||||
end
|
||||
|
||||
def clear_pending_authentication_token
|
||||
cookies.delete(:pending_authentication_token)
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,6 @@ module Authorization
|
||||
|
||||
included do
|
||||
before_action :ensure_can_access_account, if: -> { Current.account.present? && authenticated? }
|
||||
before_action :ensure_only_staff_can_access_non_production_remote_environments, if: :authenticated?
|
||||
end
|
||||
|
||||
class_methods do
|
||||
@@ -27,11 +26,7 @@ module Authorization
|
||||
end
|
||||
|
||||
def ensure_can_access_account
|
||||
redirect_to session_menu_url(script_name: nil) if Current.user.blank? || !Current.user.active?
|
||||
end
|
||||
|
||||
def ensure_only_staff_can_access_non_production_remote_environments
|
||||
head :forbidden unless Rails.env.local? || Rails.env.production? || Current.identity.staff?
|
||||
redirect_to session_menu_path(script_name: nil) if Current.user.blank? || !Current.user.active?
|
||||
end
|
||||
|
||||
def redirect_existing_user
|
||||
|
||||
@@ -16,7 +16,7 @@ module FilterScoped
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.reverse_merge(**Filter.default_values).permit(*Filter::PERMITTED_PARAMS)
|
||||
params.with_defaults(**Filter.default_values).permit(*Filter::PERMITTED_PARAMS)
|
||||
end
|
||||
|
||||
def set_user_filtering
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class JoinCodesController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { head :too_many_requests }
|
||||
|
||||
before_action :set_join_code
|
||||
before_action :ensure_join_code_is_valid
|
||||
|
||||
@@ -4,7 +4,8 @@ class My::MenusController < ApplicationController
|
||||
@boards = Current.user.boards.ordered_by_recently_accessed
|
||||
@tags = Current.account.tags.all.alphabetically
|
||||
@users = Current.account.users.active.alphabetically
|
||||
@accounts = Current.identity.accounts
|
||||
|
||||
fresh_when etag: [ @filters, @boards, @tags, @users ]
|
||||
fresh_when etag: [ @filters, @boards, @tags, @users, @accounts ]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@ class Public::BaseController < ApplicationController
|
||||
end
|
||||
|
||||
def set_card
|
||||
@card = @board.cards.find_by!(number: params[:id]) if params[:board_id] && params[:id]
|
||||
@card = @board.cards.published.find_by!(number: params[:id]) if params[:board_id] && params[:id]
|
||||
end
|
||||
|
||||
def set_public_cache_expiration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Sessions::MagicLinksController < ApplicationController
|
||||
disallow_account_scope
|
||||
require_unauthenticated_access
|
||||
rate_limit to: 10, within: 15.minutes, only: :create, with: -> { redirect_to session_magic_link_path, alert: "Wait 15 minutes, then try again" }
|
||||
rate_limit to: 10, within: 15.minutes, only: :create, with: :rate_limit_exceeded
|
||||
before_action :ensure_that_email_address_pending_authentication_exists
|
||||
|
||||
layout "public"
|
||||
@@ -11,25 +11,20 @@ class Sessions::MagicLinksController < ApplicationController
|
||||
|
||||
def create
|
||||
if magic_link = MagicLink.consume(code)
|
||||
authenticate_with magic_link
|
||||
authenticate magic_link
|
||||
else
|
||||
redirect_to session_magic_link_path, flash: { shake: true }
|
||||
invalid_code
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_that_email_address_pending_authentication_exists
|
||||
unless email_address_pending_authentication.present?
|
||||
redirect_to new_session_path, alert: "Enter your email address to sign in."
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_with(magic_link)
|
||||
if email_address_pending_authentication_matches?(magic_link.identity.email_address)
|
||||
start_new_session_for magic_link.identity
|
||||
redirect_to after_sign_in_url(magic_link)
|
||||
else
|
||||
redirect_to new_session_path, alert: "Authentication failed. Please try again."
|
||||
alert_message = "Enter your email address to sign in."
|
||||
respond_to do |format|
|
||||
format.html { redirect_to new_session_path, alert: alert_message }
|
||||
format.json { render json: { message: alert_message }, status: :unauthorized }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,6 +32,41 @@ class Sessions::MagicLinksController < ApplicationController
|
||||
params.expect(:code)
|
||||
end
|
||||
|
||||
def authenticate(magic_link)
|
||||
if ActiveSupport::SecurityUtils.secure_compare(email_address_pending_authentication || "", magic_link.identity.email_address)
|
||||
sign_in magic_link
|
||||
else
|
||||
email_address_mismatch
|
||||
end
|
||||
end
|
||||
|
||||
def sign_in(magic_link)
|
||||
clear_pending_authentication_token
|
||||
start_new_session_for magic_link.identity
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to after_sign_in_url(magic_link) }
|
||||
format.json { render json: { session_token: session_token } }
|
||||
end
|
||||
end
|
||||
|
||||
def email_address_mismatch
|
||||
clear_pending_authentication_token
|
||||
alert_message = "Something went wrong. Please try again."
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to new_session_path, alert: alert_message }
|
||||
format.json { render json: { message: alert_message }, status: :unauthorized }
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_code
|
||||
respond_to do |format|
|
||||
format.html { redirect_to session_magic_link_path, flash: { shake: true } }
|
||||
format.json { render json: { message: "Try another code." }, status: :unauthorized }
|
||||
end
|
||||
end
|
||||
|
||||
def after_sign_in_url(magic_link)
|
||||
if magic_link.for_sign_up?
|
||||
new_signup_completion_path
|
||||
@@ -44,4 +74,12 @@ class Sessions::MagicLinksController < ApplicationController
|
||||
after_authentication_url
|
||||
end
|
||||
end
|
||||
|
||||
def rate_limit_exceeded
|
||||
rate_limit_exceeded_message = "Try again in 15 minutes."
|
||||
respond_to do |format|
|
||||
format.html { redirect_to session_magic_link_path, alert: rate_limit_exceeded_message }
|
||||
format.json { render json: { message: rate_limit_exceeded_message }, status: :too_many_requests }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class SessionsController < ApplicationController
|
||||
disallow_account_scope
|
||||
require_unauthenticated_access except: :destroy
|
||||
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
|
||||
rate_limit to: 10, within: 3.minutes, only: :create, with: :rate_limit_exceeded
|
||||
|
||||
layout "public"
|
||||
|
||||
@@ -9,16 +9,12 @@ class SessionsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
if identity = Identity.find_by_email_address(email_address)
|
||||
redirect_to_session_magic_link identity.send_magic_link
|
||||
if identity = Identity.find_by(email_address: email_address)
|
||||
sign_in identity
|
||||
elsif Account.accepting_signups?
|
||||
sign_up
|
||||
else
|
||||
signup = Signup.new(email_address: email_address)
|
||||
if signup.valid?(:identity_creation)
|
||||
magic_link = signup.create_identity if Account.accepting_signups?
|
||||
redirect_to_session_magic_link magic_link
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
redirect_to_fake_session_magic_link email_address
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,7 +24,43 @@ class SessionsController < ApplicationController
|
||||
end
|
||||
|
||||
private
|
||||
def magic_link_from_sign_in_or_sign_up
|
||||
if identity = Identity.find_by_email_address(email_address)
|
||||
identity.send_magic_link
|
||||
else
|
||||
signup = Signup.new(email_address: email_address)
|
||||
signup.create_identity if signup.valid?(:identity_creation) && Account.accepting_signups?
|
||||
end
|
||||
end
|
||||
|
||||
def email_address
|
||||
params.expect(:email_address)
|
||||
end
|
||||
|
||||
def rate_limit_exceeded
|
||||
rate_limit_exceeded_message = "Try again later."
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to new_session_path, alert: rate_limit_exceeded_message }
|
||||
format.json { render json: { message: rate_limit_exceeded_message }, status: :too_many_requests }
|
||||
end
|
||||
end
|
||||
|
||||
def sign_in(identity)
|
||||
redirect_to_session_magic_link identity.send_magic_link
|
||||
end
|
||||
|
||||
def sign_up
|
||||
signup = Signup.new(email_address: email_address)
|
||||
|
||||
if signup.valid?(:identity_creation)
|
||||
magic_link = signup.create_identity
|
||||
redirect_to_session_magic_link magic_link
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html { redirect_to new_session_path, alert: "Something went wrong" }
|
||||
format.json { render json: { message: "Something went wrong" }, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,12 +10,6 @@ module ApplicationHelper
|
||||
tag.span class: class_names("icon icon--#{name}", options.delete(:class)), "aria-hidden": true, **options
|
||||
end
|
||||
|
||||
def inline_svg(name)
|
||||
file_path = "#{Rails.root}/app/assets/images/#{name}.svg"
|
||||
return File.read(file_path).html_safe if File.exist?(file_path)
|
||||
"(not found)"
|
||||
end
|
||||
|
||||
def back_link_to(label, url, action, **options)
|
||||
link_to url, class: "btn btn--back", data: { controller: "hotkey", action: action }, **options do
|
||||
icon_tag("arrow-left") + tag.strong("Back to #{label}", class: "overflow-ellipsis") + tag.kbd("ESC", class: "txt-x-small hide-on-touch").html_safe
|
||||
|
||||
@@ -43,7 +43,7 @@ module ColumnsHelper
|
||||
end
|
||||
|
||||
def column_frame_tag(id, src: nil, data: {}, **options, &block)
|
||||
data = data.reverse_merge \
|
||||
data = data.with_defaults \
|
||||
drag_and_drop_refresh: true,
|
||||
controller: "frame",
|
||||
action: "turbo:before-frame-render->frame#morphRender turbo:before-morph-element->frame#morphReload"
|
||||
|
||||
@@ -3,6 +3,8 @@ module MessagesHelper
|
||||
turbo_frame_tag dom_id(card, :messages),
|
||||
class: "comments gap center",
|
||||
style: "--card-color: #{card.color}",
|
||||
role: "group", aria: { label: "Messages" }, &
|
||||
role: "group",
|
||||
aria: { label: "Messages" },
|
||||
data: { controller: "toggle-class", toggle_class_toggle_class: "comments--system-expanded" }, &
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static values = { limit: Number, count: Number }
|
||||
static targets = ["unassigned", "limitMessage"]
|
||||
|
||||
connect() {
|
||||
this.updateState()
|
||||
}
|
||||
|
||||
countValueChanged() {
|
||||
this.updateState()
|
||||
}
|
||||
|
||||
updateState() {
|
||||
const atLimit = this.countValue >= this.limitValue
|
||||
|
||||
this.unassignedTargets.forEach(el => {
|
||||
el.hidden = atLimit
|
||||
})
|
||||
|
||||
if (this.hasLimitMessageTarget) {
|
||||
this.limitMessageTarget.hidden = !atLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,22 @@ import { Controller } from "@hotwired/stimulus"
|
||||
export default class extends Controller {
|
||||
static targets = [ "caption", "image", "dialog", "zoomedImage" ]
|
||||
|
||||
open(event) {
|
||||
imageTargetConnected(element) {
|
||||
element.addEventListener("click", this.#handleImageClick)
|
||||
}
|
||||
|
||||
imageTargetDisconnected(element) {
|
||||
element.removeEventListener("click", this.#handleImageClick)
|
||||
}
|
||||
|
||||
#handleImageClick = (event) => {
|
||||
event.preventDefault()
|
||||
this.#open(event.currentTarget)
|
||||
}
|
||||
|
||||
#open(link) {
|
||||
this.dialogTarget.showModal()
|
||||
this.#set(event.target.closest("a"))
|
||||
this.#set(link)
|
||||
}
|
||||
|
||||
// Wait for the transition to finish before resetting the image
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export const HttpStatus = {
|
||||
CONFLICT: 409,
|
||||
UNPROCESSABLE: 422
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class Account < ApplicationRecord
|
||||
def create_with_owner(account:, owner:)
|
||||
create!(**account).tap do |account|
|
||||
account.users.create!(role: :system, name: "System")
|
||||
account.users.create!(**owner.reverse_merge(role: "owner", verified_at: Time.current))
|
||||
account.users.create!(**owner.with_defaults(role: :owner, verified_at: Time.current))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -69,6 +69,7 @@ class Account::Seeder
|
||||
<li>Make another called "Working on"</li>
|
||||
</ol>
|
||||
<p>Go back to the Board view, click the little “+” to the right of the DONE column, name the column, pick a color, then do it again.</p>
|
||||
<p><br></p>
|
||||
<p>After that, drag this card to “DONE” or select “DONE” in the sidebar.</p>
|
||||
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/make-columns.gif" alt="Demo of adding columns" caption="Make two more columns" content-type="image/*" filename="make-columns.gif" presentation="gallery"></action-text-attachment>
|
||||
HTML
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
module Admin
|
||||
end
|
||||
@@ -35,10 +35,26 @@ class ApplicationPlatform < PlatformAgent
|
||||
!mobile?
|
||||
end
|
||||
|
||||
def native?
|
||||
match? /Hotwire Native/
|
||||
end
|
||||
|
||||
def windows?
|
||||
operating_system == "Windows"
|
||||
end
|
||||
|
||||
def type
|
||||
if native? && android?
|
||||
"native android"
|
||||
elsif native? && ios?
|
||||
"native ios"
|
||||
elsif mobile?
|
||||
"mobile web"
|
||||
else
|
||||
"desktop web"
|
||||
end
|
||||
end
|
||||
|
||||
def operating_system
|
||||
case user_agent.platform
|
||||
when /Android/ then "Android"
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
class Assignment < ApplicationRecord
|
||||
LIMIT = 100
|
||||
|
||||
belongs_to :account, default: -> { card.account }
|
||||
belongs_to :card, touch: true
|
||||
|
||||
belongs_to :assignee, class_name: "User"
|
||||
belongs_to :assigner, class_name: "User"
|
||||
|
||||
validate :within_limit, on: :create
|
||||
|
||||
private
|
||||
def within_limit
|
||||
if card.assignments.count >= LIMIT
|
||||
errors.add(:base, "Card already has the maximum of #{LIMIT} assignees")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,10 +24,12 @@ module Card::Assignable
|
||||
|
||||
private
|
||||
def assign(user)
|
||||
assignments.create! assignee: user, assigner: Current.user
|
||||
watch_by user
|
||||
assignment = assignments.create assignee: user, assigner: Current.user
|
||||
|
||||
track_event :assigned, assignee_ids: [ user.id ]
|
||||
if assignment.persisted?
|
||||
watch_by user
|
||||
track_event :assigned, assignee_ids: [ user.id ]
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
# Already assigned
|
||||
end
|
||||
|
||||
@@ -7,9 +7,9 @@ module Card::Closeable
|
||||
scope :closed, -> { joins(:closure) }
|
||||
scope :open, -> { where.missing(:closure) }
|
||||
|
||||
scope :recently_closed_first, -> { closed.order("closures.created_at": :desc) }
|
||||
scope :closed_at_window, ->(window) { closed.where("closures.created_at": window) }
|
||||
scope :closed_by, ->(users) { closed.where("closures.user_id": Array(users)) }
|
||||
scope :recently_closed_first, -> { closed.order(closures: { created_at: :desc }) }
|
||||
scope :closed_at_window, ->(window) { closed.where(closures: { created_at: window }) }
|
||||
scope :closed_by, ->(users) { closed.where(closures: { user_id: Array(users) }) }
|
||||
end
|
||||
|
||||
def closed?
|
||||
|
||||
@@ -7,7 +7,7 @@ module Card::Stallable
|
||||
has_one :activity_spike, class_name: "Card::ActivitySpike", dependent: :destroy
|
||||
|
||||
scope :with_activity_spikes, -> { joins(:activity_spike) }
|
||||
scope :stalled, -> { open.active.with_activity_spikes.where("card_activity_spikes.updated_at": ..STALLED_AFTER_LAST_SPIKE_PERIOD.ago, updated_at: ..STALLED_AFTER_LAST_SPIKE_PERIOD.ago) }
|
||||
scope :stalled, -> { open.active.with_activity_spikes.where(card_activity_spikes: { updated_at: ..STALLED_AFTER_LAST_SPIKE_PERIOD.ago }, updated_at: ..STALLED_AFTER_LAST_SPIKE_PERIOD.ago) }
|
||||
|
||||
before_update :remember_to_detect_activity_spikes
|
||||
after_update_commit :detect_activity_spikes_later, if: :should_detect_activity_spikes?
|
||||
|
||||
@@ -6,8 +6,6 @@ module Card::Statuses
|
||||
|
||||
before_save :mark_if_just_published
|
||||
after_create -> { track_event :published }, if: :published?
|
||||
|
||||
scope :published_or_drafted_by, ->(user) { where(status: :published).or(where(status: :drafted, creator: user)) }
|
||||
end
|
||||
|
||||
attr_accessor :was_just_published
|
||||
|
||||
@@ -31,6 +31,10 @@ module Column::Positioned
|
||||
right_column.nil?
|
||||
end
|
||||
|
||||
def adjacent_columns
|
||||
board.columns.where(id: [ left_column&.id, right_column&.id ].compact)
|
||||
end
|
||||
|
||||
private
|
||||
def set_position
|
||||
max_position = board.columns.maximum(:position) || 0
|
||||
|
||||
@@ -10,8 +10,8 @@ class Comment < ApplicationRecord
|
||||
|
||||
scope :chronologically, -> { order created_at: :asc, id: :desc }
|
||||
scope :preloaded, -> { with_rich_text_body.includes(reactions: :reacter) }
|
||||
scope :by_system, -> { joins(:creator).where(creator: { role: "system" }) }
|
||||
scope :by_user, -> { joins(:creator).where.not(creator: { role: "system" }) }
|
||||
scope :by_system, -> { joins(:creator).where(creator: { role: :system }) }
|
||||
scope :by_user, -> { joins(:creator).where.not(creator: { role: :system }) }
|
||||
|
||||
after_create_commit :watch_card_by_creator
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class Filter < ApplicationRecord
|
||||
result = result.where(creator_id: creators.ids) if creators.present?
|
||||
result = result.where(board: boards.ids) if boards.present?
|
||||
result = result.tagged_with(tags.ids) if tags.present?
|
||||
result = result.where("cards.created_at": creation_window) if creation_window
|
||||
result = result.where(cards: { created_at: creation_window }) if creation_window
|
||||
result = result.closed_at_window(closure_window) if closure_window
|
||||
result = result.closed_by(closers) if closers.present?
|
||||
result = terms.reduce(result) do |result, term|
|
||||
|
||||
@@ -8,6 +8,7 @@ class Signup
|
||||
|
||||
validates :email_address, format: { with: URI::MailTo::EMAIL_REGEXP }, on: :identity_creation
|
||||
validates :full_name, :identity, presence: true, on: :completion
|
||||
validates :full_name, length: { maximum: 240 }
|
||||
|
||||
def initialize(...)
|
||||
super
|
||||
|
||||
@@ -31,21 +31,21 @@ class TimeWindowParser
|
||||
def parse(string)
|
||||
case normalize(string)
|
||||
when "today"
|
||||
now.beginning_of_day..now.end_of_day
|
||||
now.all_day
|
||||
when "yesterday"
|
||||
(now - 1.day).beginning_of_day..(now - 1.day).end_of_day
|
||||
(now - 1.day).all_day
|
||||
when "thisweek"
|
||||
now.beginning_of_week..now.end_of_week
|
||||
now.all_week
|
||||
when "thismonth"
|
||||
now.beginning_of_month..now.end_of_month
|
||||
now.all_month
|
||||
when "thisyear"
|
||||
now.beginning_of_year..now.end_of_year
|
||||
now.all_year
|
||||
when "lastweek"
|
||||
(now - 1.week).beginning_of_week..(now - 1.week).end_of_week
|
||||
(now - 1.week).all_week
|
||||
when "lastmonth"
|
||||
(now - 1.month).beginning_of_month..(now - 1.month).end_of_month
|
||||
(now - 1.month).all_month
|
||||
when "lastyear"
|
||||
(now - 1.year).beginning_of_year..(now - 1.year).end_of_year
|
||||
(now - 1.year).all_year
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+2
-2
@@ -6,6 +6,8 @@ class User < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :identity, optional: true
|
||||
|
||||
validates :name, presence: true
|
||||
|
||||
has_many :comments, inverse_of: :creator, dependent: :destroy
|
||||
|
||||
has_many :filters, foreign_key: :creator_id, inverse_of: :creator, dependent: :destroy
|
||||
@@ -14,8 +16,6 @@ class User < ApplicationRecord
|
||||
has_many :pinned_cards, through: :pins, source: :card
|
||||
has_many :exports, class_name: "Account::Export", dependent: :destroy
|
||||
|
||||
scope :with_avatars, -> { preload(:account, :avatar_attachment) }
|
||||
|
||||
def deactivate
|
||||
transaction do
|
||||
accesses.destroy_all
|
||||
|
||||
@@ -2,13 +2,20 @@ module User::Avatar
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
ALLOWED_AVATAR_CONTENT_TYPES = %w[ image/jpeg image/png image/gif image/webp ].freeze
|
||||
MAX_AVATAR_DIMENSIONS = { width: 4096, height: 4096 }.freeze
|
||||
|
||||
included do
|
||||
has_one_attached :avatar do |attachable|
|
||||
attachable.variant :thumb, resize_to_fill: [ 256, 256 ], process: :immediately
|
||||
end
|
||||
|
||||
validate :avatar_content_type_allowed
|
||||
scope :with_avatars, -> { preload(:account, :avatar_attachment) }
|
||||
|
||||
validate :avatar_content_type_allowed, :avatar_dimensions_allowed, if: :avatar_attached?
|
||||
end
|
||||
|
||||
def avatar_attached?
|
||||
avatar.attached?
|
||||
end
|
||||
|
||||
def avatar_thumbnail
|
||||
@@ -17,8 +24,23 @@ module User::Avatar
|
||||
|
||||
private
|
||||
def avatar_content_type_allowed
|
||||
if avatar.attached? && !ALLOWED_AVATAR_CONTENT_TYPES.include?(avatar.content_type)
|
||||
if !ALLOWED_AVATAR_CONTENT_TYPES.include?(avatar.content_type)
|
||||
errors.add(:avatar, "must be a JPEG, PNG, GIF, or WebP image")
|
||||
end
|
||||
end
|
||||
|
||||
def avatar_dimensions_allowed
|
||||
return unless avatar.blob.analyzed? || avatar.blob.analyze
|
||||
|
||||
width = avatar.blob.metadata[:width]
|
||||
height = avatar.blob.metadata[:height]
|
||||
|
||||
if width && width > MAX_AVATAR_DIMENSIONS[:width]
|
||||
errors.add(:avatar, "width must be less than #{MAX_AVATAR_DIMENSIONS[:width]}px")
|
||||
end
|
||||
|
||||
if height && height > MAX_AVATAR_DIMENSIONS[:height]
|
||||
errors.add(:avatar, "height must be less than #{MAX_AVATAR_DIMENSIONS[:height]}px")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,7 +39,7 @@ module User::EmailAddressChangeable
|
||||
|
||||
private
|
||||
def generate_email_address_change_token(from: identity.email_address, to:, **options)
|
||||
options = options.reverse_merge(
|
||||
options = options.with_defaults(
|
||||
for: EMAIL_CHANGE_TOKEN_PURPOSE,
|
||||
old_email_address: from,
|
||||
new_email_address: to,
|
||||
|
||||
@@ -19,7 +19,7 @@ module User::Named
|
||||
|
||||
def familiar_name
|
||||
names = name.split
|
||||
return name if names.length == 1
|
||||
return name if names.length <= 1
|
||||
"#{names.first} #{names[1..].map { |n| "#{n[0]}." }.join}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<li class="flex align-center gap-half" data-filter-target="item" data-navigable-list-target="item">
|
||||
<li class="flex align-center gap-half" data-filter-target="item" data-navigable-list-target="item" role="option">
|
||||
<%= link_to user, class: "txt-ink flex gap-half align-center min-width" do %>
|
||||
<%= avatar_preview_tag user, hidden_for_screen_reader: true %>
|
||||
<div class="txt-align-start overflow-ellipsis">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="settings__user-filter">
|
||||
<input placeholder="Filter…" class="input input--transparent full-width txt-small" type="search" autocorrect="off" autocomplete="off" data-1p-ignore="true" data-filter-target="input" data-action="input->filter#filter">
|
||||
|
||||
<ul class="settings__user-list margin-block-half" data-filter-target="list">
|
||||
<ul class="settings__user-list margin-block-half" data-filter-target="list" role="listbox">
|
||||
<%= render partial: "account/settings/user", collection: users %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -20,3 +20,5 @@
|
||||
<%= render "account/settings/export" %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%= render "account/settings/subscription_panel" if Fizzy.saas? %>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<source src="<%= rails_blob_url(blob) %>" type="<%= blob.content_type %>">
|
||||
</audio>
|
||||
<% elsif blob.variable? %>
|
||||
<%= link_to url_for(blob.variant(variant)), data: { action: "lightbox#open:prevent", lightbox_target: "image" } do %>
|
||||
<%= link_to url_for(blob.variant(variant)), data: { lightbox_target: "image", lightbox_caption_value: blob.filename.to_s } do %>
|
||||
<%= image_tag url_for(blob.variant(variant)), width: width, height: height %>
|
||||
<% end %>
|
||||
<% elsif blob.previewable? %>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<%= turbo_stream.before("closed-cards", partial: "boards/show/column", method: :morph, locals: { column: @column }) %>
|
||||
<%= render "columns/refresh_adjacent_columns", column: @column %>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<%= turbo_stream.remove(dom_id(@column)) %>
|
||||
@@ -6,6 +6,7 @@ json.cache! card do
|
||||
|
||||
json.tags card.tags.pluck(:title).sort
|
||||
|
||||
json.closed card.closed?
|
||||
json.golden card.golden?
|
||||
json.last_active_at card.last_active_at.utc
|
||||
json.created_at card.created_at.utc
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% cache card do %>
|
||||
<section id="<%= dom_id(card, :card_container) %>" class="card-perma" style="--card-color: <%= card.color %>;">
|
||||
<section id="<%= dom_id(card, :card_container) %>" class="card-perma" style="--card-color: <%= card.color %>;">
|
||||
<% cache card do %>
|
||||
<div class="card-perma__actions card-perma__actions--left">
|
||||
<%= render "cards/container/gild", card: card if card.published? && !card.closed? %>
|
||||
<%= render "cards/container/image", card: card %>
|
||||
@@ -33,11 +33,15 @@
|
||||
<%= render "cards/display/preview/bubble", card: card %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if card.published? %>
|
||||
<%= render "cards/container/footer/published", card: card %>
|
||||
<% elsif card.drafted? %>
|
||||
<%= render "cards/container/footer/draft", card: card %>
|
||||
<% if card.published? %>
|
||||
<%= render "cards/container/footer/published", card: card %>
|
||||
<% elsif card.drafted? %>
|
||||
<% if Fizzy.saas? %>
|
||||
<%= render "cards/container/footer/saas/create", card: card %>
|
||||
<% else %>
|
||||
<%= render "cards/container/footer/create", card: card %>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<%= turbo_frame_tag @card, :assignment do %>
|
||||
<%= tag.div class: "max-width full-width", data: {
|
||||
action: "turbo:before-cache@document->dialog#close dialog:show@document->navigable-list#reset keydown->navigable-list#navigate filter:changed->navigable-list#reset",
|
||||
controller: "filter navigable-list",
|
||||
controller: "filter navigable-list assignment-limit",
|
||||
dialog_target: "dialog",
|
||||
navigable_list_focus_on_selection_value: false,
|
||||
navigable_list_actionable_items_value: true } do %>
|
||||
navigable_list_actionable_items_value: true,
|
||||
assignment_limit_limit_value: Assignment::LIMIT,
|
||||
assignment_limit_count_value: @card.assignments.count } do %>
|
||||
|
||||
<div class="flex align-start justify-space-between">
|
||||
<strong class="popup__title">Assign this to…</strong>
|
||||
@@ -17,9 +19,18 @@
|
||||
<ul class="popup__list" data-filter-target="list">
|
||||
<%= render "user", card: @card, user: Current.user, user_label: "Me" do %>
|
||||
<span class="visually-hidden"><%= Current.user.name %></span>
|
||||
<kbd class="txt-xx-small hide-on-touch">m</kbd>
|
||||
<% end %>
|
||||
<%= render collection: @assigned_to, partial: "user", locals: { card: @card } %>
|
||||
<%= render collection: @users, partial: "user", locals: { card: @card } %>
|
||||
<% @users.each do |user| %>
|
||||
<span data-assignment-limit-target="unassigned">
|
||||
<%= render "user", card: @card, user: user %>
|
||||
</span>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<div class="popup__footer" hidden data-assignment-limit-target="limitMessage">
|
||||
Maximum <%= Assignment::LIMIT %> assignees
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<% cache comment do %>
|
||||
<%= turbo_frame_tag comment, :container do %>
|
||||
<%# Bump for CSS changes: 2025-06-30 -%>
|
||||
<div id="<%= dom_id(comment) %>" data-creator-id="<%= comment.creator_id %>" class="comment flex align-start full-width <%= "comment--system" if comment.creator.system? %>">
|
||||
<%# Helper Dependency Updated: avatar_image_tag 2025-12-15 %>
|
||||
<%= turbo_frame_tag comment, :container, class: ["comment--system": comment.creator.system?] do %>
|
||||
<%# Cache bump 2025-12-14: action text attachment rendering changed for lightbox -%>
|
||||
<div id="<%= dom_id(comment) %>" data-creator-id="<%= comment.creator_id %>" class="comment align-start full-width">
|
||||
<figure class="comment__avatar flex-item-no-shrink" aria-hidden="true">
|
||||
<%= avatar_tag comment.creator, hidden_for_screen_reader: true %>
|
||||
</figure>
|
||||
@@ -19,6 +20,11 @@
|
||||
<% end %>
|
||||
</h3>
|
||||
|
||||
<button class="comment__history btn btn--circle-mobile borderless txt-x-small" data-action="toggle-class#toggle">
|
||||
<%= icon_tag "history", class: "icon--mobile-only" %>
|
||||
<span>Full history</span>
|
||||
</button>
|
||||
|
||||
<%= link_to edit_card_comment_path(comment.card, comment),
|
||||
class: "comment__edit btn btn--circle borderless translucent", data: { only_visible_to_you: true } do %>
|
||||
<%= icon_tag "menu-dots-horizontal" %>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="card-perma__notch card-perma__notch--bottom flex-column">
|
||||
<div class="card-perma__notch-new-card-buttons">
|
||||
<%= button_to card_publish_path(card), name: "creation_type", value: "add", class: "btn",
|
||||
title: "Create card (#{ hotkey_label(["ctrl", "enter"]) })",
|
||||
form: { data: { controller: "form" } },
|
||||
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+enter@document->clicker#click keydown.meta+enter@document->clicker#click" } do %>
|
||||
<span>Create card</span>
|
||||
<% end %>
|
||||
|
||||
<%= button_to card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another",
|
||||
title: "Create and add another (#{ hotkey_label(["ctrl", "shift", "enter"]) })", form: { data: { controller: "form" } },
|
||||
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+shift+enter@document->clicker#click keydown.meta+shift+enter@document->clicker#click" } do %>
|
||||
<span>Create and add another</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render "cards/container/footer/saas/near_notice" if Fizzy.saas? %>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<div class="card-perma__notch card-perma__notch--bottom">
|
||||
<%= button_to card_publish_path(card), name: "creation_type", value: "add", class: "btn",
|
||||
title: "Create card (#{ hotkey_label(["ctrl", "enter"]) })",
|
||||
form: { data: { controller: "form" } },
|
||||
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+enter@document->clicker#click keydown.meta+enter@document->clicker#click" } do %>
|
||||
<span>Create card</span>
|
||||
<% end %>
|
||||
|
||||
<%= button_to card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another",
|
||||
title: "Create and add another (#{ hotkey_label(["ctrl", "shift", "enter"]) })", form: { data: { controller: "form" } },
|
||||
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+shift+enter@document->clicker#click keydown.meta+shift+enter@document->clicker#click" } do %>
|
||||
<span>Create and add another</span>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -30,8 +30,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="card__footer">
|
||||
<footer class="card__footer flex gap-half">
|
||||
<%= render "cards/display/preview/meta", card: card, preview: true %>
|
||||
<%= render "cards/display/preview/comments", card: card %>
|
||||
<%= render "cards/display/common/background", card: card %>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<% if Current.user %>
|
||||
<%= button_to "Assign to me", card_assignments_path(card, params: { assignee_id: Current.user.id }), method: :post, data: {controller: "hotkey", action: "keydown.m@document->hotkey#click" }, hidden: true %>
|
||||
<% end %>
|
||||
|
||||
<dialog class="popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-morph-attribute->dialog#preventCloseOnMorphing turbo:submit-end->dialog#close">
|
||||
<%= yield %>
|
||||
</dialog>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%= render "cards/display/common/background", card: card do %>
|
||||
<%= link_to card.image.presence, class: "btn fill-transparent flex-item-justify-end", data: { controller: "tooltip", action: "lightbox#open:prevent", lightbox_target: "image" } do %>
|
||||
<%= link_to card.image.presence, class: "btn fill-transparent flex-item-justify-end", data: { controller: "tooltip", lightbox_target: "image" } do %>
|
||||
<%= icon_tag "picture-zoom" %>
|
||||
<span class="for-screen-reader">Zoom background image</span>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<% comments = card.comments.by_user %>
|
||||
<% if comments.any? %>
|
||||
<div class="card__comments align-center gap-half flex-item-justify-end flex-item-no-shrink">
|
||||
<%= icon_tag "comment" %>
|
||||
<strong><%= comments.count %></strong>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,3 @@
|
||||
<% column.adjacent_columns.each do |adjacent_column| %>
|
||||
<%= turbo_stream.replace(dom_id(adjacent_column), partial: "boards/show/column", method: :morph, locals: { column: adjacent_column }) %>
|
||||
<% end %>
|
||||
@@ -1,4 +1,5 @@
|
||||
<% if @left_column %>
|
||||
<%= turbo_stream.remove(dom_id(@column)) %>
|
||||
<%= turbo_stream.before(@left_column, partial: "boards/show/column", locals: { column: @column }) %>
|
||||
<%= render "columns/refresh_adjacent_columns", column: @column %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<% if @right_column %>
|
||||
<%= turbo_stream.remove(dom_id(@column)) %>
|
||||
<%= turbo_stream.after(@right_column, partial: "boards/show/column", locals: { column: @column }) %>
|
||||
<%= render "columns/refresh_adjacent_columns", column: @column %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<% cache event do %>
|
||||
<%# Helper Dependency Updated: avatar_image_tag 2025-12-15 %>
|
||||
<% if lookup_context.exists?("events/event/eventable/_#{event.action}") %>
|
||||
<%= render "events/event/eventable/#{event.action}", event: event %>
|
||||
<% else %>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<%= render "layouts/shared/head" %>
|
||||
|
||||
<body class="<%= @body_class %>" data-controller="local-time timezone-cookie turbo-navigation theme" data-action="turbo:morph@window->local-time#refreshAll turbo:before-visit@document->turbo-navigation#rememberLocation">
|
||||
<body class="<%= @body_class %>" data-controller="local-time timezone-cookie turbo-navigation theme" data-action="turbo:morph@window->local-time#refreshAll turbo:before-visit@document->turbo-navigation#rememberLocation" data-platform="<%= platform.type %>">
|
||||
<div id="global-container">
|
||||
<header class="header <%= @header_class %>" id="header">
|
||||
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
|
||||
<nav>
|
||||
<%= link_to "https://fizzy.do", class: "header__logo center flex-inline align-center" do %>
|
||||
<span><%= image_tag "logo.png" %></span>
|
||||
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
|
||||
<span><%= image_tag "logo.png", alt: "" %></span>
|
||||
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg" title="Fizzy"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
|
||||
<% end %>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
<%= render "layouts/theme_preference" %>
|
||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "fizzy/saas", "data-turbo-track": "reload" if Fizzy.saas? %>
|
||||
<%= javascript_importmap_tags %>
|
||||
|
||||
<%= tenanted_action_cable_meta_tag %>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<%= tag.button class:"nav__trigger input input--select center flex-inline align-center txt-normal", data: {
|
||||
action: "click->dialog#open keydown.j@document->hotkey#click keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
|
||||
controller: "hotkey" } do %>
|
||||
<span><%= image_tag "logo.png" %></span>
|
||||
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
|
||||
<span><%= image_tag "logo.png", alt: "" %></span>
|
||||
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg" title="Fizzy"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
|
||||
<kbd class="kbd txt-xx-small hide-on-touch">J</kbd>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<%= render "my/menus/people", users: @users %>
|
||||
<%= render "my/menus/settings" %>
|
||||
<%= render "my/menus/shortcuts" %>
|
||||
<%= render "my/menus/accounts", accounts: Current.identity.accounts %>
|
||||
<%= render "my/menus/accounts", accounts: @accounts %>
|
||||
<% end %>
|
||||
|
||||
<footer class="nav__footer">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<% cache notification do %>
|
||||
<%# Helper Dependency Updated: avatar_image_tag 2025-12-15 %>
|
||||
<%= notification_tag notification do %>
|
||||
<%= render "notifications/notification/header", notification: notification do %>
|
||||
<%= notification_toggle_read_button(notification, url: card_reading_path(notification.card)) %>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<% content_for :header do %>
|
||||
<div class="header__actions header__actions--start">
|
||||
<%= back_link_to @card.board.name, published_board_url(@card.board), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click", style: "view-transition-name: card-board-title;" %>
|
||||
<%= back_link_to @card.board.name, published_board_url(@card.board), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</header>
|
||||
|
||||
<%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: "magic-link" } } do |form| %>
|
||||
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large",
|
||||
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large txt-uppercase",
|
||||
autofocus: true, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true,
|
||||
autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code],
|
||||
data: { magic_link_target: "input", action: "keydown.enter->magic-link#submitOnEnter paste->magic-link#submitOnPaste" } %>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h1 class="txt-x-large font-weight-black margin-block-end"><%= @page_title %></h1>
|
||||
|
||||
<%= form_with model: @signup, url: signup_completion_path, scope: "signup", class: "flex flex-column gap", data: { controller: "form" } do |form| %>
|
||||
<%= form.text_field :full_name, class: "input txt-large", autocomplete: "name", placeholder: "Enter your full name…", autofocus: true, required: true %>
|
||||
<%= form.text_field :full_name, class: "input txt-large", autocomplete: "name", placeholder: "Enter your full name…", autofocus: true, required: true, maxlength: 240 %>
|
||||
|
||||
<p>You're one step away. Just enter your name to get your own Fizzy account.</p>
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
curl -s -d content="[Fizzy] ${1}" https://3.basecamp.com/2914079/integrations/5c3a7bd149c975b95312253c/buckets/4896644/chats/676031034/lines
|
||||
@@ -1,8 +1,13 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
bin/rails runner - <<EOF
|
||||
puts "Login with david@example.com to: http://fizzy.localhost:3006/"
|
||||
EOF
|
||||
PORT=3006
|
||||
USE_TAILSCALE=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--tailscale) USE_TAILSCALE=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ! -f tmp/solid-queue.txt ]; then
|
||||
export SOLID_QUEUE_IN_PUMA=false
|
||||
@@ -12,4 +17,32 @@ if [ -f tmp/oss-config.txt ]; then
|
||||
export OSS_CONFIG=1
|
||||
fi
|
||||
|
||||
exec ./bin/rails server -p 3006
|
||||
if [ "$USE_TAILSCALE" = "1" ]; then
|
||||
if ! command -v tailscale >/dev/null 2>&1; then
|
||||
echo "Error: tailscale not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TS_STATUS=$(tailscale status --self --json 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: tailscale not logged in" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TS_HOSTNAME=$(echo "$TS_STATUS" | jq -r '.Self.DNSName | rtrimstr(".")')
|
||||
TS_PORT="4$PORT"
|
||||
|
||||
stop_tailscale() { tailscale serve --https=$TS_PORT off >/dev/null 2>&1; }
|
||||
trap stop_tailscale EXIT INT TERM
|
||||
|
||||
tailscale serve --bg --https=$TS_PORT localhost:$PORT >/dev/null 2>&1
|
||||
if ! tailscale serve status --json 2>/dev/null | jq -e ".TCP.\"$TS_PORT\"" >/dev/null 2>&1; then
|
||||
echo "Error: tailscale serve failed. On Linux, run once: sudo tailscale set --operator=\$USER" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Login with david@example.com to: https://$TS_HOSTNAME:$TS_PORT/"
|
||||
else
|
||||
echo "Login with david@example.com to: http://fizzy.localhost:$PORT/"
|
||||
fi
|
||||
|
||||
./bin/rails server -p $PORT
|
||||
|
||||
@@ -136,8 +136,7 @@ bundle config set --local auto_install true
|
||||
step "Installing RubyGems" bundle install
|
||||
|
||||
if [ -n "$SAAS" ]; then
|
||||
saas_setup=$(bundle show fizzy-saas)/bin/setup
|
||||
source "$saas_setup"
|
||||
source "$app_root/saas/bin/setup"
|
||||
else
|
||||
if [ "$DATABASE_ADAPTER" = "mysql" ]; then
|
||||
oss_mysql_setup
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<%
|
||||
config_path = if Fizzy.saas?
|
||||
gem_path = Gem::Specification.find_by_name("fizzy-saas").gem_dir
|
||||
gem_path = Rails.root.join("saas").to_s
|
||||
File.join(gem_path, "config", "database.yml")
|
||||
else
|
||||
File.join("config", "database.#{Fizzy.db_adapter}.yml")
|
||||
|
||||
@@ -28,6 +28,7 @@ env:
|
||||
- SMTP_USERNAME
|
||||
- SMTP_PASSWORD
|
||||
clear:
|
||||
BASE_URL: https://fizzy.example.com # The public URL of your Fizzy instance
|
||||
MAILER_FROM_ADDRESS: support@example.com # The email "from" address that Fizzy sends email from
|
||||
SMTP_ADDRESS: mail.example.com # The SMTP server you'll use to send email
|
||||
MULTI_TENANT: false # Set to true to allow multiple accounts to sign up
|
||||
|
||||
@@ -34,6 +34,8 @@ Rails.application.configure do
|
||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||
if Rails.root.join("tmp/minio-dev.txt").exist?
|
||||
config.active_storage.service = :devminio
|
||||
config.x.content_security_policy.connect_src = "http://minio.localhost:39000"
|
||||
config.x.content_security_policy.img_src = "http://minio.localhost:39000"
|
||||
else
|
||||
config.active_storage.service = :local
|
||||
end
|
||||
@@ -83,9 +85,14 @@ Rails.application.configure do
|
||||
config.action_mailer.raise_delivery_errors = false
|
||||
end
|
||||
|
||||
config.hosts = %w[ fizzy.localhost localhost 127.0.0.1 ] + [ /^fizzy-\d+(:\d+)$/ ]
|
||||
config.hosts = [
|
||||
"fizzy.localhost",
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
/fizzy-\d+/, # review apps: fizzy-123, fizzy-456:3000
|
||||
/.*\.ts\.net/ # tailscale serve: hostname.tail1234.ts.net
|
||||
]
|
||||
|
||||
# Set host to be used by links generated in mailer and notification view templates.
|
||||
config.action_controller.default_url_options = { host: config.hosts.first, port: 3006 }
|
||||
config.action_mailer.default_url_options = { host: config.hosts.first, port: 3006 }
|
||||
# Canonical host for mailer URLs (emails always link here, not personal Tailscale URLs)
|
||||
config.action_mailer.default_url_options = { host: "#{config.hosts.first}:3006" }
|
||||
end
|
||||
|
||||
@@ -11,15 +11,27 @@ Rails.application.configure do
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
config.action_mailer.smtp_settings = {
|
||||
address: smtp_address,
|
||||
port: ENV.fetch("SMTP_PORT", "587").to_i,
|
||||
port: ENV.fetch("SMTP_PORT", ENV["SMTP_TLS"] == "true" ? "465" : "587").to_i,
|
||||
domain: ENV.fetch("SMTP_DOMAIN", nil),
|
||||
user_name: ENV.fetch("SMTP_USERNAME", nil),
|
||||
password: ENV.fetch("SMTP_PASSWORD", nil),
|
||||
authentication: ENV.fetch("SMTP_AUTHENTICATION", "plain"),
|
||||
enable_starttls_auto: ENV.fetch("SMTP_ENABLE_STARTTLS_AUTO", "true") == "true"
|
||||
tls: ENV["SMTP_TLS"] == "true",
|
||||
openssl_verify_mode: ENV["SMTP_SSL_VERIFY_MODE"]
|
||||
}
|
||||
end
|
||||
|
||||
# Base URL for links in emails and other external references.
|
||||
# Set BASE_URL to your instance's public URL (e.g., https://fizzy.example.com)
|
||||
if base_url = ENV["BASE_URL"].presence
|
||||
uri = URI.parse(base_url)
|
||||
url_options = { host: uri.host, protocol: uri.scheme }
|
||||
url_options[:port] = uri.port if uri.port != uri.default_port
|
||||
|
||||
routes.default_url_options = url_options
|
||||
config.action_mailer.default_url_options = url_options
|
||||
end
|
||||
|
||||
# Code is not reloaded between requests.
|
||||
config.enable_reloading = false
|
||||
|
||||
@@ -75,9 +87,6 @@ Rails.application.configure do
|
||||
.tap { |logger| logger.formatter = ::Logger::Formatter.new }
|
||||
.then { |logger| ActiveSupport::TaggedLogging.new(logger) }
|
||||
|
||||
# Suppress unstructured log lines
|
||||
config.log_level = :fatal
|
||||
|
||||
# Prepend all log lines with the following tags.
|
||||
config.log_tags = [ :request_id ]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user