Merge branch 'main' into mobile-app/prepare-webviews
* main: (107 commits) Document the new sign in method Replace handle_ naming Use same constant for fake magic links Replace FakeMagicLink with a temporary object Tidy up session_token Clean up interfaces Split tests by controller or responsibility Simplify auth logic Fix due to unit test when creating with invalid emails Restore sessions_controller test on creating invalid email address Move magic link api tests to their own files Rename test to clarify what they're about Cleanup session creation Update to always return a pending auth token for JSON responses. Update API test for cross code Change test expectation on single tenant mode account creation Add unit tests for the new endpoints Pass a server token when creating a magic link via API Simplify code a bit Simplify session create logic for both html and json ...
This commit is contained in:
@@ -461,7 +461,23 @@
|
||||
font-size: clamp(0.6rem, 0.85cqi, 100px);
|
||||
}
|
||||
|
||||
.card__comments {
|
||||
--column-gap: 0.8ch;
|
||||
|
||||
display: flex;
|
||||
margin-block-end: calc(var(--block-space) * -1.7);
|
||||
margin-inline-end: calc(var(--card-padding-inline) * -0.5);
|
||||
|
||||
.icon {
|
||||
--icon-size: 1.6em;
|
||||
|
||||
color: var(--card-color);
|
||||
}
|
||||
}
|
||||
|
||||
.card__steps {
|
||||
--column-gap: 0.8ch;
|
||||
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
@@ -325,6 +325,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card-perma__notch-new-card-buttons {
|
||||
display: flex;
|
||||
gap: var(--inline-space-half);
|
||||
|
||||
@media (max-width: 479px) {
|
||||
flex-direction: column;
|
||||
|
||||
.btn {
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-perma__closure-message {
|
||||
color: var(--card-color);
|
||||
grid-area: closure-message;
|
||||
|
||||
@@ -35,6 +35,56 @@
|
||||
.house-md-content {
|
||||
padding: var(--comment-padding-block) 0 0;
|
||||
}
|
||||
|
||||
.comment--system & {
|
||||
--comment-padding-block: var(--block-space-half);
|
||||
|
||||
text-align: center;
|
||||
|
||||
&::before {
|
||||
/* Make up space for lack of avatar */
|
||||
content: "";
|
||||
display: flex;
|
||||
inline-size: calc(var(--comment-padding-inline) * 0.9);
|
||||
}
|
||||
|
||||
.comment__avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment__author {
|
||||
a { margin: 0 auto; }
|
||||
h3 { margin-inline: auto; }
|
||||
strong { display: none; }
|
||||
}
|
||||
|
||||
.comment__body {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comment__content {
|
||||
--stripe-color: var(--color-ink-lightest);
|
||||
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg in srgb,
|
||||
var(--color-canvas) 0 1px,
|
||||
var(--stripe-color) 1px 10px);
|
||||
padding-inline: var(--comment-padding-inline);
|
||||
|
||||
.comments--system-expanded .comment--system & {
|
||||
--stripe-color: color-mix(in srgb, var(--card-color) 10%, var(--color-canvas));
|
||||
}
|
||||
|
||||
.comment__history {
|
||||
background-color: var(--stripe-color);
|
||||
}
|
||||
}
|
||||
|
||||
.reactions {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment__author {
|
||||
@@ -74,6 +124,7 @@
|
||||
|
||||
background-color: var(--comment-bg-color);
|
||||
border-radius: 0.2em;
|
||||
max-inline-size: calc(100% - calc(var(--comment-padding-inline) * 0.75));
|
||||
padding:
|
||||
var(--comment-padding-block)
|
||||
calc(var(--comment-padding-inline) / 2)
|
||||
@@ -94,86 +145,28 @@
|
||||
}
|
||||
|
||||
.comment--system {
|
||||
--comment-padding-block: var(--block-space-half);
|
||||
|
||||
display: none;
|
||||
max-inline-size: var(--comment-max);
|
||||
text-align: center;
|
||||
transition: var(--dialog-duration) allow-discrete;
|
||||
transition-property: display;
|
||||
|
||||
.comments--system-expanded & {
|
||||
display: flex;
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-last-child(1 of &):not(:has(~ * &) *):not(:has(~ &) *):not(:has(~ * &)):not(:has(&)) {
|
||||
/* This unholy selector targets the last system comment in the document */
|
||||
/* Hat-tip, https://css-tip.com/last-element-dom/ */
|
||||
display: flex;
|
||||
/* Show the last system comment */
|
||||
:nth-last-child(1 of .comment--system) {
|
||||
display: contents;
|
||||
|
||||
.comment__history {
|
||||
display: grid;
|
||||
}
|
||||
.comment__history {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(1 of &):not(:has(&) ~ * *):not(:has(&) ~ *):not(& ~ * *):not(& *) {
|
||||
/* Targets the first system comment in the document to effectively */
|
||||
/* hide the "Show history" button if there's only one entry */
|
||||
.comment__history {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
/* Make up space for lack of avatar */
|
||||
content: "";
|
||||
display: flex;
|
||||
inline-size: calc(var(--comment-padding-inline) * 0.75);
|
||||
}
|
||||
|
||||
.comment__avatar {
|
||||
/* Hide the "Show history" button if there's only one system comment */
|
||||
:nth-child(1 of .comment--system) {
|
||||
.comment__history {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment__author {
|
||||
a {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__body {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comment__content {
|
||||
--stripe-color: var(--color-ink-lightest);
|
||||
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg in srgb,
|
||||
var(--color-canvas) 0 1px,
|
||||
var(--stripe-color) 1px 10px);
|
||||
padding-inline: var(--comment-padding-inline);
|
||||
|
||||
.comments--system-expanded .comment--system & {
|
||||
--stripe-color: color-mix(in srgb, var(--card-color) 10%, var(--color-canvas));
|
||||
}
|
||||
|
||||
.comment__history {
|
||||
background-color: var(--stripe-color);
|
||||
}
|
||||
}
|
||||
|
||||
.reactions {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.popup__footer {
|
||||
border-block-start: 1px solid var(--color-ink-lightest);
|
||||
color: var(--card-color);
|
||||
margin-block-start: var(--popup-item-padding-inline);
|
||||
padding: var(--popup-item-padding-inline) var(--popup-item-padding-inline) 0;
|
||||
text-align: center;
|
||||
text-transform: initial;
|
||||
}
|
||||
|
||||
.popup__title {
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
inset-inline-end: calc(var(--comment-padding-inline) / 3);
|
||||
}
|
||||
|
||||
.reactions__list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
p:has(+ p) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
padding-inline-start: 3ch;
|
||||
}
|
||||
|
||||
@@ -93,17 +93,14 @@
|
||||
}
|
||||
|
||||
.steps__icon {
|
||||
--icon-size: 0.875em;
|
||||
|
||||
background-color: var(--card-color);
|
||||
block-size: 1.3em;
|
||||
border-radius: 50%;
|
||||
color: var(--color-ink-inverted);
|
||||
display: grid;
|
||||
inline-size: 1.3em;
|
||||
place-content: center;
|
||||
|
||||
.icon {
|
||||
background-color: var(--color-ink-inverted);
|
||||
block-size: 0.8em;
|
||||
inline-size: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,6 +440,7 @@
|
||||
.card__meta-text:not(.card__meta-text--updated),
|
||||
.card__stages,
|
||||
.card__steps,
|
||||
.card__comments,
|
||||
.card__closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class BoardsController < ApplicationController
|
||||
def show_columns
|
||||
cards = @board.cards.awaiting_triage.latest.with_golden_first.preloaded
|
||||
set_page_and_extract_portion_from cards
|
||||
fresh_when etag: [ @board, @page.records, @user_filtering ]
|
||||
fresh_when etag: [ @board, @page.records, @user_filtering, Current.account ]
|
||||
end
|
||||
|
||||
def board_params
|
||||
|
||||
@@ -8,11 +8,16 @@ class Cards::AssignmentsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
@card.toggle_assignment @board.users.active.find(params[:assignee_id])
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
if @card.toggle_assignment @board.users.active.find(params[:assignee_id])
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,6 +61,6 @@ class CardsController < ApplicationController
|
||||
end
|
||||
|
||||
def card_params
|
||||
params.expect(card: [ :status, :title, :description, :image, :created_at, :last_active_at, tag_ids: [] ])
|
||||
params.expect(card: [ :status, :title, :description, :image, :created_at, :last_active_at ])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,13 +4,12 @@ module Authentication
|
||||
included do
|
||||
before_action :require_account # Checking and setting account must happen first
|
||||
before_action :require_authentication
|
||||
after_action :ensure_development_magic_link_not_leaked
|
||||
helper_method :authenticated?
|
||||
helper_method :email_address_pending_authentication
|
||||
|
||||
etag { Current.identity.id if authenticated? }
|
||||
|
||||
include LoginHelper
|
||||
include Authentication::ViaMagicLink, LoginHelper
|
||||
end
|
||||
|
||||
class_methods do
|
||||
@@ -102,35 +101,7 @@ module Authentication
|
||||
cookies.delete(:session_token)
|
||||
end
|
||||
|
||||
def ensure_development_magic_link_not_leaked
|
||||
unless Rails.env.development?
|
||||
raise "Leaking magic link via flash in #{Rails.env}?" if flash[:magic_link_code].present?
|
||||
end
|
||||
end
|
||||
|
||||
def email_address_pending_authentication_matches?(email_address)
|
||||
if ActiveSupport::SecurityUtils.secure_compare(email_address, email_address_pending_authentication || "")
|
||||
session.delete(:email_address_pending_authentication)
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def email_address_pending_authentication
|
||||
session[:email_address_pending_authentication]
|
||||
end
|
||||
|
||||
def redirect_to_session_magic_link(magic_link, return_to: nil)
|
||||
serve_development_magic_link(magic_link)
|
||||
session[:email_address_pending_authentication] = magic_link.identity.email_address if magic_link
|
||||
session[:return_to_after_authenticating] = return_to if return_to
|
||||
redirect_to main_app.session_magic_link_path(script_name: nil)
|
||||
end
|
||||
|
||||
def serve_development_magic_link(magic_link)
|
||||
if Rails.env.development?
|
||||
flash[:magic_link_code] = magic_link&.code
|
||||
end
|
||||
def session_token
|
||||
cookies[:session_token]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
module Authentication::ViaMagicLink
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_action :ensure_development_magic_link_not_leaked
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_development_magic_link_not_leaked
|
||||
unless Rails.env.development?
|
||||
raise "Leaking magic link via flash in #{Rails.env}?" if flash[:magic_link_code].present?
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_to_fake_session_magic_link(email_address, **options)
|
||||
fake_magic_link = MagicLink.new(
|
||||
identity: Identity.new(email_address: email_address),
|
||||
code: SecureRandom.base32(6),
|
||||
expires_at: MagicLink::EXPIRATION_TIME.from_now
|
||||
)
|
||||
|
||||
redirect_to_session_magic_link fake_magic_link, **options
|
||||
end
|
||||
|
||||
def redirect_to_session_magic_link(magic_link, return_to: nil)
|
||||
serve_development_magic_link(magic_link)
|
||||
set_pending_authentication_token(magic_link)
|
||||
session[:return_to_after_authenticating] = return_to if return_to
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to main_app.session_magic_link_url(script_name: nil) }
|
||||
format.json { render json: { pending_authentication_token: pending_authentication_token }, status: :created }
|
||||
end
|
||||
end
|
||||
|
||||
def serve_development_magic_link(magic_link)
|
||||
if Rails.env.development? && magic_link.present?
|
||||
flash[:magic_link_code] = magic_link.code
|
||||
response.set_header("X-Magic-Link-Code", magic_link.code)
|
||||
end
|
||||
end
|
||||
|
||||
def set_pending_authentication_token(magic_link)
|
||||
cookies[:pending_authentication_token] = {
|
||||
value: pending_authentication_token_verifier.generate(magic_link.identity.email_address, expires_at: magic_link.expires_at),
|
||||
httponly: true,
|
||||
same_site: :lax,
|
||||
expires: magic_link.expires_at
|
||||
}
|
||||
end
|
||||
|
||||
def email_address_pending_authentication
|
||||
pending_authentication_token_verifier.verified(pending_authentication_token)
|
||||
end
|
||||
|
||||
def pending_authentication_token_verifier
|
||||
Rails.application.message_verifier(:pending_authentication)
|
||||
end
|
||||
|
||||
def pending_authentication_token
|
||||
cookies[:pending_authentication_token]
|
||||
end
|
||||
|
||||
def clear_pending_authentication_token
|
||||
cookies.delete(:pending_authentication_token)
|
||||
end
|
||||
end
|
||||
@@ -16,7 +16,7 @@ module FilterScoped
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.reverse_merge(**Filter.default_values).permit(*Filter::PERMITTED_PARAMS)
|
||||
params.with_defaults(**Filter.default_values).permit(*Filter::PERMITTED_PARAMS)
|
||||
end
|
||||
|
||||
def set_user_filtering
|
||||
|
||||
@@ -4,7 +4,8 @@ class My::MenusController < ApplicationController
|
||||
@boards = Current.user.boards.ordered_by_recently_accessed
|
||||
@tags = Current.account.tags.all.alphabetically
|
||||
@users = Current.account.users.active.alphabetically
|
||||
@accounts = Current.identity.accounts
|
||||
|
||||
fresh_when etag: [ @filters, @boards, @tags, @users ]
|
||||
fresh_when etag: [ @filters, @boards, @tags, @users, @accounts ]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Sessions::MagicLinksController < ApplicationController
|
||||
disallow_account_scope
|
||||
require_unauthenticated_access
|
||||
rate_limit to: 10, within: 15.minutes, only: :create, with: -> { redirect_to session_magic_link_path, alert: "Wait 15 minutes, then try again" }
|
||||
rate_limit to: 10, within: 15.minutes, only: :create, with: :rate_limit_exceeded
|
||||
before_action :ensure_that_email_address_pending_authentication_exists
|
||||
|
||||
layout "public"
|
||||
@@ -11,25 +11,20 @@ class Sessions::MagicLinksController < ApplicationController
|
||||
|
||||
def create
|
||||
if magic_link = MagicLink.consume(code)
|
||||
authenticate_with magic_link
|
||||
authenticate magic_link
|
||||
else
|
||||
redirect_to session_magic_link_path, flash: { shake: true }
|
||||
invalid_code
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_that_email_address_pending_authentication_exists
|
||||
unless email_address_pending_authentication.present?
|
||||
redirect_to new_session_path, alert: "Enter your email address to sign in."
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_with(magic_link)
|
||||
if email_address_pending_authentication_matches?(magic_link.identity.email_address)
|
||||
start_new_session_for magic_link.identity
|
||||
redirect_to after_sign_in_url(magic_link)
|
||||
else
|
||||
redirect_to new_session_path, alert: "Authentication failed. Please try again."
|
||||
alert_message = "Enter your email address to sign in."
|
||||
respond_to do |format|
|
||||
format.html { redirect_to new_session_path, alert: alert_message }
|
||||
format.json { render json: { message: alert_message }, status: :unauthorized }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,6 +32,41 @@ class Sessions::MagicLinksController < ApplicationController
|
||||
params.expect(:code)
|
||||
end
|
||||
|
||||
def authenticate(magic_link)
|
||||
if ActiveSupport::SecurityUtils.secure_compare(email_address_pending_authentication || "", magic_link.identity.email_address)
|
||||
sign_in magic_link
|
||||
else
|
||||
email_address_mismatch
|
||||
end
|
||||
end
|
||||
|
||||
def sign_in(magic_link)
|
||||
clear_pending_authentication_token
|
||||
start_new_session_for magic_link.identity
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to after_sign_in_url(magic_link) }
|
||||
format.json { render json: { session_token: session_token } }
|
||||
end
|
||||
end
|
||||
|
||||
def email_address_mismatch
|
||||
clear_pending_authentication_token
|
||||
alert_message = "Something went wrong. Please try again."
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to new_session_path, alert: alert_message }
|
||||
format.json { render json: { message: alert_message }, status: :unauthorized }
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_code
|
||||
respond_to do |format|
|
||||
format.html { redirect_to session_magic_link_path, flash: { shake: true } }
|
||||
format.json { render json: { message: "Try another code." }, status: :unauthorized }
|
||||
end
|
||||
end
|
||||
|
||||
def after_sign_in_url(magic_link)
|
||||
if magic_link.for_sign_up?
|
||||
new_signup_completion_path
|
||||
@@ -44,4 +74,12 @@ class Sessions::MagicLinksController < ApplicationController
|
||||
after_authentication_url
|
||||
end
|
||||
end
|
||||
|
||||
def rate_limit_exceeded
|
||||
rate_limit_exceeded_message = "Try again in 15 minutes."
|
||||
respond_to do |format|
|
||||
format.html { redirect_to session_magic_link_path, alert: rate_limit_exceeded_message }
|
||||
format.json { render json: { message: rate_limit_exceeded_message }, status: :too_many_requests }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class SessionsController < ApplicationController
|
||||
disallow_account_scope
|
||||
require_unauthenticated_access except: :destroy
|
||||
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
|
||||
rate_limit to: 10, within: 3.minutes, only: :create, with: :rate_limit_exceeded
|
||||
|
||||
layout "public"
|
||||
|
||||
@@ -9,16 +9,12 @@ class SessionsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
if identity = Identity.find_by_email_address(email_address)
|
||||
redirect_to_session_magic_link identity.send_magic_link
|
||||
if identity = Identity.find_by(email_address: email_address)
|
||||
sign_in identity
|
||||
elsif Account.accepting_signups?
|
||||
sign_up
|
||||
else
|
||||
signup = Signup.new(email_address: email_address)
|
||||
if signup.valid?(:identity_creation)
|
||||
magic_link = signup.create_identity if Account.accepting_signups?
|
||||
redirect_to_session_magic_link magic_link
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
redirect_to_fake_session_magic_link email_address
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,7 +24,43 @@ class SessionsController < ApplicationController
|
||||
end
|
||||
|
||||
private
|
||||
def magic_link_from_sign_in_or_sign_up
|
||||
if identity = Identity.find_by_email_address(email_address)
|
||||
identity.send_magic_link
|
||||
else
|
||||
signup = Signup.new(email_address: email_address)
|
||||
signup.create_identity if signup.valid?(:identity_creation) && Account.accepting_signups?
|
||||
end
|
||||
end
|
||||
|
||||
def email_address
|
||||
params.expect(:email_address)
|
||||
end
|
||||
|
||||
def rate_limit_exceeded
|
||||
rate_limit_exceeded_message = "Try again later."
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to new_session_path, alert: rate_limit_exceeded_message }
|
||||
format.json { render json: { message: rate_limit_exceeded_message }, status: :too_many_requests }
|
||||
end
|
||||
end
|
||||
|
||||
def sign_in(identity)
|
||||
redirect_to_session_magic_link identity.send_magic_link
|
||||
end
|
||||
|
||||
def sign_up
|
||||
signup = Signup.new(email_address: email_address)
|
||||
|
||||
if signup.valid?(:identity_creation)
|
||||
magic_link = signup.create_identity
|
||||
redirect_to_session_magic_link magic_link
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html { redirect_to new_session_path, alert: "Something went wrong" }
|
||||
format.json { render json: { message: "Something went wrong" }, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,12 +10,6 @@ module ApplicationHelper
|
||||
tag.span class: class_names("icon icon--#{name}", options.delete(:class)), "aria-hidden": true, **options
|
||||
end
|
||||
|
||||
def inline_svg(name)
|
||||
file_path = "#{Rails.root}/app/assets/images/#{name}.svg"
|
||||
return File.read(file_path).html_safe if File.exist?(file_path)
|
||||
"(not found)"
|
||||
end
|
||||
|
||||
def back_link_to(label, url, action, **options)
|
||||
link_to url, class: "btn btn--back", data: { controller: "hotkey", action: action }, **options do
|
||||
icon_tag("arrow-left") + tag.strong("Back to #{label}", class: "overflow-ellipsis") + tag.kbd("ESC", class: "txt-x-small hide-on-touch").html_safe
|
||||
|
||||
@@ -43,7 +43,7 @@ module ColumnsHelper
|
||||
end
|
||||
|
||||
def column_frame_tag(id, src: nil, data: {}, **options, &block)
|
||||
data = data.reverse_merge \
|
||||
data = data.with_defaults \
|
||||
drag_and_drop_refresh: true,
|
||||
controller: "frame",
|
||||
action: "turbo:before-frame-render->frame#morphRender turbo:before-morph-element->frame#morphReload"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static values = { limit: Number, count: Number }
|
||||
static targets = ["unassigned", "limitMessage"]
|
||||
|
||||
connect() {
|
||||
this.updateState()
|
||||
}
|
||||
|
||||
countValueChanged() {
|
||||
this.updateState()
|
||||
}
|
||||
|
||||
updateState() {
|
||||
const atLimit = this.countValue >= this.limitValue
|
||||
|
||||
this.unassignedTargets.forEach(el => {
|
||||
el.hidden = atLimit
|
||||
})
|
||||
|
||||
if (this.hasLimitMessageTarget) {
|
||||
this.limitMessageTarget.hidden = !atLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class Account < ApplicationRecord
|
||||
def create_with_owner(account:, owner:)
|
||||
create!(**account).tap do |account|
|
||||
account.users.create!(role: :system, name: "System")
|
||||
account.users.create!(**owner.reverse_merge(role: "owner", verified_at: Time.current))
|
||||
account.users.create!(**owner.with_defaults(role: :owner, verified_at: Time.current))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -69,6 +69,7 @@ class Account::Seeder
|
||||
<li>Make another called "Working on"</li>
|
||||
</ol>
|
||||
<p>Go back to the Board view, click the little “+” to the right of the DONE column, name the column, pick a color, then do it again.</p>
|
||||
<p><br></p>
|
||||
<p>After that, drag this card to “DONE” or select “DONE” in the sidebar.</p>
|
||||
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/make-columns.gif" alt="Demo of adding columns" caption="Make two more columns" content-type="image/*" filename="make-columns.gif" presentation="gallery"></action-text-attachment>
|
||||
HTML
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
class Assignment < ApplicationRecord
|
||||
LIMIT = 100
|
||||
|
||||
belongs_to :account, default: -> { card.account }
|
||||
belongs_to :card, touch: true
|
||||
|
||||
belongs_to :assignee, class_name: "User"
|
||||
belongs_to :assigner, class_name: "User"
|
||||
|
||||
validate :within_limit, on: :create
|
||||
|
||||
private
|
||||
def within_limit
|
||||
if card.assignments.count >= LIMIT
|
||||
errors.add(:base, "Card already has the maximum of #{LIMIT} assignees")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,10 +24,12 @@ module Card::Assignable
|
||||
|
||||
private
|
||||
def assign(user)
|
||||
assignments.create! assignee: user, assigner: Current.user
|
||||
watch_by user
|
||||
assignment = assignments.create assignee: user, assigner: Current.user
|
||||
|
||||
track_event :assigned, assignee_ids: [ user.id ]
|
||||
if assignment.persisted?
|
||||
watch_by user
|
||||
track_event :assigned, assignee_ids: [ user.id ]
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
# Already assigned
|
||||
end
|
||||
|
||||
@@ -6,8 +6,6 @@ module Card::Statuses
|
||||
|
||||
before_save :mark_if_just_published
|
||||
after_create -> { track_event :published }, if: :published?
|
||||
|
||||
scope :published_or_drafted_by, ->(user) { where(status: :published).or(where(status: :drafted, creator: user)) }
|
||||
end
|
||||
|
||||
attr_accessor :was_just_published
|
||||
|
||||
@@ -39,7 +39,7 @@ module User::EmailAddressChangeable
|
||||
|
||||
private
|
||||
def generate_email_address_change_token(from: identity.email_address, to:, **options)
|
||||
options = options.reverse_merge(
|
||||
options = options.with_defaults(
|
||||
for: EMAIL_CHANGE_TOKEN_PURPOSE,
|
||||
old_email_address: from,
|
||||
new_email_address: to,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<li class="flex align-center gap-half" data-filter-target="item" data-navigable-list-target="item">
|
||||
<li class="flex align-center gap-half" data-filter-target="item" data-navigable-list-target="item" role="option">
|
||||
<%= link_to user, class: "txt-ink flex gap-half align-center min-width" do %>
|
||||
<%= avatar_preview_tag user, hidden_for_screen_reader: true %>
|
||||
<div class="txt-align-start overflow-ellipsis">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="settings__user-filter">
|
||||
<input placeholder="Filter…" class="input input--transparent full-width txt-small" type="search" autocorrect="off" autocomplete="off" data-1p-ignore="true" data-filter-target="input" data-action="input->filter#filter">
|
||||
|
||||
<ul class="settings__user-list margin-block-half" data-filter-target="list">
|
||||
<ul class="settings__user-list margin-block-half" data-filter-target="list" role="listbox">
|
||||
<%= render partial: "account/settings/user", collection: users %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<% postponed_count = board.cards.postponed.size %>
|
||||
<% awaiting_triage_count = board.cards.awaiting_triage.size %>
|
||||
<% closed_count = board.cards.closed.size %>
|
||||
|
||||
<section class="mobile-card-columns">
|
||||
<div class="cards margin-block-end-half">
|
||||
<%= render "columns/show/add_card_button", board: board %>
|
||||
</div>
|
||||
|
||||
<%= link_to board_columns_not_now_path(board), class: "cards cards--on-deck is-collapsed", style: "--card-color: var(--color-card-complete);", data: { turbo_frame: "_top" } do %>
|
||||
<div class="cards__expander btn btn--plain" style="--card-count: <%= board.cards.postponed.count %>">
|
||||
<span class="cards__expander-count"><%= board.cards.postponed.count %></span>
|
||||
<div class="cards__expander btn btn--plain" style="--card-count: <%= postponed_count %>">
|
||||
<span class="cards__expander-count"><%= postponed_count %></span>
|
||||
<h2 class="cards__expander-title">
|
||||
Not Now
|
||||
</h2>
|
||||
@@ -13,8 +17,8 @@
|
||||
<% end %>
|
||||
|
||||
<%= link_to board_columns_stream_path(board), class: "cards cards--considering is-collapsed", data: { turbo_frame: "_top" } do %>
|
||||
<div class="cards__expander btn btn--plain" style="--card-count: <%= board.cards.awaiting_triage.count %>">
|
||||
<span class="cards__expander-count"><%= board.cards.awaiting_triage.count %></span>
|
||||
<div class="cards__expander btn btn--plain" style="--card-count: <%= awaiting_triage_count %>">
|
||||
<span class="cards__expander-count"><%= awaiting_triage_count %></span>
|
||||
<h2 class="cards__expander-title">
|
||||
Maybe?
|
||||
</h2>
|
||||
@@ -22,9 +26,10 @@
|
||||
<% end %>
|
||||
|
||||
<% board.columns.sorted.each do |column| %>
|
||||
<% active_count = column.cards.active.size %>
|
||||
<%= link_to board_column_path(column.board, column), class: "cards cards--doing is-collapsed", style: "--card-color: #{column.color}", data: { turbo_frame: "_top" } do %>
|
||||
<div class="cards__expander btn btn--plain" style="--card-count: <%= column.cards.active.count %>">
|
||||
<span class="cards__expander-count"><%= column.cards.active.count %></span>
|
||||
<div class="cards__expander btn btn--plain" style="--card-count: <%= active_count %>">
|
||||
<span class="cards__expander-count"><%= active_count %></span>
|
||||
<h2 class="cards__expander-title">
|
||||
<%= column.name %>
|
||||
</h2>
|
||||
@@ -33,8 +38,8 @@
|
||||
<% end %>
|
||||
|
||||
<%= link_to board_columns_closed_path(board), class: "cards cards--closed is-collapsed", style: "--card-color: var(--color-card-complete);", data: { turbo_frame: "_top" } do %>
|
||||
<div class="cards__expander btn btn--plain" style="--card-count: <%= board.cards.closed.count %>">
|
||||
<span class="cards__expander-count"><%= board.cards.closed.count %></span>
|
||||
<div class="cards__expander btn btn--plain" style="--card-count: <%= closed_count %>">
|
||||
<span class="cards__expander-count"><%= closed_count %></span>
|
||||
<h2 class="cards__expander-title">
|
||||
Done
|
||||
</h2>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<%= turbo_frame_tag @card, :assignment do %>
|
||||
<%= tag.div class: "max-width full-width", data: {
|
||||
action: "turbo:before-cache@document->dialog#close dialog:show@document->navigable-list#reset keydown->navigable-list#navigate filter:changed->navigable-list#reset",
|
||||
controller: "filter navigable-list",
|
||||
controller: "filter navigable-list assignment-limit",
|
||||
dialog_target: "dialog",
|
||||
navigable_list_focus_on_selection_value: false,
|
||||
navigable_list_actionable_items_value: true } do %>
|
||||
navigable_list_actionable_items_value: true,
|
||||
assignment_limit_limit_value: Assignment::LIMIT,
|
||||
assignment_limit_count_value: @card.assignments.count } do %>
|
||||
|
||||
<div class="flex align-start justify-space-between">
|
||||
<strong class="popup__title">Assign this to…</strong>
|
||||
@@ -17,9 +19,18 @@
|
||||
<ul class="popup__list" data-filter-target="list">
|
||||
<%= render "user", card: @card, user: Current.user, user_label: "Me" do %>
|
||||
<span class="visually-hidden"><%= Current.user.name %></span>
|
||||
<kbd class="txt-xx-small hide-on-touch">m</kbd>
|
||||
<% end %>
|
||||
<%= render collection: @assigned_to, partial: "user", locals: { card: @card } %>
|
||||
<%= render collection: @users, partial: "user", locals: { card: @card } %>
|
||||
<% @users.each do |user| %>
|
||||
<span data-assignment-limit-target="unassigned">
|
||||
<%= render "user", card: @card, user: user %>
|
||||
</span>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<div class="popup__footer" hidden data-assignment-limit-target="limitMessage">
|
||||
Maximum <%= Assignment::LIMIT %> assignees
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<% cache comment do %>
|
||||
<%# Helper Dependency Updated: avatar_image_tag 2025-12-15 %>
|
||||
<%= turbo_frame_tag comment, :container do %>
|
||||
<%= turbo_frame_tag comment, :container, class: ["comment--system": comment.creator.system?] do %>
|
||||
<%# Cache bump 2025-12-14: action text attachment rendering changed for lightbox -%>
|
||||
<div id="<%= dom_id(comment) %>" data-creator-id="<%= comment.creator_id %>" class="comment align-start full-width <%= "comment--system" if comment.creator.system? %>">
|
||||
<div id="<%= dom_id(comment) %>" data-creator-id="<%= comment.creator_id %>" class="comment align-start full-width">
|
||||
<figure class="comment__avatar flex-item-no-shrink" aria-hidden="true">
|
||||
<%= avatar_tag comment.creator, hidden_for_screen_reader: true %>
|
||||
</figure>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="card-perma__notch card-perma__notch--bottom flex-column">
|
||||
<div class="flex gap-half">
|
||||
<div class="card-perma__notch-new-card-buttons">
|
||||
<%= button_to card_publish_path(card), name: "creation_type", value: "add", class: "btn",
|
||||
title: "Create card (#{ hotkey_label(["ctrl", "enter"]) })",
|
||||
form: { data: { controller: "form" } },
|
||||
|
||||
@@ -30,8 +30,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="card__footer">
|
||||
<footer class="card__footer flex gap-half">
|
||||
<%= render "cards/display/preview/meta", card: card, preview: true %>
|
||||
<%= render "cards/display/preview/comments", card: card %>
|
||||
<%= render "cards/display/common/background", card: card %>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<% if Current.user %>
|
||||
<%= button_to "Assign to me", card_assignments_path(card, params: { assignee_id: Current.user.id }), method: :post, data: {controller: "hotkey", action: "keydown.m@document->hotkey#click" }, hidden: true %>
|
||||
<% end %>
|
||||
|
||||
<dialog class="popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-morph-attribute->dialog#preventCloseOnMorphing turbo:submit-end->dialog#close">
|
||||
<%= yield %>
|
||||
</dialog>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<% comments = card.comments.by_user %>
|
||||
<% if comments.any? %>
|
||||
<div class="card__comments align-center gap-half flex-item-justify-end flex-item-no-shrink">
|
||||
<%= icon_tag "comment" %>
|
||||
<strong><%= comments.count %></strong>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -7,8 +7,8 @@
|
||||
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
|
||||
<nav>
|
||||
<%= link_to "https://fizzy.do", class: "header__logo center flex-inline align-center" do %>
|
||||
<span><%= image_tag "logo.png" %></span>
|
||||
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
|
||||
<span><%= image_tag "logo.png", alt: "" %></span>
|
||||
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg" title="Fizzy"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
|
||||
<% end %>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<%= tag.button class:"nav__trigger input input--select center flex-inline align-center txt-normal", data: {
|
||||
action: "click->dialog#open keydown.j@document->hotkey#click keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
|
||||
controller: "hotkey" } do %>
|
||||
<span><%= image_tag "logo.png" %></span>
|
||||
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
|
||||
<span><%= image_tag "logo.png", alt: "" %></span>
|
||||
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg" title="Fizzy"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
|
||||
<kbd class="kbd txt-xx-small hide-on-touch">J</kbd>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<%= render "my/menus/people", users: @users %>
|
||||
<%= render "my/menus/settings" %>
|
||||
<%= render "my/menus/shortcuts" %>
|
||||
<%= render "my/menus/accounts", accounts: Current.identity.accounts %>
|
||||
<%= render "my/menus/accounts", accounts: @accounts %>
|
||||
<% end %>
|
||||
|
||||
<footer class="nav__footer">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</header>
|
||||
|
||||
<%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: "magic-link" } } do |form| %>
|
||||
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large",
|
||||
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large txt-uppercase",
|
||||
autofocus: true, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true,
|
||||
autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code],
|
||||
data: { magic_link_target: "input", action: "keydown.enter->magic-link#submitOnEnter paste->magic-link#submitOnPaste" } %>
|
||||
|
||||
Reference in New Issue
Block a user