Merge branch 'main' into fix-blurred-nav-menu-in-safari
This commit is contained in:
@@ -83,7 +83,7 @@ jobs:
|
||||
|
||||
- name: Build and push (${{ matrix.platform }})
|
||||
id: build
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
uses: docker/build-push-action@v6.19.2
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
|
||||
@@ -50,6 +50,14 @@ bin/jobs # Manage Solid Queue jobs
|
||||
bin/kamal deploy # Deploy (requires 1Password CLI for secrets)
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
Default branch: `main`
|
||||
Pre-deploy: `bin/rails saas:enable`
|
||||
Deploy: `bin/kamal deploy -d <destination>`
|
||||
Destinations: production, staging, beta, beta1, beta2, beta3, beta4
|
||||
Note: `beta` is a template requiring `BETA_NUMBER` env var; typical targets are `beta1`-`beta4`.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Multi-Tenancy (URL-Based)
|
||||
@@ -140,7 +148,7 @@ Key recurring tasks (via `config/recurring.yml`):
|
||||
### Imports and exports
|
||||
|
||||
Allow people to move between OSS and SAAS Fizzy instances:
|
||||
- Exports/Imports can be wirtten to/read from local or S3 storage depending on the config of the instance (both myst be supported)
|
||||
- Exports/Imports can be written to/read from local or S3 storage depending on the config of the instance (both must be supported)
|
||||
- Must be able to handle very large ZIP files (500+GB)
|
||||
- Models in `app/models/account/data_transfer/`, `app/models/zip_file`
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ gem "kamal", require: false
|
||||
gem "puma", ">= 5.0"
|
||||
gem "solid_cable", ">= 3.0"
|
||||
gem "solid_cache", "~> 1.0"
|
||||
gem "solid_queue", "~> 1.2"
|
||||
gem "solid_queue", "~> 1.3"
|
||||
gem "sqlite3", ">= 2.0"
|
||||
gem "thruster", require: false
|
||||
gem "trilogy", "~> 2.9"
|
||||
gem "trilogy", "~> 2.10"
|
||||
|
||||
# Features
|
||||
gem "bcrypt", "~> 3.1.7"
|
||||
@@ -54,7 +54,7 @@ group :development, :test do
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem "web-console"
|
||||
gem "web-console", github: "rails/web-console"
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
||||
+71
-64
@@ -1,8 +1,8 @@
|
||||
GIT
|
||||
remote: https://github.com/basecamp/lexxy
|
||||
revision: 4bcf8d0213971560e90734493f8bd835addc3642
|
||||
revision: b7f0f1429df5fa425eac043191ae2b65edee7488
|
||||
specs:
|
||||
lexxy (0.7.4.beta)
|
||||
lexxy (0.7.6.beta)
|
||||
rails (>= 8.0.2)
|
||||
|
||||
GIT
|
||||
@@ -13,7 +13,7 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rails/rails.git
|
||||
revision: 60d92e4e7dfe923528ccdccc18820ccfe841b7b8
|
||||
revision: 12e24eaf2f0a9613e015653f013dd131317d9bf5
|
||||
branch: main
|
||||
specs:
|
||||
actioncable (8.2.0.alpha)
|
||||
@@ -47,7 +47,7 @@ GIT
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.2.0.alpha)
|
||||
action_text-trix (~> 2.1.15)
|
||||
action_text-trix (~> 2.1.16)
|
||||
actionpack (= 8.2.0.alpha)
|
||||
activerecord (= 8.2.0.alpha)
|
||||
activestorage (= 8.2.0.alpha)
|
||||
@@ -113,18 +113,27 @@ GIT
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rails/web-console.git
|
||||
revision: 90e3474306f2367cfeaf2875e91e2bc2d71b5f0f
|
||||
specs:
|
||||
web-console (4.3.0)
|
||||
actionview (>= 8.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 8.0.0)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
action_text-trix (2.1.16)
|
||||
railties
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
addressable (2.8.8)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
ast (2.4.3)
|
||||
autotuner (1.1.0)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1203.0)
|
||||
aws-sdk-core (3.241.3)
|
||||
aws-partitions (1.1218.0)
|
||||
aws-sdk-core (3.242.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -132,11 +141,11 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.120.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sdk-kms (1.122.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.211.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sdk-s3 (1.213.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
@@ -147,9 +156,9 @@ GEM
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.0.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.22.0)
|
||||
bootsnap (1.23.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (8.0.1)
|
||||
brakeman (8.0.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bundler-audit (0.9.3)
|
||||
@@ -180,7 +189,7 @@ GEM
|
||||
dotenv (3.2.0)
|
||||
drb (2.2.3)
|
||||
ed25519 (1.4.0)
|
||||
erb (6.0.1)
|
||||
erb (6.0.2)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
@@ -208,20 +217,21 @@ GEM
|
||||
image_processing (1.14.0)
|
||||
mini_magick (>= 4.9.5, < 6)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
importmap-rails (2.2.2)
|
||||
importmap-rails (2.2.3)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
io-console (0.8.2)
|
||||
irb (1.16.0)
|
||||
irb (1.17.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jbuilder (2.14.1)
|
||||
actionview (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.18.0)
|
||||
json (2.18.1)
|
||||
jwt (3.1.2)
|
||||
base64
|
||||
kamal (2.10.1)
|
||||
@@ -258,7 +268,8 @@ GEM
|
||||
mini_magick (5.3.1)
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
minitest (6.0.1)
|
||||
minitest (6.0.2)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
mission_control-jobs (1.1.0)
|
||||
actioncable (>= 7.1)
|
||||
@@ -271,12 +282,12 @@ GEM
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
mittens (0.3.1)
|
||||
mocha (3.0.1)
|
||||
mocha (3.0.2)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.8.0)
|
||||
net-http-persistent (4.0.8)
|
||||
connection_pool (>= 2.2.4, < 4)
|
||||
net-imap (0.6.2)
|
||||
net-imap (0.6.3)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -291,21 +302,21 @@ GEM
|
||||
net-protocol
|
||||
net-ssh (7.3.0)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.0-aarch64-linux-gnu)
|
||||
nokogiri (1.19.1-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-aarch64-linux-musl)
|
||||
nokogiri (1.19.1-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-arm-linux-gnu)
|
||||
nokogiri (1.19.1-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-arm-linux-musl)
|
||||
nokogiri (1.19.1-arm-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-arm64-darwin)
|
||||
nokogiri (1.19.1-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-darwin)
|
||||
nokogiri (1.19.1-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-linux-gnu)
|
||||
nokogiri (1.19.1-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-linux-musl)
|
||||
nokogiri (1.19.1-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
openssl (4.0.0)
|
||||
ostruct (0.6.3)
|
||||
@@ -319,7 +330,7 @@ GEM
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.8.0)
|
||||
prism (1.9.0)
|
||||
propshaft (1.3.1)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
@@ -327,12 +338,12 @@ GEM
|
||||
psych (5.3.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.2)
|
||||
puma (7.1.0)
|
||||
public_suffix (7.0.2)
|
||||
puma (7.2.0)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.4)
|
||||
rack (3.2.5)
|
||||
rack-mini-profiler (4.0.1)
|
||||
rack (>= 1.2.0)
|
||||
rack-session (2.1.1)
|
||||
@@ -346,12 +357,12 @@ GEM
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
rails-html-sanitizer (1.7.0)
|
||||
loofah (~> 2.25)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.1)
|
||||
rdoc (7.0.3)
|
||||
rdoc (7.2.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
@@ -399,7 +410,7 @@ GEM
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (3.2.2)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.40.0)
|
||||
selenium-webdriver (4.41.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
@@ -414,21 +425,21 @@ GEM
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
solid_queue (1.2.4)
|
||||
solid_queue (1.3.2)
|
||||
activejob (>= 7.1)
|
||||
activerecord (>= 7.1)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
fugit (~> 1.11)
|
||||
railties (>= 7.1)
|
||||
thor (>= 1.3.1)
|
||||
sqlite3 (2.8.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.8.0-aarch64-linux-musl)
|
||||
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)
|
||||
sqlite3 (2.9.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.9.0-aarch64-linux-musl)
|
||||
sqlite3 (2.9.0-arm-linux-gnu)
|
||||
sqlite3 (2.9.0-arm-linux-musl)
|
||||
sqlite3 (2.9.0-arm64-darwin)
|
||||
sqlite3 (2.9.0-x86_64-darwin)
|
||||
sqlite3 (2.9.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.9.0-x86_64-linux-musl)
|
||||
sshkit (1.25.0)
|
||||
base64
|
||||
logger
|
||||
@@ -436,20 +447,21 @@ GEM
|
||||
net-sftp (>= 2.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
ostruct
|
||||
stackprof (0.2.27)
|
||||
stackprof (0.2.28)
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.2.0)
|
||||
thor (1.5.0)
|
||||
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)
|
||||
thruster (0.1.18)
|
||||
thruster (0.1.18-aarch64-linux)
|
||||
thruster (0.1.18-arm64-darwin)
|
||||
thruster (0.1.18-x86_64-darwin)
|
||||
thruster (0.1.18-x86_64-linux)
|
||||
timeout (0.6.0)
|
||||
trilogy (2.9.0)
|
||||
trilogy (2.10.0)
|
||||
bigdecimal
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.21)
|
||||
turbo-rails (2.0.23)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
@@ -459,11 +471,6 @@ GEM
|
||||
unicode-emoji (4.1.0)
|
||||
uri (1.1.1)
|
||||
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.1.0)
|
||||
jwt (~> 3.0)
|
||||
openssl (>= 3.0)
|
||||
@@ -478,7 +485,7 @@ GEM
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.4)
|
||||
zeitwerk (2.7.5)
|
||||
zip_kit (6.3.4)
|
||||
|
||||
PLATFORMS
|
||||
@@ -488,7 +495,7 @@ PLATFORMS
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
arm64-darwin
|
||||
x86_64-darwin-25
|
||||
x86_64-darwin
|
||||
x86_64-linux
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
@@ -526,16 +533,16 @@ DEPENDENCIES
|
||||
selenium-webdriver
|
||||
solid_cable (>= 3.0)
|
||||
solid_cache (~> 1.0)
|
||||
solid_queue (~> 1.2)
|
||||
solid_queue (~> 1.3)
|
||||
sqlite3 (>= 2.0)
|
||||
stackprof
|
||||
stimulus-rails
|
||||
thruster
|
||||
trilogy (~> 2.9)
|
||||
trilogy (~> 2.10)
|
||||
turbo-rails
|
||||
useragent!
|
||||
vcr
|
||||
web-console
|
||||
web-console!
|
||||
web-push
|
||||
webmock
|
||||
zip_kit
|
||||
|
||||
@@ -4,12 +4,16 @@ eval_gemfile "Gemfile"
|
||||
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
|
||||
|
||||
gem "activeresource", require: "active_resource"
|
||||
gem "actionpack-xml_parser" # needed by queenbee for XML request body parsing
|
||||
gem "stripe", "~> 18.0"
|
||||
gem "queenbee", bc: "queenbee-plugin"
|
||||
gem "fizzy-saas", path: "saas"
|
||||
gem "console1984", bc: "console1984"
|
||||
gem "audits1984", bc: "audits1984", branch: "flavorjones/coworker-api"
|
||||
|
||||
# Native push notifications (iOS/Android)
|
||||
gem "action_push_native"
|
||||
|
||||
# Telemetry
|
||||
gem "rails_structured_logging", bc: "rails-structured-logging"
|
||||
gem "sentry-ruby"
|
||||
|
||||
+122
-62
@@ -23,9 +23,9 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/lexxy
|
||||
revision: 4bcf8d0213971560e90734493f8bd835addc3642
|
||||
revision: b7f0f1429df5fa425eac043191ae2b65edee7488
|
||||
specs:
|
||||
lexxy (0.7.4.beta)
|
||||
lexxy (0.7.6.beta)
|
||||
rails (>= 8.0.2)
|
||||
|
||||
GIT
|
||||
@@ -39,9 +39,9 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/rails-structured-logging
|
||||
revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6
|
||||
revision: f6633fd8fee5906d8affa2329c61de7a3450eb7e
|
||||
specs:
|
||||
rails_structured_logging (0.2.1)
|
||||
rails_structured_logging (0.3.0)
|
||||
json
|
||||
rails (>= 6.0.0)
|
||||
|
||||
@@ -62,7 +62,7 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rails/rails.git
|
||||
revision: 60d92e4e7dfe923528ccdccc18820ccfe841b7b8
|
||||
revision: 12e24eaf2f0a9613e015653f013dd131317d9bf5
|
||||
branch: main
|
||||
specs:
|
||||
actioncable (8.2.0.alpha)
|
||||
@@ -96,7 +96,7 @@ GIT
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.2.0.alpha)
|
||||
action_text-trix (~> 2.1.15)
|
||||
action_text-trix (~> 2.1.16)
|
||||
actionpack (= 8.2.0.alpha)
|
||||
activerecord (= 8.2.0.alpha)
|
||||
activestorage (= 8.2.0.alpha)
|
||||
@@ -162,6 +162,15 @@ GIT
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rails/web-console.git
|
||||
revision: 90e3474306f2367cfeaf2875e91e2bc2d71b5f0f
|
||||
specs:
|
||||
web-console (4.3.0)
|
||||
actionview (>= 8.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 8.0.0)
|
||||
|
||||
PATH
|
||||
remote: saas
|
||||
specs:
|
||||
@@ -186,8 +195,18 @@ PATH
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
action_push_native (0.3.1)
|
||||
activejob (>= 8.0)
|
||||
activerecord (>= 8.0)
|
||||
googleauth (~> 1.14)
|
||||
httpx (~> 1.6)
|
||||
jwt (>= 2)
|
||||
railties (>= 8.0)
|
||||
action_text-trix (2.1.16)
|
||||
railties
|
||||
actionpack-xml_parser (2.0.1)
|
||||
actionpack (>= 5.0)
|
||||
railties (>= 5.0)
|
||||
activemodel-serializers-xml (1.0.3)
|
||||
activemodel (>= 5.0.0.a)
|
||||
activesupport (>= 5.0.0.a)
|
||||
@@ -196,15 +215,15 @@ GEM
|
||||
activemodel (>= 7.0)
|
||||
activemodel-serializers-xml (~> 1.0)
|
||||
activesupport (>= 7.0)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
addressable (2.8.8)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
anyway_config (2.7.2)
|
||||
ruby-next-core (~> 1.0)
|
||||
ast (2.4.3)
|
||||
autotuner (1.1.0)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1203.0)
|
||||
aws-sdk-core (3.241.3)
|
||||
aws-partitions (1.1218.0)
|
||||
aws-sdk-core (3.242.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -212,11 +231,11 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.120.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sdk-kms (1.122.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.211.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sdk-s3 (1.213.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
@@ -227,9 +246,9 @@ GEM
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.0.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.22.0)
|
||||
bootsnap (1.23.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (8.0.1)
|
||||
brakeman (8.0.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bundler-audit (0.9.3)
|
||||
@@ -261,17 +280,24 @@ GEM
|
||||
drb (2.2.3)
|
||||
dry-initializer (3.2.0)
|
||||
ed25519 (1.4.0)
|
||||
erb (6.0.1)
|
||||
erb (6.0.2)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
faker (3.6.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.14.0)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-net_http (3.4.2)
|
||||
net-http (~> 0.5)
|
||||
ffi (1.17.2-aarch64-linux-gnu)
|
||||
ffi (1.17.2-aarch64-linux-musl)
|
||||
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)
|
||||
@@ -282,27 +308,43 @@ GEM
|
||||
addressable (>= 2.5.0)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
google-cloud-env (2.3.1)
|
||||
base64 (~> 0.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-logging-utils (0.2.0)
|
||||
googleauth (1.16.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.2)
|
||||
google-logging-utils (~> 0.1)
|
||||
jwt (>= 1.4, < 4.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
gvltools (0.4.0)
|
||||
hashdiff (1.2.1)
|
||||
http-2 (1.1.1)
|
||||
httpx (1.7.1)
|
||||
http-2 (>= 1.0.0)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.14.0)
|
||||
mini_magick (>= 4.9.5, < 6)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
importmap-rails (2.2.2)
|
||||
importmap-rails (2.2.3)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
io-console (0.8.2)
|
||||
irb (1.16.0)
|
||||
irb (1.17.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jbuilder (2.14.1)
|
||||
actionview (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.18.0)
|
||||
json (2.18.1)
|
||||
jwt (3.1.2)
|
||||
base64
|
||||
kamal (2.10.1)
|
||||
@@ -339,7 +381,8 @@ GEM
|
||||
mini_magick (5.3.1)
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
minitest (6.0.1)
|
||||
minitest (6.0.2)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
mission_control-jobs (1.1.0)
|
||||
actioncable (>= 7.1)
|
||||
@@ -352,12 +395,15 @@ GEM
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
mittens (0.3.1)
|
||||
mocha (3.0.1)
|
||||
mocha (3.0.2)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.19.1)
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
net-http-persistent (4.0.8)
|
||||
connection_pool (>= 2.2.4, < 4)
|
||||
net-imap (0.6.2)
|
||||
net-imap (0.6.3)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -372,21 +418,24 @@ GEM
|
||||
net-protocol
|
||||
net-ssh (7.3.0)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.0-aarch64-linux-gnu)
|
||||
nokogiri (1.19.1-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-aarch64-linux-musl)
|
||||
nokogiri (1.19.1-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-arm-linux-gnu)
|
||||
nokogiri (1.19.1-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-arm-linux-musl)
|
||||
nokogiri (1.19.1-arm-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-arm64-darwin)
|
||||
nokogiri (1.19.1-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-linux-gnu)
|
||||
nokogiri (1.19.1-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-linux-musl)
|
||||
nokogiri (1.19.1-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.1-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
openssl (4.0.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.0)
|
||||
@@ -398,7 +447,7 @@ GEM
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.8.0)
|
||||
prism (1.9.0)
|
||||
prometheus-client-mmap (1.4.0)
|
||||
base64
|
||||
bigdecimal
|
||||
@@ -419,6 +468,11 @@ GEM
|
||||
bigdecimal
|
||||
logger
|
||||
rb_sys (~> 0.9.117)
|
||||
prometheus-client-mmap (1.4.0-x86_64-darwin)
|
||||
base64
|
||||
bigdecimal
|
||||
logger
|
||||
rb_sys (~> 0.9.117)
|
||||
prometheus-client-mmap (1.4.0-x86_64-linux-gnu)
|
||||
base64
|
||||
bigdecimal
|
||||
@@ -436,12 +490,12 @@ GEM
|
||||
psych (5.3.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.2)
|
||||
puma (7.1.0)
|
||||
public_suffix (7.0.2)
|
||||
puma (7.2.0)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.4)
|
||||
rack (3.2.5)
|
||||
rack-mini-profiler (4.0.1)
|
||||
rack (>= 1.2.0)
|
||||
rack-session (2.1.1)
|
||||
@@ -455,15 +509,15 @@ GEM
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
rails-html-sanitizer (1.7.0)
|
||||
loofah (~> 2.25)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.1)
|
||||
rake-compiler-dock (1.9.1)
|
||||
rb_sys (0.9.117)
|
||||
rake-compiler-dock (= 1.9.1)
|
||||
rdoc (7.0.3)
|
||||
rdoc (7.2.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
@@ -513,7 +567,7 @@ GEM
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (3.2.2)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.40.0)
|
||||
selenium-webdriver (4.41.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
@@ -525,6 +579,11 @@ GEM
|
||||
sentry-ruby (6.2.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
signet (0.21.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 4.0)
|
||||
multi_json (~> 1.10)
|
||||
sniffer (0.5.0)
|
||||
anyway_config (>= 1.0)
|
||||
dry-initializer (~> 3)
|
||||
@@ -537,20 +596,21 @@ GEM
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
solid_queue (1.2.4)
|
||||
solid_queue (1.3.2)
|
||||
activejob (>= 7.1)
|
||||
activerecord (>= 7.1)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
fugit (~> 1.11)
|
||||
railties (>= 7.1)
|
||||
thor (>= 1.3.1)
|
||||
sqlite3 (2.8.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.8.0-aarch64-linux-musl)
|
||||
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-linux-gnu)
|
||||
sqlite3 (2.8.0-x86_64-linux-musl)
|
||||
sqlite3 (2.9.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.9.0-aarch64-linux-musl)
|
||||
sqlite3 (2.9.0-arm-linux-gnu)
|
||||
sqlite3 (2.9.0-arm-linux-musl)
|
||||
sqlite3 (2.9.0-arm64-darwin)
|
||||
sqlite3 (2.9.0-x86_64-darwin)
|
||||
sqlite3 (2.9.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.9.0-x86_64-linux-musl)
|
||||
sshkit (1.25.0)
|
||||
base64
|
||||
logger
|
||||
@@ -558,20 +618,22 @@ GEM
|
||||
net-sftp (>= 2.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
ostruct
|
||||
stackprof (0.2.27)
|
||||
stackprof (0.2.28)
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.2.0)
|
||||
stripe (18.0.1)
|
||||
thor (1.5.0)
|
||||
thruster (0.1.17)
|
||||
thruster (0.1.17-aarch64-linux)
|
||||
thruster (0.1.17-arm64-darwin)
|
||||
thruster (0.1.17-x86_64-linux)
|
||||
thruster (0.1.18)
|
||||
thruster (0.1.18-aarch64-linux)
|
||||
thruster (0.1.18-arm64-darwin)
|
||||
thruster (0.1.18-x86_64-darwin)
|
||||
thruster (0.1.18-x86_64-linux)
|
||||
timeout (0.6.0)
|
||||
trilogy (2.9.0)
|
||||
trilogy (2.10.0)
|
||||
bigdecimal
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.21)
|
||||
turbo-rails (2.0.23)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
@@ -581,11 +643,6 @@ GEM
|
||||
unicode-emoji (4.1.0)
|
||||
uri (1.1.1)
|
||||
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.1.0)
|
||||
jwt (~> 3.0)
|
||||
openssl (>= 3.0)
|
||||
@@ -628,7 +685,7 @@ GEM
|
||||
anyway_config (>= 1.3, < 3)
|
||||
railties
|
||||
yabeda (~> 0.8)
|
||||
zeitwerk (2.7.4)
|
||||
zeitwerk (2.7.5)
|
||||
zip_kit (6.3.4)
|
||||
|
||||
PLATFORMS
|
||||
@@ -638,11 +695,14 @@ PLATFORMS
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
arm64-darwin
|
||||
x86_64-darwin
|
||||
x86_64-linux
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
action_push_native
|
||||
actionpack-xml_parser
|
||||
activeresource
|
||||
audits1984!
|
||||
autotuner
|
||||
@@ -685,17 +745,17 @@ DEPENDENCIES
|
||||
sentry-ruby
|
||||
solid_cable (>= 3.0)
|
||||
solid_cache (~> 1.0)
|
||||
solid_queue (~> 1.2)
|
||||
solid_queue (~> 1.3)
|
||||
sqlite3 (>= 2.0)
|
||||
stackprof
|
||||
stimulus-rails
|
||||
stripe (~> 18.0)
|
||||
thruster
|
||||
trilogy (~> 2.9)
|
||||
trilogy (~> 2.10)
|
||||
turbo-rails
|
||||
useragent!
|
||||
vcr
|
||||
web-console
|
||||
web-console!
|
||||
web-push
|
||||
webmock
|
||||
webrick
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="3.25" cy="12" r="3.25"/><circle cx="12" cy="12" r="3.25"/><circle cx="20.75" cy="12" r="3.25"/></svg>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="3"/><circle cx="4" cy="12" r="3"/><circle cx="20" cy="12" r="3"/></svg>
|
||||
|
Before Width: | Height: | Size: 173 B After Width: | Height: | Size: 158 B |
@@ -18,6 +18,7 @@
|
||||
padding: var(--btn-padding, 0.5em 1.1em);
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
transition: 100ms ease-out;
|
||||
transition-property: background-color, border, box-shadow, color, opacity, scale;
|
||||
|
||||
@@ -127,7 +128,7 @@
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.btn--circle-mobile .icon--mobile-only {
|
||||
.btn .icon--mobile-only {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -271,4 +272,26 @@
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Button utilities
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
:is([data-platform~=mobile], [data-platform~=native]) {
|
||||
.btn--ensure-tap-target-size {
|
||||
--tap-target-z-index: 1;
|
||||
--tap-target-min-size: 44px;
|
||||
|
||||
z-index: var(--tap-target-z-index);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
block-size: var(--tap-target-min-size);
|
||||
inline-size: var(--tap-target-min-size);
|
||||
inset: calc(50% - var(--tap-target-min-size) / 2) auto auto calc(50% - var(--tap-target-min-size) / 2);
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +286,7 @@
|
||||
inset-inline-end: 0;
|
||||
position: absolute;
|
||||
translate: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@@ -769,13 +770,16 @@
|
||||
--panel-padding: 2ch;
|
||||
--panel-size: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap);
|
||||
inset-block-start: 0;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
&[open] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* On Deck (Not Now)
|
||||
@@ -819,6 +823,10 @@
|
||||
position: absolute;
|
||||
translate: 20% -20%;
|
||||
z-index: 1;
|
||||
|
||||
@media (max-width: 639px) {
|
||||
translate: 20% 0%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Maybe column: position bubble relative to the title, not the container */
|
||||
|
||||
@@ -36,20 +36,10 @@
|
||||
|
||||
@media (max-width: 639px) {
|
||||
&:has(.card__closed) {
|
||||
padding-block-start: 2rem;
|
||||
}
|
||||
padding-block-start: 4.5rem;
|
||||
|
||||
&:has(.reactions .reaction) {
|
||||
.card__meta-avatars--assignees {
|
||||
margin-inline-end: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:has(.reactions .reaction)) {
|
||||
&:has(.card__closed) {
|
||||
.card__meta-text--assignees {
|
||||
margin-inline-end: calc(var(--btn-size) + var(--meta-spacer-block));
|
||||
}
|
||||
.card-perma__bg {
|
||||
box-shadow: 0 -2rem 0 0 var(--color-container);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,6 +100,7 @@
|
||||
@media (max-width: 639px) {
|
||||
flex-direction: column;
|
||||
padding-block: var(--card-padding-block) calc(var(--card-padding-block) * 1.5);
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +177,7 @@
|
||||
}
|
||||
|
||||
.card__meta-avatars--assignees {
|
||||
margin-inline: 0 calc(var(--btn-size) + var(--meta-spacer-block));
|
||||
margin-inline: 0;
|
||||
margin-block-start: var(--meta-spacer-block);
|
||||
}
|
||||
|
||||
@@ -232,6 +223,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card__closed {
|
||||
@media (max-width: 639px) {
|
||||
inset: auto 0 100% auto;
|
||||
translate: 0 30%;
|
||||
scale: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
.card__footer {
|
||||
--btn-size: 2.5rem;
|
||||
|
||||
@@ -251,8 +250,12 @@
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
display: grid;
|
||||
font-size: var(--text-x-small);
|
||||
position: relative;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-areas:
|
||||
"meta bg-zoom"
|
||||
"meta reactions";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,17 +289,11 @@
|
||||
}
|
||||
|
||||
&:not(:has(.reaction)) {
|
||||
margin: 0;
|
||||
|
||||
.reactions__trigger {
|
||||
--btn-border-color: var(--color-ink-light);
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -207,6 +207,10 @@
|
||||
line-height: 1.15;
|
||||
text-wrap: balance;
|
||||
|
||||
&.overflow-line-clamp {
|
||||
text-wrap: unset; /* text-wrap: balance breaks -webkit-line-clamp in Safari */
|
||||
}
|
||||
|
||||
.card-field__title {
|
||||
overflow: hidden; /* prevent scrolling on windows */
|
||||
padding-block: var(--autosize-block-padding);
|
||||
@@ -398,6 +402,7 @@
|
||||
position: absolute;
|
||||
rotate: 5deg;
|
||||
transform-origin: top right;
|
||||
z-index: 2;
|
||||
|
||||
.cards & {
|
||||
display: none;
|
||||
@@ -409,12 +414,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
inset: auto calc(var(--card-padding-inline) * -0.5) 100% auto;
|
||||
translate: 0 40%;
|
||||
scale: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
.card:has(.card__closed),
|
||||
|
||||
@@ -84,6 +84,17 @@
|
||||
.reactions {
|
||||
margin-block-start: var(--block-space-half);
|
||||
margin-inline: calc(var(--column-gap) / -1);
|
||||
|
||||
&:not(:has(.reaction)) {
|
||||
inset-block-end: var(--comment-padding-block);
|
||||
inset-inline-end: calc(var(--comment-padding-inline) / 2);
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
inset-inline-end: calc(var(--comment-padding-inline) / 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@layer components {
|
||||
.flash {
|
||||
display: flex;
|
||||
inset-block-start: var(--block-space);
|
||||
inset-block-start: calc(var(--block-space) + var(--custom-safe-inset-top));
|
||||
inset-inline-start: 50%;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
@@ -16,7 +16,9 @@
|
||||
color: var(--flash-color, var(--color-ink-inverted));
|
||||
display: inline-flex;
|
||||
font-size: var(--font-size-medium);
|
||||
inline-size: max-content;
|
||||
margin: 0 auto;
|
||||
max-inline-size: 90vw;
|
||||
padding: 0.7em 1.4em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
|
||||
--lexxy-color-canvas: var(--color-canvas);
|
||||
|
||||
--lexxy-color-accent-dark: var(--color-accent-dark);
|
||||
--lexxy-color-accent-medium: var(--color-accent-medium);
|
||||
--lexxy-color-accent-light: var(--color-accent-light);
|
||||
--lexxy-color-accent-lightest: var(--color-accent-lightest);
|
||||
--lexxy-color-accent-dark: var(--color-ink-dark);
|
||||
--lexxy-color-accent-medium: var(--color-ink-medium);
|
||||
--lexxy-color-accent-light: var(--color-ink-light);
|
||||
--lexxy-color-accent-lightest: var(--color-ink-lighter);
|
||||
|
||||
--lexxy-color-red: oklch(var(--lch-red-medium));
|
||||
--lexxy-color-green: oklch(var(--lch-green-medium));
|
||||
@@ -38,6 +38,8 @@
|
||||
--lexxy-color-table-cell-selected-bg: var(--lexxy-color-selected);
|
||||
--lexxy-color-table-cell-toggle: var(--lexxy-color-selected);
|
||||
--lexxy-color-table-cell-remove: oklch(var(--lch-red-medium) / 20%);
|
||||
|
||||
--lexxy-focus-ring-offset: 2px;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
@@ -139,7 +141,9 @@
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
padding-inline-start: 1ch;
|
||||
&:not(.lexxy-prompt-menu) {
|
||||
padding-inline-start: 1ch;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
|
||||
@@ -20,6 +20,16 @@
|
||||
/* Header
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
.header {
|
||||
padding-block-start: var(--custom-safe-inset-top);
|
||||
}
|
||||
|
||||
.header--mobile-actions-stack {
|
||||
.header__title {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header:is(
|
||||
:not(:has(.header__title, .header__actions)),
|
||||
:not(:has(.header__title, .header__actions--end)):has(.header__actions--start .btn--back:only-child)
|
||||
@@ -68,13 +78,32 @@
|
||||
|
||||
.card-perma--draft {
|
||||
.card-perma__bg {
|
||||
box-shadow: 0 calc(100vh + 100px) 0 100vh var(--color-container);
|
||||
padding-inline: 0;
|
||||
padding-block-start: 0;
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: linear-gradient(to bottom, var(--color-canvas), var(--card-bg-color));
|
||||
box-shadow: 0 101vh 0 100vh var(--card-bg-color);
|
||||
}
|
||||
|
||||
.card__board {
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
}
|
||||
|
||||
.card-perma__notch--bottom {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Search
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
.search {
|
||||
overscroll-behavior: auto;
|
||||
}
|
||||
|
||||
.search__title {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -69,11 +69,11 @@
|
||||
0 0.2em 0.2em oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.4em 0.4em oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.8em 0.8em oklch(var(--lch-blue-medium) / 5%);
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: var(--nav-section-gap);
|
||||
grid-template-rows: auto 1fr auto;
|
||||
inset: 0 0 auto 0;
|
||||
margin-inline: auto;
|
||||
max-block-size: calc(100vh - var(--block-space) - var(--footer-height));
|
||||
max-block-size: calc(100dvh - var(--block-space) - var(--footer-height));
|
||||
overflow: hidden;
|
||||
padding-block-end: 0;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
@@ -82,7 +82,7 @@
|
||||
z-index: var(--z-nav);
|
||||
|
||||
@media (max-height: 668px) {
|
||||
max-block-size: calc(100vh - var(--block-space));
|
||||
max-block-size: calc(100dvh - var(--block-space));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -232,8 +232,7 @@
|
||||
/* Settings
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
.settings__user-filter .input,
|
||||
.settings__user-list-tips {
|
||||
.settings__user-filter .input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
flex-wrap: wrap;
|
||||
gap: var(--inline-space-half);
|
||||
inline-size: 100%;
|
||||
z-index: 3;
|
||||
|
||||
&:has([open]) {
|
||||
z-index: var(--z-popup);
|
||||
@@ -17,14 +18,6 @@
|
||||
|
||||
&:not(:has(.reaction)) {
|
||||
inline-size: auto;
|
||||
inset-block-end: var(--comment-padding-block);
|
||||
inset-inline-end: calc(var(--comment-padding-inline) / 2);
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
inset-inline-end: calc(var(--comment-padding-inline) / 3);
|
||||
}
|
||||
|
||||
.reactions__list {
|
||||
display: none;
|
||||
|
||||
@@ -117,7 +117,7 @@ summary {
|
||||
|
||||
.search-perma {
|
||||
.search__form > label,
|
||||
.search__reset {
|
||||
.search__form:has(.search__input:placeholder-shown) .search__reset {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,20 @@
|
||||
--settings-spacer: var(--block-space);
|
||||
--settings-item-padding-inline: 0.5ch;
|
||||
|
||||
display: grid;
|
||||
align-items: start;
|
||||
display: flex;
|
||||
gap: calc(var(--settings-spacer) * 2);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
max-inline-size: min(100ch, 100%);
|
||||
|
||||
@media (min-width: 960px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
flex-direction: row;
|
||||
|
||||
.settings__panel {
|
||||
max-inline-size: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +62,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Users
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
.settings__section--users {
|
||||
.settings__section:has(.settings__scrollable-list) {
|
||||
@media (min-width: 640px) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -66,24 +70,8 @@
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
.settings__user-filter {
|
||||
--btn-size: 3.5ch;
|
||||
--avatar-size: var(--btn-size);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--settings-spacer) / 2);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.settings__user-filter--bg {
|
||||
background-color: var(--color-ink-lightest);
|
||||
border-radius: 0.5em;
|
||||
margin-block: 0;
|
||||
padding: var(--settings-spacer);
|
||||
}
|
||||
|
||||
.settings__user-list {
|
||||
.settings__scrollable-list {
|
||||
flex: 1 1 auto;
|
||||
list-style: none;
|
||||
margin: calc(var(--settings-spacer) / -4) calc(-1 * var(--settings-item-padding-inline));
|
||||
@@ -116,15 +104,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.settings__user-list-tips {
|
||||
border-top: 1px solid var(--color-ink-light);
|
||||
color: var(--color-ink-dark);
|
||||
font-size: var(--text-small);
|
||||
padding-block-start: var(--settings-spacer);
|
||||
/* Users
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
.settings__user-filter--bg & {
|
||||
margin-inline: calc(-1 * var(--settings-spacer));
|
||||
padding-inline: var(--settings-spacer);
|
||||
}
|
||||
.settings__user-filter {
|
||||
--btn-size: 3.5ch;
|
||||
--avatar-size: var(--btn-size);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--settings-spacer) / 2);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.settings__user-filter--bg {
|
||||
background-color: var(--color-ink-lightest);
|
||||
border-radius: 0.5em;
|
||||
margin-block: 0;
|
||||
padding: var(--settings-spacer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ class BoardsController < ApplicationController
|
||||
before_action :ensure_permission_to_admin_board, only: %i[ update destroy ]
|
||||
|
||||
def index
|
||||
set_page_and_extract_portion_from Current.user.boards
|
||||
set_page_and_extract_portion_from Current.user.boards.ordered_by_recently_accessed.includes(creator: :identity)
|
||||
fresh_when etag: @page.records
|
||||
end
|
||||
|
||||
def show
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class ClientConfigurationsController < ApplicationController
|
||||
skip_before_action :require_account, :require_authentication
|
||||
allow_unauthorized_access
|
||||
|
||||
def show
|
||||
expires_in 1.minute, public: true
|
||||
|
||||
render action: client_configuration_name
|
||||
end
|
||||
|
||||
private
|
||||
def client_configuration_name
|
||||
"#{params.require(:platform)}_v#{params.require(:version)}"
|
||||
end
|
||||
end
|
||||
@@ -7,14 +7,10 @@ module RequestForgeryProtection
|
||||
|
||||
private
|
||||
def verified_via_header_only?
|
||||
super || allowed_api_request? || allowed_insecure_context_request?
|
||||
super || allowed_api_request?
|
||||
end
|
||||
|
||||
def allowed_api_request?
|
||||
sec_fetch_site_value.nil? && request.format.json?
|
||||
end
|
||||
|
||||
def allowed_insecure_context_request?
|
||||
sec_fetch_site_value.nil? && !request.ssl? && !Rails.configuration.force_ssl
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
class Notifications::TraysController < ApplicationController
|
||||
MAX_ENTRIES_LIMIT = 100
|
||||
|
||||
def show
|
||||
@notifications = Current.user.notifications.unread.preloaded.ordered.limit(100)
|
||||
@notifications = unread_notifications
|
||||
if include_read?
|
||||
@notifications += read_notifications
|
||||
end
|
||||
|
||||
# Invalidate on the whole set instead of the unread set since the max updated at in the unread set
|
||||
# can stay the same when reading old notifications.
|
||||
fresh_when Current.user.notifications
|
||||
fresh_when etag: [ Current.user.notifications, include_read? ]
|
||||
end
|
||||
|
||||
private
|
||||
def unread_notifications
|
||||
Current.user.notifications.unread.preloaded.ordered.limit(MAX_ENTRIES_LIMIT)
|
||||
end
|
||||
|
||||
def read_notifications
|
||||
Current.user.notifications.read.preloaded.ordered.limit(MAX_ENTRIES_LIMIT)
|
||||
end
|
||||
|
||||
def include_read?
|
||||
ActiveModel::Type::Boolean.new.cast(params[:include_read])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,10 +2,12 @@ class SearchesController < ApplicationController
|
||||
include Turbo::DriveHelper
|
||||
|
||||
def show
|
||||
if card = Current.user.accessible_cards.find_by_id(params[:q])
|
||||
@query = params[:q].blank? ? nil : params[:q]
|
||||
|
||||
if card = Current.user.accessible_cards.find_by_id(@query)
|
||||
@card = card
|
||||
else
|
||||
set_page_and_extract_portion_from Current.user.search(params[:q])
|
||||
set_page_and_extract_portion_from Current.user.search(@query)
|
||||
@recent_search_queries = Current.user.search_queries.order(updated_at: :desc).limit(10)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,7 +29,7 @@ class Signups::CompletionsController < ApplicationController
|
||||
redirect_to landing_url(script_name: @signup.account.slug)
|
||||
end
|
||||
|
||||
format.json { render json: { account_id: @signup.account.id }, status: :created }
|
||||
format.json { head :created }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ class UsersController < ApplicationController
|
||||
before_action :ensure_permission_to_change_user, only: %i[ update destroy ]
|
||||
|
||||
def index
|
||||
set_page_and_extract_portion_from Current.account.users.active.alphabetically
|
||||
set_page_and_extract_portion_from Current.account.users.active.alphabetically.includes(:identity)
|
||||
end
|
||||
|
||||
def show
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
require "zlib"
|
||||
|
||||
module AvatarsHelper
|
||||
AVATAR_COLORS = %w[
|
||||
#AF2E1B #CC6324 #3B4B59 #BFA07A #ED8008 #ED3F1C #BF1B1B #736B1E #D07B53
|
||||
#736356 #AD1D1D #BF7C2A #C09C6F #698F9C #7C956B #5D618F #3B3633 #67695E
|
||||
]
|
||||
|
||||
def avatar_background_color(user)
|
||||
AVATAR_COLORS[Zlib.crc32(user.to_param) % AVATAR_COLORS.size]
|
||||
user.avatar_background_color
|
||||
end
|
||||
|
||||
def avatar_tag(user, hidden_for_screen_reader: false, **options)
|
||||
|
||||
@@ -9,12 +9,4 @@ module BoardsHelper
|
||||
icon_tag("settings") + tag.span("Settings for #{board.name}", class: "for-screen-reader")
|
||||
end
|
||||
end
|
||||
|
||||
def bridged_button_to_board(board)
|
||||
link_to "Go to #{board.name}", board, hidden: true, data: {
|
||||
bridge__buttons_target: "button",
|
||||
bridge_icon_url: bridge_icon("board"),
|
||||
bridge_title: "Go to #{board.name}"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,4 +2,36 @@ module BridgeHelper
|
||||
def bridge_icon(name)
|
||||
asset_url("#{name}.svg")
|
||||
end
|
||||
|
||||
def bridged_button_to_board(board)
|
||||
link_to "Go to #{board.name}", board, hidden: true, data: {
|
||||
bridge__buttons_target: "button",
|
||||
bridge_icon_url: bridge_icon("board"),
|
||||
bridge_title: "Go to #{board.name}"
|
||||
}
|
||||
end
|
||||
|
||||
def bridged_share_url_button(description = nil)
|
||||
tag.button "Share", hidden: true, data: {
|
||||
controller: "bridge--share",
|
||||
action: "bridge--share#shareUrl",
|
||||
bridge__overflow_menu_target: "item",
|
||||
bridge_title: "Share",
|
||||
bridge_share_description: description
|
||||
}
|
||||
end
|
||||
|
||||
def bridge_share_card_description(card)
|
||||
date_added = card.created_at.strftime("%b %e")
|
||||
date_updated = card.last_active_at.strftime("%b %e")
|
||||
author = card.creator.familiar_name
|
||||
assignees = card.assignees.any? ? "assigned to #{card.assignees.map { |assignee| h assignee.familiar_name }.to_sentence}" : "not assigned"
|
||||
"Added #{date_added} by #{author} and #{assignees}. Updated #{date_updated}"
|
||||
end
|
||||
|
||||
def bridge_share_board_description(board)
|
||||
count_open = board.cards.active.count
|
||||
count_in_stream = board.cards.awaiting_triage.count
|
||||
"#{count_open} open cards, #{count_in_stream} in MAYBE?"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,6 +11,7 @@ module My::MenuHelper
|
||||
aria: { activedescendant: "" },
|
||||
data: {
|
||||
"1p-ignore": "true",
|
||||
dialog_target: "focusMouse",
|
||||
filter_target: "input",
|
||||
nav_section_expander_target: "input",
|
||||
navigable_list_target: "input",
|
||||
|
||||
@@ -22,15 +22,6 @@ export default class extends Controller {
|
||||
this.#hideItem(this.searchTarget)
|
||||
}
|
||||
|
||||
clearInput() {
|
||||
if (this.searchInputTarget.value) {
|
||||
this.searchInputTarget.value = ""
|
||||
this.searchInputTarget.focus()
|
||||
} else {
|
||||
this.reset()
|
||||
}
|
||||
}
|
||||
|
||||
showModalAndSubmit(event) {
|
||||
this.showModal()
|
||||
this.formTarget.requestSubmit()
|
||||
|
||||
@@ -5,6 +5,26 @@ export default class extends BridgeComponent {
|
||||
static component = "buttons"
|
||||
static targets = [ "button" ]
|
||||
|
||||
connect() {
|
||||
super.connect()
|
||||
|
||||
if (!this.beforeUnloadHandler) {
|
||||
this.beforeUnloadHandler = this.handleBeforeUnload.bind(this)
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", this.beforeUnloadHandler)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
super.disconnect()
|
||||
|
||||
if (this.beforeUnloadHandler) {
|
||||
window.removeEventListener("beforeunload", this.beforeUnloadHandler)
|
||||
}
|
||||
|
||||
this.notifyBridgeOfDisconnect()
|
||||
}
|
||||
|
||||
buttonTargetConnected() {
|
||||
this.notifyBridgeOfConnect()
|
||||
}
|
||||
@@ -27,6 +47,14 @@ export default class extends BridgeComponent {
|
||||
})
|
||||
}
|
||||
|
||||
notifyBridgeOfDisconnect() {
|
||||
this.send("disconnect")
|
||||
}
|
||||
|
||||
handleBeforeUnload() {
|
||||
this.notifyBridgeOfDisconnect()
|
||||
}
|
||||
|
||||
#clickButton(message) {
|
||||
const selectedIndex = message.data.selectedIndex
|
||||
this.#enabledButtonTargets[selectedIndex].click()
|
||||
|
||||
@@ -6,13 +6,31 @@ export default class extends BridgeComponent {
|
||||
static targets = [ "submit", "cancel" ]
|
||||
static values = { submitTitle: String }
|
||||
|
||||
connect() {
|
||||
super.connect()
|
||||
|
||||
if (!this.beforeUnloadHandler) {
|
||||
this.beforeUnloadHandler = this.handleBeforeUnload.bind(this)
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", this.beforeUnloadHandler)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
super.disconnect()
|
||||
|
||||
if (this.beforeUnloadHandler) {
|
||||
window.removeEventListener("beforeunload", this.beforeUnloadHandler)
|
||||
}
|
||||
}
|
||||
|
||||
submitTargetConnected() {
|
||||
this.notifyBridgeOfConnect()
|
||||
this.#observeSubmitTarget()
|
||||
}
|
||||
|
||||
submitTargetDisconnected() {
|
||||
this.notifyBridgeOfDisonnect()
|
||||
this.notifyBridgeOfDisconnect()
|
||||
this.submitObserver?.disconnect()
|
||||
}
|
||||
|
||||
@@ -37,7 +55,7 @@ export default class extends BridgeComponent {
|
||||
}
|
||||
}
|
||||
|
||||
notifyBridgeOfDisonnect() {
|
||||
notifyBridgeOfDisconnect() {
|
||||
this.send("disconnect")
|
||||
}
|
||||
|
||||
@@ -49,6 +67,10 @@ export default class extends BridgeComponent {
|
||||
this.send("submitEnd")
|
||||
}
|
||||
|
||||
handleBeforeUnload() {
|
||||
this.notifyBridgeOfDisconnect()
|
||||
}
|
||||
|
||||
#observeSubmitTarget() {
|
||||
this.submitObserver = new MutationObserver(() => {
|
||||
this.send(this.submitTarget.disabled ? "submitDisabled" : "submitEnabled")
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
|
||||
|
||||
export default class extends BridgeComponent {
|
||||
static component = "share"
|
||||
|
||||
shareUrl() {
|
||||
const description = this.bridgeElement.bridgeAttribute("share-description")
|
||||
this.send("shareUrl", {
|
||||
title: document.title,
|
||||
url: window.location.href,
|
||||
description: description
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["searchInput"]
|
||||
|
||||
clearInput() {
|
||||
if (this.searchInputTarget.value) {
|
||||
this.searchInputTarget.value = ""
|
||||
this.searchInputTarget.focus()
|
||||
} else {
|
||||
this.dispatch("reset")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import { BridgeElement } from "@hotwired/hotwire-native-bridge"
|
||||
BridgeElement.prototype.getButton = function() {
|
||||
return {
|
||||
title: this.title,
|
||||
icon: this.getIcon()
|
||||
icon: this.getIcon(),
|
||||
displayTitle: this.getDisplayTitle(),
|
||||
displayAsPrimaryAction: this.getDisplayAsPrimaryAction()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,3 +18,11 @@ BridgeElement.prototype.getIcon = function() {
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
BridgeElement.prototype.getDisplayTitle = function() {
|
||||
return !!this.bridgeAttribute(`display-title`)
|
||||
}
|
||||
|
||||
BridgeElement.prototype.getDisplayAsPrimaryAction = function() {
|
||||
return !!this.bridgeAttribute(`display-as-primary-action`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class Notification::PushJob < ApplicationJob
|
||||
def perform(notification)
|
||||
notification.push
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,6 @@ class PushNotificationJob < ApplicationJob
|
||||
discard_on ActiveJob::DeserializationError
|
||||
|
||||
def perform(notification)
|
||||
NotificationPusher.new(notification).push
|
||||
notification.push
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,7 +31,7 @@ class Account::DataTransfer::AccountRecordSet < Account::DataTransfer::RecordSet
|
||||
account_data = load(files.first)
|
||||
join_code_data = account_data.delete("join_code")
|
||||
|
||||
account.update!(name: account_data.fetch("name"))
|
||||
account.update!(name: account_data.fetch("name"), cards_count: account_data.fetch("cards_count", 0))
|
||||
account.join_code.update!(join_code_data.slice("usage_count", "usage_limit"))
|
||||
account.join_code.update(code: join_code_data.fetch("code"))
|
||||
end
|
||||
|
||||
+54
-11
@@ -1,4 +1,4 @@
|
||||
class Account::DataTransfer::ActionTextRichTextRecordSet < Account::DataTransfer::RecordSet
|
||||
class Account::DataTransfer::ActionText::RichTextRecordSet < Account::DataTransfer::RecordSet
|
||||
ATTRIBUTES = %w[
|
||||
account_id
|
||||
body
|
||||
@@ -11,16 +11,16 @@ class Account::DataTransfer::ActionTextRichTextRecordSet < Account::DataTransfer
|
||||
].freeze
|
||||
|
||||
def initialize(account)
|
||||
super(account: account, model: ActionText::RichText)
|
||||
super(account: account, model: ::ActionText::RichText)
|
||||
end
|
||||
|
||||
private
|
||||
def records
|
||||
ActionText::RichText.where(account: account)
|
||||
::ActionText::RichText.where(account: account)
|
||||
end
|
||||
|
||||
def export_record(rich_text)
|
||||
data = rich_text.as_json.merge("body" => convert_sgids_to_gids(rich_text.body))
|
||||
data = rich_text.as_json.merge("body" => transform_body_for_export(rich_text.body))
|
||||
zip.add_file "data/action_text_rich_texts/#{rich_text.id}.json", data.to_json
|
||||
end
|
||||
|
||||
@@ -31,11 +31,11 @@ class Account::DataTransfer::ActionTextRichTextRecordSet < Account::DataTransfer
|
||||
def import_batch(files)
|
||||
batch_data = files.map do |file|
|
||||
data = load(file)
|
||||
data["body"] = convert_gids_to_sgids(data["body"])
|
||||
data["body"] = transform_body_for_import(data["body"])
|
||||
data.slice(*ATTRIBUTES).merge("account_id" => account.id)
|
||||
end
|
||||
|
||||
ActionText::RichText.insert_all!(batch_data)
|
||||
::ActionText::RichText.insert_all!(batch_data)
|
||||
end
|
||||
|
||||
def check_record(file_path)
|
||||
@@ -54,11 +54,16 @@ class Account::DataTransfer::ActionTextRichTextRecordSet < Account::DataTransfer
|
||||
check_associations_dont_exist(data)
|
||||
end
|
||||
|
||||
def convert_sgids_to_gids(content)
|
||||
def transform_body_for_export(content)
|
||||
return nil if content.blank?
|
||||
|
||||
html = convert_sgids_to_gids(content)
|
||||
relativize_urls(html)
|
||||
end
|
||||
|
||||
def convert_sgids_to_gids(content)
|
||||
content.send(:attachment_nodes).each do |node|
|
||||
sgid = SignedGlobalID.parse(node["sgid"], for: ActionText::Attachable::LOCATOR_NAME)
|
||||
sgid = SignedGlobalID.parse(node["sgid"], for: ::ActionText::Attachable::LOCATOR_NAME)
|
||||
|
||||
record = begin
|
||||
sgid&.find
|
||||
@@ -75,11 +80,35 @@ class Account::DataTransfer::ActionTextRichTextRecordSet < Account::DataTransfer
|
||||
content.fragment.source.to_html
|
||||
end
|
||||
|
||||
def convert_gids_to_sgids(html)
|
||||
return html if html.blank?
|
||||
def relativize_urls(html)
|
||||
host = Rails.application.routes.default_url_options[:host]
|
||||
return html unless host
|
||||
|
||||
fragment = Nokogiri::HTML.fragment(html)
|
||||
|
||||
fragment.css("a[href]").each do |link|
|
||||
uri = URI.parse(link["href"]) rescue nil
|
||||
|
||||
if uri.respond_to?(:host) && uri.host == host
|
||||
link["href"] = uri.path
|
||||
link["href"] += "?#{uri.query}" if uri.query
|
||||
link["href"] += "##{uri.fragment}" if uri.fragment
|
||||
end
|
||||
end
|
||||
|
||||
fragment.to_html
|
||||
end
|
||||
|
||||
def transform_body_for_import(body)
|
||||
return body if body.blank?
|
||||
|
||||
Nokogiri::HTML.fragment(body)
|
||||
.then { convert_gids_to_sgids(it) }
|
||||
.then { replace_account_slugs(it) }
|
||||
.to_html
|
||||
end
|
||||
|
||||
def convert_gids_to_sgids(fragment)
|
||||
fragment.css("action-text-attachment[gid]").each do |node|
|
||||
gid = GlobalID.parse(node["gid"])
|
||||
|
||||
@@ -97,6 +126,20 @@ class Account::DataTransfer::ActionTextRichTextRecordSet < Account::DataTransfer
|
||||
end
|
||||
end
|
||||
|
||||
fragment.to_html
|
||||
fragment
|
||||
end
|
||||
|
||||
def replace_account_slugs(fragment)
|
||||
fragment.css("a[href]").each do |link|
|
||||
match = link["href"].match(AccountSlug::PATH_INFO_MATCH)
|
||||
|
||||
if match
|
||||
path = match.post_match.presence || "/"
|
||||
valid_path = Rails.application.routes.recognize_path(path) rescue nil
|
||||
link["href"] = "#{account.slug}#{path}" if valid_path
|
||||
end
|
||||
end
|
||||
|
||||
fragment
|
||||
end
|
||||
end
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
class Account::DataTransfer::ActiveStorageBlobRecordSet < Account::DataTransfer::RecordSet
|
||||
class Account::DataTransfer::ActiveStorage::BlobRecordSet < Account::DataTransfer::RecordSet
|
||||
def initialize(account)
|
||||
super(
|
||||
account: account,
|
||||
model: ActiveStorage::Blob,
|
||||
attributes: ActiveStorage::Blob.column_names - %w[service_name]
|
||||
model: ::ActiveStorage::Blob,
|
||||
attributes: ::ActiveStorage::Blob.column_names - %w[service_name]
|
||||
)
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ class Account::DataTransfer::ActiveStorageBlobRecordSet < Account::DataTransfer:
|
||||
data = load(file)
|
||||
data.slice(*attributes).merge(
|
||||
"account_id" => account.id,
|
||||
"service_name" => ActiveStorage::Blob.service.name
|
||||
"service_name" => ::ActiveStorage::Blob.service.name
|
||||
)
|
||||
end
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
class Account::DataTransfer::BlobFileRecordSet < Account::DataTransfer::RecordSet
|
||||
class Account::DataTransfer::ActiveStorage::FileRecordSet < Account::DataTransfer::RecordSet
|
||||
def initialize(account)
|
||||
super(account: account, model: ActiveStorage::Blob)
|
||||
super(account: account, model: ::ActiveStorage::Blob)
|
||||
end
|
||||
|
||||
private
|
||||
def records
|
||||
ActiveStorage::Blob.where(account: account)
|
||||
::ActiveStorage::Blob.where(account: account)
|
||||
end
|
||||
|
||||
def export_record(blob)
|
||||
@@ -23,7 +23,7 @@ class Account::DataTransfer::BlobFileRecordSet < Account::DataTransfer::RecordSe
|
||||
def import_batch(files)
|
||||
files.each do |file|
|
||||
key = File.basename(file)
|
||||
blob = ActiveStorage::Blob.find_by(key: key, account: account)
|
||||
blob = ::ActiveStorage::Blob.find_by(key: key, account: account)
|
||||
next unless blob
|
||||
|
||||
zip.read(file) do |stream|
|
||||
@@ -35,7 +35,7 @@ class Account::DataTransfer::BlobFileRecordSet < Account::DataTransfer::RecordSe
|
||||
def check_record(file_path)
|
||||
key = File.basename(file_path)
|
||||
|
||||
unless zip.exists?("data/active_storage_blobs/#{key}.json") || ActiveStorage::Blob.exists?(key: key, account: account)
|
||||
unless zip.exists?("data/active_storage_blobs/#{key}.json") || ::ActiveStorage::Blob.exists?(key: key, account: account)
|
||||
# File exists without corresponding blob record - could be orphaned or blob not yet imported
|
||||
# We allow this since blob metadata is imported before files
|
||||
end
|
||||
@@ -35,10 +35,10 @@ class Account::DataTransfer::Manifest
|
||||
::Filter, ::Webhook::DelinquencyTracker, ::Event,
|
||||
::Notification, ::Notification::Bundle, ::Webhook::Delivery
|
||||
),
|
||||
Account::DataTransfer::ActiveStorageBlobRecordSet.new(account),
|
||||
Account::DataTransfer::ActiveStorage::BlobRecordSet.new(account),
|
||||
*build_record_sets(::ActiveStorage::Attachment),
|
||||
Account::DataTransfer::ActionTextRichTextRecordSet.new(account),
|
||||
Account::DataTransfer::BlobFileRecordSet.new(account)
|
||||
Account::DataTransfer::ActionText::RichTextRecordSet.new(account),
|
||||
Account::DataTransfer::ActiveStorage::FileRecordSet.new(account)
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ class Account::Import < ApplicationRecord
|
||||
end
|
||||
|
||||
add_importer_to_all_access_boards
|
||||
reconcile_cards_count
|
||||
reconcile_account_storage
|
||||
|
||||
mark_completed
|
||||
@@ -78,6 +79,10 @@ class Account::Import < ApplicationRecord
|
||||
ImportMailer.failed(self).deliver_later
|
||||
end
|
||||
|
||||
def reconcile_cards_count
|
||||
account.update_column :cards_count, [ account.cards_count, account.cards.maximum(:number).to_i ].max
|
||||
end
|
||||
|
||||
def add_importer_to_all_access_boards
|
||||
importer = account.users.find_by!(identity: identity)
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ module Mentions
|
||||
rich_text_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ")
|
||||
end
|
||||
|
||||
private
|
||||
def scan_mentionees
|
||||
mentionees_from_attachments & mentionable_users
|
||||
end
|
||||
def scan_mentionees
|
||||
mentionees_from_attachments & mentionable_users
|
||||
end
|
||||
|
||||
private
|
||||
def mentionees_from_attachments
|
||||
rich_text_associations.flat_map { send(it.name)&.body&.attachments&.collect { it.attachable } }.compact
|
||||
end
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
module PushNotifiable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_create_commit :push_notification_later
|
||||
after_update_commit :push_notification_later, if: :source_id_previously_changed?
|
||||
end
|
||||
|
||||
private
|
||||
def push_notification_later
|
||||
PushNotificationJob.perform_later(self)
|
||||
end
|
||||
end
|
||||
@@ -28,6 +28,10 @@ class Identity < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def users_with_active_accounts
|
||||
users.joins(:account).merge(Account.active).includes(:account)
|
||||
end
|
||||
|
||||
private
|
||||
def deactivate_users
|
||||
users.find_each(&:deactivate)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Notification < ApplicationRecord
|
||||
include PushNotifiable
|
||||
include Notification::Pushable
|
||||
|
||||
belongs_to :account, default: -> { user.account }
|
||||
belongs_to :user
|
||||
@@ -10,7 +10,13 @@ class Notification < ApplicationRecord
|
||||
scope :unread, -> { where(read_at: nil) }
|
||||
scope :read, -> { where.not(read_at: nil) }
|
||||
scope :ordered, -> { order(read_at: :desc, updated_at: :desc) }
|
||||
scope :preloaded, -> { preload(:card, :creator, :account, source: [ :board, :creator, { eventable: [ :closure, :board, :assignments ] } ]) }
|
||||
scope :preloaded, -> {
|
||||
preload(
|
||||
:creator, :account,
|
||||
card: [ :board, :column, :closure, :not_now ],
|
||||
source: [ :board, :creator, { eventable: [ :closure, :board, :assignments ] } ]
|
||||
)
|
||||
}
|
||||
|
||||
before_validation :set_card
|
||||
after_create :bundle
|
||||
@@ -21,6 +27,7 @@ class Notification < ApplicationRecord
|
||||
after_destroy_commit -> { broadcast_remove_to user, :notifications }
|
||||
|
||||
delegate :notifiable_target, to: :source
|
||||
delegate :identity, to: :user
|
||||
|
||||
class << self
|
||||
def read_all
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
class Notification::DefaultPayload
|
||||
attr_reader :notification
|
||||
|
||||
delegate :card, to: :notification
|
||||
|
||||
def initialize(notification)
|
||||
@notification = notification
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ title: title, body: body, url: url }
|
||||
end
|
||||
|
||||
def title
|
||||
"New notification"
|
||||
end
|
||||
|
||||
def body
|
||||
"You have a new notification"
|
||||
end
|
||||
|
||||
def url
|
||||
notifications_url
|
||||
end
|
||||
|
||||
def category
|
||||
"default"
|
||||
end
|
||||
|
||||
def high_priority?
|
||||
false
|
||||
end
|
||||
|
||||
def base_url
|
||||
Rails.application.routes.url_helpers.root_url(**url_options.except(:script_name)).chomp("/")
|
||||
end
|
||||
|
||||
def avatar_url
|
||||
Rails.application.routes.url_helpers.user_avatar_url(notification.creator, **url_options)
|
||||
end
|
||||
|
||||
private
|
||||
def card_url(card)
|
||||
Rails.application.routes.url_helpers.card_url(card, **url_options)
|
||||
end
|
||||
|
||||
def notifications_url
|
||||
Rails.application.routes.url_helpers.notifications_url(**url_options)
|
||||
end
|
||||
|
||||
def url_options
|
||||
base_options = Rails.application.routes.default_url_options.presence ||
|
||||
Rails.application.config.action_mailer.default_url_options ||
|
||||
{}
|
||||
base_options.merge(script_name: notification.account.slug)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,107 @@
|
||||
class Notification::EventPayload < Notification::DefaultPayload
|
||||
include ExcerptHelper
|
||||
|
||||
def title
|
||||
case event.action
|
||||
when "comment_created"
|
||||
"RE: #{card_title}"
|
||||
else
|
||||
card_title
|
||||
end
|
||||
end
|
||||
|
||||
def body
|
||||
case event.action
|
||||
when "comment_created"
|
||||
format_excerpt(event.eventable.body, length: 200)
|
||||
when "card_assigned"
|
||||
"Assigned to you by #{event.creator.name}"
|
||||
when "card_published"
|
||||
"Added by #{event.creator.name}"
|
||||
when "card_closed"
|
||||
card.closure ? "Moved to Done by #{event.creator.name}" : "Closed by #{event.creator.name}"
|
||||
when "card_reopened"
|
||||
"Reopened by #{event.creator.name}"
|
||||
when "card_triaged"
|
||||
if column_name.present?
|
||||
"Moved to #{column_name} by #{event.creator.name}"
|
||||
else
|
||||
"Moved by #{event.creator.name}"
|
||||
end
|
||||
when "card_sent_back_to_triage"
|
||||
"Moved back to Maybe? by #{event.creator.name}"
|
||||
when "card_board_changed", "card_collection_changed"
|
||||
if new_location_name.present?
|
||||
"Moved to #{new_location_name} by #{event.creator.name}"
|
||||
else
|
||||
"Moved by #{event.creator.name}"
|
||||
end
|
||||
when "card_title_changed"
|
||||
if new_title.present?
|
||||
"Renamed to #{new_title} by #{event.creator.name}"
|
||||
else
|
||||
"Renamed by #{event.creator.name}"
|
||||
end
|
||||
when "card_postponed"
|
||||
"Moved to Not Now by #{event.creator.name}"
|
||||
when "card_auto_postponed"
|
||||
"Moved to Not Now due to inactivity"
|
||||
else
|
||||
"Updated by #{event.creator.name}"
|
||||
end
|
||||
end
|
||||
|
||||
def url
|
||||
case event.action
|
||||
when "comment_created"
|
||||
card_url_with_comment_anchor(event.eventable)
|
||||
else
|
||||
card_url(card)
|
||||
end
|
||||
end
|
||||
|
||||
def category
|
||||
case event.action
|
||||
when "card_assigned" then "assignment"
|
||||
when "comment_created" then "comment"
|
||||
else "card"
|
||||
end
|
||||
end
|
||||
|
||||
def high_priority?
|
||||
event.action.card_assigned?
|
||||
end
|
||||
|
||||
private
|
||||
def event
|
||||
notification.source
|
||||
end
|
||||
|
||||
def card_title
|
||||
card.title.presence || "Card #{card.number}"
|
||||
end
|
||||
|
||||
def event_particulars
|
||||
event.particulars.dig("particulars") || {}
|
||||
end
|
||||
|
||||
def column_name
|
||||
event_particulars["column"]
|
||||
end
|
||||
|
||||
def new_location_name
|
||||
event_particulars["new_board"] || event_particulars["new_collection"]
|
||||
end
|
||||
|
||||
def new_title
|
||||
event_particulars["new_title"]
|
||||
end
|
||||
|
||||
def card_url_with_comment_anchor(comment)
|
||||
Rails.application.routes.url_helpers.card_url(
|
||||
comment.card,
|
||||
anchor: ActionView::RecordIdentifier.dom_id(comment),
|
||||
**url_options
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
class Notification::MentionPayload < Notification::DefaultPayload
|
||||
include ExcerptHelper
|
||||
|
||||
def title
|
||||
"#{mention.mentioner.first_name} mentioned you"
|
||||
end
|
||||
|
||||
def body
|
||||
format_excerpt(mention.source.mentionable_content, length: 200)
|
||||
end
|
||||
|
||||
def url
|
||||
card_url(card)
|
||||
end
|
||||
|
||||
def category
|
||||
"mention"
|
||||
end
|
||||
|
||||
def high_priority?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
def mention
|
||||
notification.source
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
class Notification::PushTarget
|
||||
attr_reader :notification
|
||||
|
||||
delegate :card, to: :notification
|
||||
|
||||
def self.process(notification)
|
||||
new(notification).process
|
||||
end
|
||||
|
||||
def initialize(notification)
|
||||
@notification = notification
|
||||
end
|
||||
|
||||
def process
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
class Notification::PushTarget::Web < Notification::PushTarget
|
||||
def process
|
||||
if subscriptions.any?
|
||||
Rails.configuration.x.web_push_pool.queue(notification.payload.to_h, subscriptions)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def subscriptions
|
||||
@subscriptions ||= notification.user.push_subscriptions
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
module Notification::Pushable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
class_attribute :push_targets, default: []
|
||||
|
||||
after_save_commit :push_later, if: :source_id_previously_changed?
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def register_push_target(target)
|
||||
target = resolve_push_target(target)
|
||||
push_targets << target unless push_targets.include?(target)
|
||||
end
|
||||
|
||||
private
|
||||
def resolve_push_target(target)
|
||||
if target.is_a?(Symbol)
|
||||
"Notification::PushTarget::#{target.to_s.classify}".constantize
|
||||
else
|
||||
target
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def push_later
|
||||
Notification::PushJob.perform_later(self)
|
||||
end
|
||||
|
||||
def push
|
||||
return unless pushable?
|
||||
|
||||
self.class.push_targets.each { |target| push_to(target) }
|
||||
end
|
||||
|
||||
def payload
|
||||
"Notification::#{payload_type}Payload".constantize.new(self)
|
||||
end
|
||||
|
||||
private
|
||||
def pushable?
|
||||
!creator.system? && user.active? && account.active?
|
||||
end
|
||||
|
||||
def push_to(target)
|
||||
target.process(self)
|
||||
end
|
||||
|
||||
def payload_type
|
||||
source_type.presence_in(%w[ Event Mention ]) || "Default"
|
||||
end
|
||||
end
|
||||
@@ -1,124 +0,0 @@
|
||||
class NotificationPusher
|
||||
include Rails.application.routes.url_helpers
|
||||
include ExcerptHelper
|
||||
|
||||
attr_reader :notification
|
||||
|
||||
def initialize(notification)
|
||||
@notification = notification
|
||||
end
|
||||
|
||||
def push
|
||||
return unless should_push?
|
||||
|
||||
build_payload.tap do |payload|
|
||||
push_to_user(payload)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def should_push?
|
||||
notification.user.push_subscriptions.any? &&
|
||||
!notification.creator.system? &&
|
||||
notification.user.active? &&
|
||||
notification.account.active?
|
||||
end
|
||||
|
||||
def build_payload
|
||||
case notification.source_type
|
||||
when "Event"
|
||||
build_event_payload
|
||||
when "Mention"
|
||||
build_mention_payload
|
||||
else
|
||||
build_default_payload
|
||||
end
|
||||
end
|
||||
|
||||
def build_event_payload
|
||||
event = notification.source
|
||||
card = event.card
|
||||
|
||||
base_payload = {
|
||||
title: card_notification_title(card),
|
||||
path: card_path(card)
|
||||
}
|
||||
|
||||
case event.action
|
||||
when "comment_created"
|
||||
base_payload.merge(
|
||||
title: "RE: #{base_payload[:title]}",
|
||||
body: comment_notification_body(event),
|
||||
path: card_path_with_comment_anchor(event.eventable)
|
||||
)
|
||||
when "card_assigned"
|
||||
base_payload.merge(
|
||||
body: "Assigned to you by #{event.creator.name}"
|
||||
)
|
||||
when "card_published"
|
||||
base_payload.merge(
|
||||
body: "Added by #{event.creator.name}"
|
||||
)
|
||||
when "card_closed"
|
||||
base_payload.merge(
|
||||
body: card.closure ? "Moved to Done by #{event.creator.name}" : "Closed by #{event.creator.name}"
|
||||
)
|
||||
when "card_reopened"
|
||||
base_payload.merge(
|
||||
body: "Reopened by #{event.creator.name}"
|
||||
)
|
||||
else
|
||||
base_payload.merge(
|
||||
body: event.creator.name
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def build_mention_payload
|
||||
mention = notification.source
|
||||
card = mention.card
|
||||
|
||||
{
|
||||
title: "#{mention.mentioner.first_name} mentioned you",
|
||||
body: format_excerpt(mention.source.mentionable_content, length: 200),
|
||||
path: card_path(card)
|
||||
}
|
||||
end
|
||||
|
||||
def build_default_payload
|
||||
{
|
||||
title: "New notification",
|
||||
body: "You have a new notification",
|
||||
path: notifications_path(script_name: notification.account.slug)
|
||||
}
|
||||
end
|
||||
|
||||
def push_to_user(payload)
|
||||
subscriptions = notification.user.push_subscriptions
|
||||
enqueue_payload_for_delivery(payload, subscriptions)
|
||||
end
|
||||
|
||||
def enqueue_payload_for_delivery(payload, subscriptions)
|
||||
Rails.configuration.x.web_push_pool.queue(payload, subscriptions)
|
||||
end
|
||||
|
||||
def card_notification_title(card)
|
||||
card.title.presence || "Card #{card.number}"
|
||||
end
|
||||
|
||||
def comment_notification_body(event)
|
||||
format_excerpt(event.eventable.body, length: 200)
|
||||
end
|
||||
|
||||
def card_path(card)
|
||||
Rails.application.routes.url_helpers.card_path(card, script_name: notification.account.slug)
|
||||
end
|
||||
|
||||
def card_path_with_comment_anchor(comment)
|
||||
Rails.application.routes.url_helpers.card_path(
|
||||
comment.card,
|
||||
anchor: ActionView::RecordIdentifier.dom_id(comment),
|
||||
script_name: notification.account.slug
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -8,9 +8,9 @@ class Notifier::CardEventNotifier < Notifier
|
||||
when "card_assigned"
|
||||
source.assignees.excluding(creator)
|
||||
when "card_published"
|
||||
board.watchers.without(creator, *card.mentionees).including(*card.assignees).uniq
|
||||
board.watchers.without(creator, *card.scan_mentionees).including(*card.assignees).uniq
|
||||
when "comment_created"
|
||||
card.watchers.without(creator, *source.eventable.mentionees)
|
||||
card.watchers.without(creator, *source.eventable.scan_mentionees)
|
||||
else
|
||||
board.watchers.without(creator)
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@ class Notifier::CommentEventNotifier < Notifier
|
||||
|
||||
private
|
||||
def recipients
|
||||
card.watchers.without(creator, *source.eventable.mentionees)
|
||||
card.watchers.without(creator, *source.eventable.scan_mentionees)
|
||||
end
|
||||
|
||||
def card
|
||||
|
||||
@@ -5,7 +5,7 @@ module Search::Stemmer
|
||||
|
||||
def stem(value)
|
||||
if value.present?
|
||||
value.gsub(/[^\w\s]/, "").split(/\s+/).map { |word| STEMMER.stem(word.downcase) }.join(" ")
|
||||
value.gsub(/[^\w\s]/, " ").split(/\s+/).map { |word| STEMMER.stem(word.downcase) }.join(" ")
|
||||
else
|
||||
value
|
||||
end
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
require "zlib"
|
||||
|
||||
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
|
||||
AVATAR_COLORS = %w[
|
||||
#AF2E1B #CC6324 #3B4B59 #BFA07A #ED8008 #ED3F1C #BF1B1B #736B1E #D07B53
|
||||
#736356 #AD1D1D #BF7C2A #C09C6F #698F9C #7C956B #5D618F #3B3633 #67695E
|
||||
].freeze
|
||||
|
||||
included do
|
||||
has_one_attached :avatar do |attachable|
|
||||
@@ -22,6 +28,10 @@ module User::Avatar
|
||||
avatar.variable? ? avatar.variant(:thumb) : avatar
|
||||
end
|
||||
|
||||
def avatar_background_color
|
||||
AVATAR_COLORS[Zlib.crc32(to_param) % AVATAR_COLORS.size]
|
||||
end
|
||||
|
||||
# Avatars are always publicly accessible
|
||||
def publicly_accessible?
|
||||
true
|
||||
|
||||
@@ -36,6 +36,7 @@ class Webhook < ApplicationRecord
|
||||
after_create :create_delinquency_tracker!
|
||||
|
||||
normalizes :subscribed_actions, with: ->(value) { Array.wrap(value).map(&:to_s).uniq & PERMITTED_ACTIONS }
|
||||
normalizes :url, with: -> { it.strip }
|
||||
|
||||
validates :name, presence: true
|
||||
validate :validate_url
|
||||
|
||||
@@ -2,7 +2,7 @@ class ZipFile::Reader
|
||||
def initialize(io)
|
||||
@io = io
|
||||
@reader = ZipKit::FileReader.read_zip_structure(io: io)
|
||||
rescue ZipKit::FileReader::InvalidStructure => e
|
||||
rescue ZipKit::FileReader::ReadError, ZipKit::FileReader::MissingEOCD, ZipKit::FileReader::UnsupportedFeature => e
|
||||
raise ZipFile::InvalidFileError, e.message
|
||||
end
|
||||
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
<% if Current.account.cancellable? && Current.user.owner? %>
|
||||
<header class="margin-block-start-double">
|
||||
<h2 class="divider txt-large txt-negative">Cancel account</h2>
|
||||
<p class="margin-none-block">Delete your Fizzy account</p>
|
||||
</header>
|
||||
<section class="settings__section">
|
||||
<header>
|
||||
<h2 class="divider txt-negative">Cancel account</h2>
|
||||
<div>Delete your Fizzy account.</div>
|
||||
</header>
|
||||
|
||||
<div data-controller="dialog" data-dialog-modal-value="true">
|
||||
<button type="button" class="btn btn--negative" data-action="dialog#open">Delete account</button>
|
||||
<div data-controller="dialog" data-dialog-modal-value="true">
|
||||
<button type="button" class="btn btn--negative" data-action="dialog#open">Delete account</button>
|
||||
|
||||
<dialog class="dialog panel panel--wide shadow" data-dialog-target="dialog">
|
||||
<h2 class="margin-none txt-large txt-negative">Delete your account?</h2>
|
||||
<ul class="txt-align-start">
|
||||
<li>All users, including you, will lose access</li>
|
||||
<% if Current.account.try(:active_subscription) %>
|
||||
<li>Your subscription will be canceled</li>
|
||||
<% end %>
|
||||
<li>After 30 days your data will be permanently deleted</li>
|
||||
</ul>
|
||||
<dialog class="dialog panel panel--wide shadow" data-dialog-target="dialog" style="--panel-size: 48ch;">
|
||||
<h2 class="margin-none txt-large txt-negative">Delete your account?</h2>
|
||||
<ul class="txt-align-start">
|
||||
<li>All users, including you, will lose access</li>
|
||||
<% if Current.account.try(:active_subscription) %>
|
||||
<li>Your subscription will be canceled</li>
|
||||
<% end %>
|
||||
<li>After 30 days, your data will be permanently deleted</li>
|
||||
</ul>
|
||||
|
||||
<div class="flex gap justify-center">
|
||||
<button type="button" class="btn" data-action="dialog#close">Cancel</button>
|
||||
<%= button_to "Delete my account", account_cancellation_path, method: :post, class: "btn btn--negative", form: { data: { action: "submit->dialog#close", turbo: false } } %>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
<div class="flex gap justify-center">
|
||||
<button type="button" class="btn" data-action="dialog#close">Cancel</button>
|
||||
<%= button_to "Delete my account", account_cancellation_path, method: :post, class: "btn btn--negative", form: { data: { action: "submit->dialog#close", turbo: false } } %>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<section class="settings__section settings__section--users">
|
||||
<section class="settings__section">
|
||||
<header>
|
||||
<h2 class="divider">People on this account</h2>
|
||||
</header>
|
||||
@@ -13,7 +13,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" role="listbox">
|
||||
<ul class="settings__scrollable-list margin-block-half" data-filter-target="list" role="listbox">
|
||||
<%= render partial: "account/settings/user", collection: users %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
<section class="settings">
|
||||
<div class="settings__panel settings__panel--users panel shadow">
|
||||
<%= form_with model: @board, class: "display-contents", data: {
|
||||
controller: "form boards-form",
|
||||
controller: "form boards-form bridge--form",
|
||||
boards_form_self_removal_prompt_message_value: "Are you sure you want to remove yourself from this board? You won’t be able to get back in unless someone invites you.",
|
||||
action: "turbo:submit-start->boards-form#submitWithWarning" } do |form| %>
|
||||
<%= render "boards/edit/name", form: form, board: @board %>
|
||||
<%= render "boards/edit/users", board: @board, selected_users: @selected_users, unselected_users: @unselected_users, form: form %>
|
||||
|
||||
<button type="submit" id="log_in" class="btn btn--link center txt-normal" <%= "disabled" unless Current.user.can_administer_board?(@board) %>>
|
||||
<button type="submit" id="log_in" class="btn btn--link center txt-normal" data-bridge--form-target="submit" <%= "disabled" unless Current.user.can_administer_board?(@board) %>>
|
||||
<span>Save changes</span>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="settings__user-list" data-filter-target="list">
|
||||
<ul class="settings__scrollable-list" data-filter-target="list">
|
||||
<%= access_toggles_for selected_users, selected: true, disabled: %>
|
||||
<%= access_toggles_for unselected_users, selected: false, disabled: %>
|
||||
</ul>
|
||||
|
||||
@@ -30,3 +30,5 @@
|
||||
<%= render "boards/show/columns", page: @page, board: @board %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= bridged_share_url_button(bridge_share_board_description(@board)) %>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
navigable_list_auto_select_value: false,
|
||||
navigable_list_auto_scroll_value: false,
|
||||
card_hotkeys_navigable_list_outlet: ".cards__transition-container",
|
||||
native_prevent_pull_to_refresh: true,
|
||||
action: "
|
||||
keydown->navigable-list#navigate
|
||||
keydown->card-hotkeys#handleKeydown
|
||||
|
||||
@@ -8,6 +8,7 @@ json.cache! card do
|
||||
json.tags card.tags.pluck(:title).sort
|
||||
|
||||
json.closed card.closed?
|
||||
json.postponed card.postponed?
|
||||
json.golden card.golden?
|
||||
json.last_active_at card.last_active_at.utc
|
||||
json.created_at card.created_at.utc
|
||||
|
||||
@@ -24,13 +24,16 @@
|
||||
<%= button_to_set_column @card, column %>
|
||||
<% end %>
|
||||
|
||||
<%= button_to "Done", card_closure_path(@card),
|
||||
<%= button_to 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 },
|
||||
form_class: "flex gap-half" %>
|
||||
form_class: "flex gap-half" do %>
|
||||
<%= icon_tag "check", class: "icon--mobile-only" %>
|
||||
Done
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -13,9 +13,9 @@ json.cache! comment do
|
||||
|
||||
json.card do
|
||||
json.id comment.card_id
|
||||
json.url card_url(comment.card_id)
|
||||
json.url card_url(comment.card)
|
||||
end
|
||||
|
||||
json.reactions_url card_comment_reactions_url(comment.card_id, comment.id)
|
||||
json.url card_comment_url(comment.card_id, comment.id)
|
||||
json.reactions_url card_comment_reactions_url(comment.card, comment)
|
||||
json.url card_comment_url(comment.card, comment)
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</figure>
|
||||
|
||||
<div class="comment__content flex-inline flex-column full-width">
|
||||
<div class="comment__body lexxy-content" data-turbo-permanent>
|
||||
<div class="comment__body" data-turbo-permanent>
|
||||
<%= form_with model: Comment.new, url: card_comments_path(card), class: "flex flex-column gap full-width",
|
||||
data: { controller: "form local-save",
|
||||
local_save_key_value: "comment-#{card.id}",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</figure>
|
||||
|
||||
<div class="comment__content flex flex-column flex-item-grow full-width">
|
||||
<div class="comment__body lexxy-content" data-turbo-permanent>
|
||||
<div class="comment__body" data-turbo-permanent>
|
||||
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width",
|
||||
data: { controller: "form", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %>
|
||||
<%= form.rich_textarea :body, required: true, autofocus: true, placeholder: new_comment_placeholder(@card) do %>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<%= button_to card_closure_path(card), method: :delete, class: "btn btn--plain borderless fill-transparent" do %>
|
||||
<span class="pad-inline-end">Undo</span>
|
||||
<% end %>
|
||||
<%= bridged_button_to_board(card.board) %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="card-perma__notch card-perma__notch--bottom">
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
<% end %>
|
||||
|
||||
<%= button_to card_closure_path(card), class: "btn btn--circle-mobile borderless",
|
||||
data: { controller: "hotkey", form_target: "submit", bridge__buttons_target: "button", bridge_title: "Mark as Done", bridge_icon_url: bridge_icon("check"), action: "keydown.d@document->hotkey#click" },
|
||||
data: { controller: "hotkey", form_target: "submit", bridge__buttons_target: "button", bridge_title: "Mark done", bridge_display_as_primary_action: true, bridge_display_title: true, bridge_icon_url: bridge_icon("check"), action: "keydown.d@document->hotkey#click" },
|
||||
form: { data: { controller: "form" } } do %>
|
||||
<%= icon_tag "check", class: "icon--mobile-only" %>
|
||||
<span class="overflow-ellipsis">Mark as Done</span>
|
||||
<kbd class="txt-x-small hide-on-touch">d</kbd>
|
||||
<% end %>
|
||||
|
||||
<%= bridged_button_to_board(card.board) %>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<% if card.image.attached? %>
|
||||
<%= button_to card_image_path(card), method: :delete, class: "btn",
|
||||
data: { controller: "tooltip", bridge__overflow_menu_target: "item", bridge_title: "Remove background image" } do %>
|
||||
<%= button_to card_image_path(card), method: :delete, class: "btn hide-on-native",
|
||||
data: { controller: "tooltip" } do %>
|
||||
<%= icon_tag "picture-remove" %>
|
||||
<span class="for-screen-reader">Remove background image</span>
|
||||
<% end %>
|
||||
<% elsif !card.closed? %>
|
||||
<%= form_with model: card, data: { controller: "form" } do |form| %>
|
||||
<label class="card-perma__image-btn btn input--file btn--circle-mobile" data-controller="tooltip" data-bridge--overflow-menu-target="item" data-bridge-title="Add background image">
|
||||
<label class="card-perma__image-btn btn input--file btn--circle-mobile hide-on-native" data-controller="tooltip">
|
||||
<%= icon_tag "picture-add" %>
|
||||
<span class="for-screen-reader">Add background image</span>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div id="<%= dom_id(card, :tags) %>" class="card__tags">
|
||||
<div data-controller="dialog"
|
||||
data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside mouseenter->dialog#loadLazyFrames" <%= "hidden" if card.closed? %>>
|
||||
<button class="card__tag-picker-button btn card__hide-on-index" style="--btn-background: var(--card-bg-color);"
|
||||
<button class="card__tag-picker-button btn btn--ensure-tap-target-size card__hide-on-index" style="--btn-background: var(--card-bg-color);"
|
||||
data-controller="tooltip hotkey" data-action="click->dialog#open:stop keydown.t@document->hotkey#click">
|
||||
<%= icon_tag "tag" %>
|
||||
<span class="for-screen-reader">Add a tag</span>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div id="<%= dom_id(card, :pin_button) %>">
|
||||
<% if card.pinned_by? Current.user %>
|
||||
<%= button_to card_pin_path(card), method: :delete, class: "btn btn--reversed btn--circle-mobile",
|
||||
data: { controller: "tooltip hotkey", action: "keydown.shift+p@document->hotkey#click", bridge__overflow_menu_target: "item", bridge_title: "Un-pin this card" } do %>
|
||||
<%= icon_tag "pinned" %> <span class="for-screen-reader">Un-pin this card (shift+p)</span>
|
||||
data: { controller: "tooltip hotkey", action: "keydown.shift+p@document->hotkey#click", bridge__overflow_menu_target: "item", bridge_title: "Unpin this card" } do %>
|
||||
<%= icon_tag "pinned" %> <span class="for-screen-reader">Unpin this card (shift+p)</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= button_to card_pin_path(card), class: "btn btn--circle-mobile",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<div class="header__actions header__actions--start">
|
||||
<%= link_back_to_board(@card.board) %>
|
||||
</div>
|
||||
<%= bridged_button_to_board(@card.board) %>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_stream_from @card %>
|
||||
@@ -23,3 +22,5 @@
|
||||
<%= button_to_remove_card_image(@card) if @card.image.attached? %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= bridged_share_url_button(bridge_share_card_description(@card)) %>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"settings": {},
|
||||
"rules": [
|
||||
{
|
||||
"patterns": [
|
||||
".*"
|
||||
],
|
||||
"properties": {
|
||||
"context": "default",
|
||||
"presentation": "default",
|
||||
"query_string_presentation": "replace",
|
||||
"uri": "hotwire://fragment/web",
|
||||
"fallback_uri": "hotwire://fragment/web",
|
||||
"pull_to_refresh_enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/new$",
|
||||
"/new\\?.+$",
|
||||
"/edit$",
|
||||
"/edit\\?.+$",
|
||||
"/cards/[0-9]+/draft",
|
||||
"/notifications/settings",
|
||||
"/account/settings",
|
||||
"/account/join_code"
|
||||
],
|
||||
"properties": {
|
||||
"context": "modal",
|
||||
"pull_to_refresh_enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/devices$"
|
||||
],
|
||||
"properties": {
|
||||
"context": "modal",
|
||||
"pull_to_refresh_enabled": false,
|
||||
"allow_untenanted_navigation": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/native/login/blank"
|
||||
],
|
||||
"properties": {
|
||||
"uri": "hotwire://fragment/login/blank"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/native/login/email"
|
||||
],
|
||||
"properties": {
|
||||
"uri": "hotwire://fragment/login/email"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/native/login/magic_code"
|
||||
],
|
||||
"properties": {
|
||||
"uri": "hotwire://fragment/login/magic_code"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/native/login/signup_completion"
|
||||
],
|
||||
"properties": {
|
||||
"uri": "hotwire://fragment/login/signup_completion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/native/settings"
|
||||
],
|
||||
"properties": {
|
||||
"uri": "hotwire://fragment/settings"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/my/pins"
|
||||
],
|
||||
"properties": {
|
||||
"uri": "hotwire://fragment/pins"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/notifications$"
|
||||
],
|
||||
"properties": {
|
||||
"uri": "hotwire://fragment/notifications"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"settings": {
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"patterns": [
|
||||
".*"
|
||||
],
|
||||
"properties": {
|
||||
"context": "default",
|
||||
"query_string_presentation": "replace",
|
||||
"pull_to_refresh_enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/new$",
|
||||
"/new\\?.+$",
|
||||
"/edit$",
|
||||
"/edit\\?.+$",
|
||||
"/accounts$",
|
||||
"/cards/[0-9]+/draft"
|
||||
],
|
||||
"properties": {
|
||||
"context": "modal",
|
||||
"pull_to_refresh_enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/native/my_menu$"
|
||||
],
|
||||
"properties": {
|
||||
"context": "modal",
|
||||
"view_controller": "main_menu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/native/add_account$"
|
||||
],
|
||||
"properties": {
|
||||
"context": "modal",
|
||||
"view_controller": "login"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/native/login$"
|
||||
],
|
||||
"properties": {
|
||||
"view_controller": "login"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/my/pins$"
|
||||
],
|
||||
"properties": {
|
||||
"view_controller": "pinned"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/notifications/tray$"
|
||||
],
|
||||
"properties": {
|
||||
"view_controller": "notifications"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"internal/devtools/enable*"
|
||||
],
|
||||
"properties": {
|
||||
"context": "modal",
|
||||
"view_controller": "dev_settings_launcher"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patterns": [
|
||||
"/native/settings$"
|
||||
],
|
||||
"properties": {
|
||||
"view_controller": "settings"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="board-tools card">
|
||||
<%= button_to board_cards_path(board), method: :post, class: "btn btn--link",
|
||||
form: { data: { turbo_frame: "_top" } },
|
||||
data: { controller: "hotkey", action: "keydown.c@document->hotkey#click", bridge__buttons_target: "button", bridge_title: "Add card", bridge_icon_url: bridge_icon("add") } do %>
|
||||
data: { controller: "hotkey", action: "keydown.c@document->hotkey#click", bridge__buttons_target: "button", bridge_title: "Add card", bridge_display_as_primary_action: true, bridge_display_title: true, bridge_icon_url: bridge_icon("add") } do %>
|
||||
<%= icon_tag "add", class: "show-on-touch" %>
|
||||
<span>Add a card</span>
|
||||
<kbd class="hide-on-touch">C</kbd>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="header__actions header__actions--end">
|
||||
<div>
|
||||
<%= link_to new_board_path, class: "btn btn--link btn--circle-mobile",
|
||||
data: { controller: "hotkey", action: "keydown.b@document->hotkey#click", bridge__buttons_target: "button", bridge_title: "Add a board", bridge_icon_url: bridge_icon("board") } do %>
|
||||
data: { controller: "hotkey", action: "keydown.b@document->hotkey#click", bridge__buttons_target: "button", bridge_title: "Add board", bridge_display_title: true, bridge_icon_url: bridge_icon("board") } do %>
|
||||
<%= icon_tag "board" %>
|
||||
<span class="overflow-ellipsis">Add a board</span>
|
||||
<kbd class="hide-on-touch">B</kbd>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="header__actions header__actions--start">
|
||||
<% if board = user_filtering.single_board_or_first %>
|
||||
<%= button_to board_cards_path(board), method: :post, class: "btn btn--link btn--circle-mobile",
|
||||
data: { controller: "hotkey", action: "keydown.c@document->hotkey#click", bridge__buttons_target: "button", bridge_title: "Add a card", bridge_icon_url: bridge_icon("add") } do %>
|
||||
data: { controller: "hotkey", action: "keydown.c@document->hotkey#click", bridge__buttons_target: "button", bridge_title: "Add card", bridge_display_as_primary_action: true, bridge_display_title: true, bridge_icon_url: bridge_icon("add") } do %>
|
||||
<%= icon_tag "add" %>
|
||||
<span class="overflow-ellipsis">Add a card</span>
|
||||
<kbd class="hide-on-touch">C</kbd>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<%= render "layouts/shared/head" %>
|
||||
|
||||
<body class="public <%= @body_class %>" data-controller="local-time timezone-cookie" data-action="turbo:morph@window->local-time#refreshAll">
|
||||
<body class="public <%= @body_class %>" data-controller="local-time timezone-cookie bridge--title bridge--text-size bridge--insets" data-action="turbo:morph@window->local-time#refreshAll" data-platform="<%= platform.type %>" data-bridge--title-title-value="<%= @page_title %>">
|
||||
<div id="global-container">
|
||||
<header class="header" id="header">
|
||||
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
json.accounts @identity.users do |user|
|
||||
json.id @identity.id
|
||||
|
||||
json.accounts @identity.users_with_active_accounts do |user|
|
||||
json.partial! "my/identities/account", account: user.account
|
||||
json.user user, partial: "users/user", as: :user
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="flex gap">
|
||||
<div class="flex flex-column full-width justify-center gap-half">
|
||||
<div class="nav__header">
|
||||
<div class="nav__header" tabindex="-1" data-dialog-target="focusTouch">
|
||||
<% if Current.identity.accounts.many? %>
|
||||
<div class="nav__header-title max-width">
|
||||
<div class="overflow-ellipsis"><strong><%= Current.account.name %></strong></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="tray__item tray__item--pin" id="<%= dom_id pin %>" data-navigable-list-target="item">
|
||||
<%= render "cards/display/preview", card: pin.card %>
|
||||
<%= button_to card_pin_path(pin.card), method: :delete, class: "tray__remove-pin-btn btn btn--circle borderless" do %>
|
||||
<%= icon_tag "pinned" %> <span class="for-screen-reader">Un-pin this card</span>
|
||||
<%= icon_tag "pinned" %> <span class="for-screen-reader">Unpin this card</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,19 @@ json.cache! notification do
|
||||
json.read notification.read?
|
||||
json.read_at notification.read_at&.utc
|
||||
json.created_at notification.created_at.utc
|
||||
json.source_type notification.source_type.underscore
|
||||
|
||||
json.partial! "notifications/notification/#{notification.source_type.underscore}/body", notification: notification
|
||||
|
||||
json.creator notification.creator, partial: "users/user", as: :user
|
||||
|
||||
json.card do
|
||||
json.(notification.card, :id, :title, :status)
|
||||
json.(notification.card, :id, :number, :title, :status)
|
||||
json.board_name notification.card.board.name
|
||||
json.closed notification.card.closed?
|
||||
json.postponed notification.card.postponed?
|
||||
json.url card_url(notification.card)
|
||||
json.column notification.card.column, partial: "columns/column", as: :column if notification.card.column
|
||||
end
|
||||
|
||||
json.url notification_url(notification)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="settings__panel settings__panel--users panel shadow">
|
||||
<section class="settings__section">
|
||||
<h2 class="divider">Boards</h2>
|
||||
<div class="settings__user-list flex flex-column gap-half">
|
||||
<div class="settings__scrollable-list flex flex-column gap-half">
|
||||
<%= render partial: "notifications/settings/board", collection: @boards, locals: { user: Current.user } %>
|
||||
</div>
|
||||
</section>
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
<div class="settings__panel panel shadow">
|
||||
<%= render "notifications/settings/push_notifications" %>
|
||||
<%= render "notifications/settings/native_devices" if Fizzy.saas? %>
|
||||
<%= render "notifications/settings/email", settings: @settings %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @notifications, partial: "notifications/notification", as: :notification
|
||||
@@ -25,7 +25,7 @@ async function updateBadgeCount({ data: { badge } }) {
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
event.notification.close()
|
||||
|
||||
const url = new URL(event.notification.data.path, self.location.origin).href
|
||||
const url = new URL(event.notification.data.url, self.location.origin).href
|
||||
event.waitUntil(openURL(url))
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<%= form_with url: search_path, method: :get, class: "search__form flex align-center justify-center gap-half", data: { bar_target: "form", turbo_frame: defined?(target_turbo_frame) ? target_turbo_frame : nil } do |form| %>
|
||||
<%= form_with url: search_path, method: :get,
|
||||
class: "search__form flex align-center justify-center gap-half",
|
||||
data: {
|
||||
controller: "search-form",
|
||||
action: "search-form:reset->bar#reset",
|
||||
bar_target: "form",
|
||||
turbo_frame: defined?(target_turbo_frame) ? target_turbo_frame : nil } do |form| %>
|
||||
<%= form.label :q, "Search Fizzy", class: "font-weight-black txt-nowrap" %>
|
||||
<%= text_field_tag :q, query_terms,
|
||||
class: "search__input input",
|
||||
@@ -7,9 +13,11 @@
|
||||
autocomplete: "off",
|
||||
autofocus: true,
|
||||
data: {
|
||||
search_form_target: "searchInput",
|
||||
bar_target: "searchInput",
|
||||
action: "keydown.enter->bar#showModalAndSubmit:prevent keydown.esc->bar#reset" } %>
|
||||
<button class="search__reset btn btn--circle borderless" data-action="bar#clearInput">
|
||||
<button hidden>Search</button>
|
||||
<button class="search__reset btn btn--circle borderless" data-action="search-form#clearInput">
|
||||
<%= icon_tag "close" %>
|
||||
<span class="for-screen-reader">Close search</span>
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<section class="search">
|
||||
<div class="search__results">
|
||||
<% unless page.used? %>
|
||||
<% if !page.used? && query %>
|
||||
<div class="search__blank-slate blank-slate">
|
||||
No matches
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<% @page_title = params.has_key?(:q) ? "Search results for \"#{params[:q]}\"" : "Search" %>
|
||||
<% @page_title = @query ? "Search results for \"#{@query}\"" : "Search" %>
|
||||
|
||||
<% content_for :header do %>
|
||||
<div class="header__actions header__actions--start">
|
||||
@@ -9,12 +9,12 @@
|
||||
<% end %>
|
||||
|
||||
<div class="search-perma margin-block-start">
|
||||
<%= render "form", query_terms: params[:q] %>
|
||||
<%= render "form", query_terms: @query %>
|
||||
<%= turbo_frame_tag "bar_content" do %>
|
||||
<% if @card %>
|
||||
<%= auto_submit_form_with url: card_path(@card), method: :get, data: { turbo_action: "advance", turbo_frame: "_top", search_redirect: true } %>
|
||||
<% else %>
|
||||
<%= render "results", page: @page %>
|
||||
<%= render "results", page: @page, query: @query %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -5,4 +5,5 @@ json.cache! user do
|
||||
json.created_at user.created_at.utc
|
||||
|
||||
json.url user_url(user)
|
||||
json.avatar_url user_avatar_url(user)
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="panel panel--centered flex flex-column gap-half" style="--panel-size: 45ch;">
|
||||
<div class="panel panel--centered flex flex-column gap-half">
|
||||
<h1 class="txt-x-large font-weight-black margin-none">
|
||||
<%= @page_title %>
|
||||
</h1>
|
||||
@@ -18,7 +18,7 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p class="margin-none-block-start margin-block-end-half">Enter your new email address, then check your email to confirm the change.</p>
|
||||
<p class="margin-none-block-start margin-block-end-half">Enter your new email address, then check your email to confirm the change.</p>
|
||||
|
||||
<button type="submit" class="btn btn--link center" data-form-target="submit">
|
||||
<span>Continue</span>
|
||||
|
||||
@@ -2,13 +2,31 @@
|
||||
|
||||
PORT=3006
|
||||
USE_TAILSCALE=0
|
||||
USE_PUSH=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--tailscale) USE_TAILSCALE=1 ;;
|
||||
--push) USE_PUSH=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$USE_PUSH" = "1" ]; then
|
||||
if [ ! -f tmp/saas.txt ]; then
|
||||
echo "Enabling SaaS mode for push notifications..."
|
||||
./bin/rails saas:enable
|
||||
fi
|
||||
echo "Loading push credentials from 1Password..."
|
||||
if ! eval "$(BUNDLE_GEMFILE=Gemfile.saas bundle exec push-dev)"; then
|
||||
echo "Error: failed to load push credentials. Are you signed into 1Password?" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$APNS_ENCRYPTION_KEY_B64" ] || [ -z "$APNS_KEY_ID" ]; then
|
||||
echo "Error: Push credentials not set. Missing APNS_ENCRYPTION_KEY_B64 or APNS_KEY_ID." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f tmp/solid-queue.txt ]; then
|
||||
export SOLID_QUEUE_IN_PUMA=false
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Rails.application.config.to_prepare do
|
||||
Notification.register_push_target(:web)
|
||||
end
|
||||
@@ -245,6 +245,10 @@ Rails.application.routes.draw do
|
||||
get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
|
||||
get "service-worker" => "pwa#service_worker"
|
||||
|
||||
# Mobile clients
|
||||
get "client_configurations/(:platform)_v(:version)" => "client_configurations#show",
|
||||
platform: /android|ios/, version: /\d+/
|
||||
|
||||
namespace :admin do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user