Merge branch 'main' into update-column-icons

* main: (41 commits)
  Remove animations from popups
  Remove fixed position
  Sticky Fizzy nav
  Account for MS mail
  Style avatar initials
  Save three requests per card view
  Ensure mention avatars cover the entire circle
  Bump fizzy
  Enable public file server for assets
  Respect size
  Reuse logo as avatar
  Add test
  Add background color
  Don't render SVG avatars in emails since mail services won't render them
  Disable user creation tests for the OSS config
  Update test
  Remove debug
  Further copy tweaks
  More copy and design edits
  Extract saas helpers into a concern
  ...
This commit is contained in:
Andy Smith
2025-11-07 15:59:09 -06:00
87 changed files with 492 additions and 394 deletions
+1 -1
View File
@@ -290,7 +290,7 @@ GEM
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lexxy (0.1.19.beta)
lexxy (0.1.20.beta)
rails (>= 8.0.2)
lint_roller (1.1.0)
logger (1.7.0)
+1 -1
View File
@@ -89,7 +89,7 @@
--lch-ink-darkest: 26% 0.05 264;
--lch-ink-darker: 40% 0.026 262;
--lch-ink-dark: 57% 0.014 260;
--lch-ink-dark: 56% 0.014 260;
--lch-ink-medium: 66% 0.008 258;
--lch-ink-light: 84% 0.005 256;
--lch-ink-lighter: 92% 0.003 254;
+14 -4
View File
@@ -3,6 +3,10 @@
/* ------------------------------------------------------------------------ */
#main:has(.card-columns) {
--main-padding: 0;
}
.card-columns {
--bubble-size: 3.5rem;
--cards-gap: min(1.2cqi, 1.7rem);
--column-gap: 8px;
@@ -14,10 +18,7 @@
--progress-max-cards: 20;
--progress-max-height: 50dvh;
padding-inline: var(--column-gap);
}
.card-columns {
-ms-overflow-style: none; /* Hide x-scrollbar on Edge */
container-type: inline-size;
display: grid;
gap: var(--column-gap);
@@ -26,7 +27,14 @@
max-inline-size: var(--main-width);
overflow-x: auto;
overflow-y: hidden;
padding-block-end: var(--column-width-collapsed);
position: relative;
scrollbar-width: none; /* Hide x-scrollbar on FF */
/* Hide x-scrollbar on Chrome/Safari/Opera */
&::-webkit-scrollbar {
display: none;
}
/* When it has something expanded */
&:has(.card-columns__left .cards:not(.is-collapsed), .card-columns__right .cards:not(.is-collapsed)) {
@@ -51,10 +59,12 @@
.card-columns__left {
justify-content: end;
padding-inline-start: var(--column-gap);
}
.card-columns__right {
justify-content: start;
padding-inline-end: var(--column-gap);
}
/* Column
+4
View File
@@ -498,5 +498,9 @@
margin-inline: auto;
text-align: start;
}
code {
text-align: left;
}
}
}
+14 -1
View File
@@ -13,7 +13,8 @@
"menu menu menu"
"actions-start title actions-end";
max-inline-size: 100dvw;
padding: calc(var(--block-space-half) + env(safe-area-inset-top)) var(--main-padding);
padding-block: calc(var(--block-space-half) + env(safe-area-inset-top)) var(--block-space-half);
padding-inline: var(--main-padding);
/* Change the grid size depending on how many buttons are present */
&:has(.header__actions > *:nth-child(1)) { --header-button-count: 1; }
@@ -78,4 +79,16 @@
--left-offset: var(--inline-space);
}
}
.header__logo {
color: var(--color-ink);
inline-size: auto;
margin-block-start: 0.1em;
img {
block-size: auto;
inline-size: 1.15em;
margin-inline-end: 0.8ch;
}
}
}
@@ -1,17 +1,5 @@
@layer components {
.boxcar-header-logo {
color: var(--color-ink);
inline-size: auto;
margin-block-start: 0.1em;
img {
block-size: auto;
inline-size: 1.15em;
margin-inline-end: 0.8ch;
}
}
.input:is(.boxcar-menu-trigger) {
.nav__trigger {
--input-background: var(--color-canvas);
--input-border-color: transparent;
--input-padding: 0.3em 2em 0.3em 0.75em;
@@ -58,9 +46,10 @@
}
}
.boxcar-menu:is(.panel) {
.nav__menu.popup {
--panel-border-color: var(--color-selected-dark);
--panel-border-radius: 1.4em;
--panel-padding: var(--block-space) var(--block-space) 0 var(--block-space);
--panel-size: 45ch;
align-items: stretch;
@@ -71,24 +60,15 @@
flex-direction: column;
gap: 2px;
scrollbar-gutter: stable both-edges;
/* Safari doesn't position the sticky footer properly during the
* transition, so we need to delay the footer entrance. */
.popup__footer {
animation: safari-sticky-footer-hack 0s;
animation-delay: var(--dialog-duration);
animation-fill-mode: both;
inset-inline: var(--block-space);
}
}
.boxcar-menu__close {
.nav__close {
@media (any-hover: hover) {
display: none;
}
}
.boxcar-menu__hotkeys {
.nav__hotkeys {
--gap: 8px;
align-items: center;
@@ -152,4 +132,28 @@
}
}
}
/* Safari doesn't position the sticky footer properly during the
* transition, so we need to delay the footer entrance. */
.nav__footer {
animation: safari-sticky-footer-hack 0s;
animation-delay: var(--dialog-duration);
animation-fill-mode: both;
background-color: var(--color-canvas);
border-block-start: 1px solid var(--color-ink-lighter);
font-size: var(--text-small);
inset: auto 0 0;
inset-inline: var(--block-space);
line-height: 1.6;
margin-block-start: var(--block-space-half);
padding: 1.5ch;
position: sticky;
text-align: center;
z-index: 1;
}
@keyframes safari-sticky-footer-hack {
0% { position: absolute; }
100% { position: sticky; }
}
}
+36 -49
View File
@@ -14,39 +14,9 @@
position: absolute;
transform: translateX(-50%);
z-index: var(--z-popup);
opacity: 0;
transform: scale(0.2) translateX(-50%);
transform-origin: top left;
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay, transform;
&::backdrop {
background-color: var(--color-always-black);
opacity: 0;
transform: scale(1);
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay;
}
&[open] {
display: flex;
opacity: 1;
transform: scale(1) translateX(-50%);
&::backdrop {
opacity: 0.5;
}
}
@starting-style {
&[open] {
opacity: 0;
transform: scale(0.2) translateX(-50%);
}
&[open]::backdrop {
opacity: 0;
}
}
&.orient-left {
@@ -59,10 +29,6 @@
transform: translateX(0);
}
&:has(.popup__footer) {
--panel-padding: var(--block-space) var(--block-space) 0 var(--block-space);
}
form {
display: contents;
}
@@ -196,21 +162,42 @@
}
}
.popup__footer {
background-color: var(--color-canvas);
border-block-start: 1px solid var(--color-ink-lighter);
font-size: var(--text-small);
inset: auto 0 0;
line-height: 1.6;
margin-block-start: var(--block-space-half);
padding: 1.5ch;
position: sticky;
text-align: center;
z-index: 1;
}
/* Animated
/* -------------------------------------------------------------------------- */
@keyframes safari-sticky-footer-hack {
0% { position: absolute; }
100% { position: sticky; }
.popup--animated {
opacity: 0;
transform: scale(0.2) translateX(-50%);
transform-origin: top left;
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay, transform;
&::backdrop {
background-color: var(--color-always-black);
opacity: 0;
transform: scale(1);
transition: var(--dialog-duration) allow-discrete;
transition-property: display, opacity, overlay;
}
&[open] {
opacity: 1;
transform: scale(1) translateX(-50%);
&::backdrop {
opacity: 0.5;
}
}
@starting-style {
&[open] {
opacity: 0;
transform: scale(0.2) translateX(-50%);
}
&[open]::backdrop {
opacity: 0;
}
}
}
}
+2 -2
View File
@@ -35,8 +35,8 @@
print-color-adjust: exact;
}
.boxcar-menu,
.boxcar-menu-trigger,
.nav__menu,
.nav__trigger,
.header__actions {
display: none;
}
+14
View File
@@ -29,6 +29,7 @@
}
.reactions__trigger {
--btn-border-color: var(--color-ink-lightest);
background-color: var(--color-ink-lightest);
&:not(:hover) {
@@ -38,10 +39,23 @@
}
}
.reactions__trigger {
--btn-border-color: var(--reaction-border-color);
img {
block-size: 65%;
inline-size: 65%;
}
}
.reactions__list {
display: inline-flex;
flex-wrap: wrap;
gap: var(--inline-space-half);
&:not(:has(.reaction)) {
display: none;
}
}
/* Single reaction
@@ -375,6 +375,12 @@
}
}
action-text-attachment[content-type^='application/vnd.actiontext.mention'] {
img {
object-fit: cover;
}
}
[data-lexical-cursor] {
animation: blink 1s step-end infinite;
block-size: 1lh;
+1 -1
View File
@@ -16,7 +16,7 @@
.txt-ink { color: var(--color-ink); }
.txt-reversed { color: var(--color-ink-inverted); }
.txt-negative { color: var(--color-negative); }
.txt-subtle { color: var(--color-ink-medium); }
.txt-subtle { color: var(--color-ink-dark); }
.txt-alert { color: var(--color-marker); }
.txt-undecorated { text-decoration: none; }
.txt-underline { text-decoration: underline; }
@@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base
include Authorization
include CurrentRequest, CurrentTimezone, SetPlatform
include TurboFlash, ViewTransitions
include Saas
stale_when_importmap_changes
allow_browser versions: :modern
+11
View File
@@ -0,0 +1,11 @@
module Saas
extend ActiveSupport::Concern
included do
helper_method :signups_allowed?
end
def signups_allowed?
defined?(Signup) && defined?(saas)
end
end
+4 -1
View File
@@ -4,6 +4,8 @@ class JoinCodesController < ApplicationController
before_action :set_join_code
before_action :ensure_join_code_is_valid
layout "public"
def new
@account_name = ApplicationRecord.with_tenant(tenant) { Account.sole.name }
end
@@ -14,7 +16,8 @@ class JoinCodesController < ApplicationController
identity.memberships.find_or_create_by!(tenant: tenant) do |membership|
membership.join_code = code
end
identity.send_magic_link
magic_link = identity.send_magic_link
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
end
session[:return_to_after_authenticating] = landing_url(script_name: "/#{tenant}")
+14 -2
View File
@@ -1,4 +1,9 @@
class SessionsController < ApplicationController
# FIXME: Remove this before launch!
SIGNUP_USERNAME = Rails.env.test? ? "testname" : Rails.application.credentials.account_signup_http_basic_auth.name
SIGNUP_PASSWORD = Rails.env.test? ? "testpassword" : Rails.application.credentials.account_signup_http_basic_auth.password
http_basic_authenticate_with name: SIGNUP_USERNAME, password: SIGNUP_PASSWORD, realm: "Fizzy Signup", only: :create, unless: -> { Identity.exists?(email_address: email_address) }
require_untenanted_access
require_unauthenticated_access except: :destroy
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
@@ -9,8 +14,15 @@ class SessionsController < ApplicationController
end
def create
Identity.find_by_email_address(email_address)&.send_magic_link
redirect_to session_magic_link_path
if identity = Identity.find_by_email_address(email_address)
magic_link = identity.send_magic_link
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
redirect_to session_magic_link_path
elsif signups_allowed?
Signup.new(email_address: email_address).create_identity
session[:return_to_after_authenticating] = saas.new_signup_membership_path
redirect_to session_magic_link_path
end
end
def destroy
@@ -3,6 +3,8 @@ class Users::JoinsController < ApplicationController
before_action :set_join_code, :ensure_join_code_is_valid
layout "public"
def new
end
+10
View File
@@ -19,6 +19,16 @@ module AvatarsHelper
end
end
def mail_avatar_tag(user, size: 48, **options)
if user.avatar.attached?
image_tag user_avatar_url(user), alt: user.name, class: "avatar", size: size, **options
else
tag.span class: "avatar", style: "background-color: #{avatar_background_color(user)};" do
user.initials
end
end
end
def avatar_preview_tag(user, hidden_for_screen_reader: false, **options)
tag.span class: class_names("avatar", options.delete(:class)),
aria: { hidden: hidden_for_screen_reader, label: user.name },
@@ -24,6 +24,7 @@ export default class extends Controller {
limitHeightToViewport(this.dialogTarget, true)
this.#loadLazyFrames()
this.dialogTarget.setAttribute("aria-hidden", "false")
this.dispatch("show")
}
@@ -54,4 +55,8 @@ export default class extends Controller {
event.stopPropagation()
}
}
#loadLazyFrames() {
Array.from(this.dialogTarget.querySelectorAll("turbo-frame")).forEach(frame => { frame.loading = "eager" })
}
}
@@ -0,0 +1,24 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.element.addEventListener("dialog:show", this.handleDialogShow.bind(this))
}
disconnect() {
this.element.removeEventListener("dialog:show", this.handleDialogShow.bind(this))
}
handleDialogShow(event) {
this.#dialogControllers.forEach(dialogController => {
if (dialogController !== event.target) {
const dialog = dialogController.querySelector("dialog")
dialog.removeAttribute("open")
}
})
}
get #dialogControllers() {
return this.element.querySelectorAll('[data-controller~="dialog"]')
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
class ApplicationMailer < ActionMailer::Base
default from: "The Fizzy team <support@37signals.com>"
default from: "Fizzy <support@37signals.com>"
layout "mailer"
append_view_path Rails.root.join("app/views/mailers")
+1 -1
View File
@@ -3,7 +3,7 @@ class MagicLinkMailer < ApplicationMailer
@magic_link = magic_link
@identity = @magic_link.identity
mail to: @identity.email_address, subject: "Sign in to Fizzy"
mail to: @identity.email_address, subject: "Your Fizzy verification code"
end
private
-8
View File
@@ -1,8 +0,0 @@
class UserMailer < ApplicationMailer
def email_change_confirmation(user:, email_address:, token:)
@user = user
@token = token
mail to: email_address, subject: "Confirm your new email address"
end
end
+5
View File
@@ -45,6 +45,7 @@ module Board::Accessible
mentions_for_user(user).destroy_all
notifications_for_user(user).destroy_all
watches_for(user).destroy_all
end
def watchers
@@ -89,4 +90,8 @@ module Board::Accessible
(notifications.source_type = 'Event' AND events.eventable_type = 'Comment' AND event_comment_cards.board_id = ?)",
id, id)
end
def watches_for(user)
Watch.where(card: cards, user: user)
end
end
+1 -1
View File
@@ -5,7 +5,7 @@ class Identity < UntenantedRecord
has_many :magic_links, dependent: :destroy
has_many :sessions, dependent: :destroy
normalizes :email_address, with: ->(value) { value.strip.downcase }
normalizes :email_address, with: ->(value) { value.strip.downcase.presence }
def send_magic_link
magic_links.create!.tap do |magic_link|
+1 -1
View File
@@ -33,6 +33,6 @@
<div class="settings__panel panel shadow center">
<%= render "boards/edit/auto_close", board: @board %>
<%= render "boards/edit/publication", board: @board %>
<%= render "boards/edit/delete", board: @board %>
<%= render "boards/edit/delete", board: @board if Current.user.can_administer_card?(@board) %>
</div>
</section>
@@ -6,7 +6,7 @@
<%= turbo_frame_tag comment, :new_reaction do %>
<%= link_to new_card_comment_reaction_path(comment.card, comment), role: "button",
class: "reactions__trigger btn btn--circle borderless", action: "soft-keyboard#open",
class: "reactions__trigger btn btn--circle", action: "soft-keyboard#open",
data: { turbo_frame: dom_id(comment, :new_reaction), action: "dialog#close" } do %>
<%= image_tag "boost-color.svg", aria: { hidden: true } %>
<span class="for-screen-reader">Add your own reaction</span>
@@ -1,3 +1,3 @@
<%= render "cards/display/common/assignees", card: card do %>
<%= turbo_frame_tag card, :assignment, src: new_card_assignment_path(card), refresh: "morph" %>
<%= turbo_frame_tag card, :assignment, src: new_card_assignment_path(card), loading: :lazy, refresh: "morph" %>
<% end %>
@@ -6,7 +6,7 @@
</button>
<dialog class="popup panel flex-column align-start gap-half fill-white shadow margin-block-double" data-dialog-target="dialog" data-action="turbo:before-morph-attribute->dialog#preventCloseOnMorphing turbo:submit-end->dialog#close">
<%= turbo_frame_tag "board_picker", src: edit_card_board_path(card), target: "_top", refresh: "morph" %>
<%= turbo_frame_tag "board_picker", src: edit_card_board_path(card), target: "_top", loading: :lazy, refresh: "morph" %>
</dialog>
</div>
<% end %>
+8 -4
View File
@@ -1,12 +1,16 @@
<div id="<%= dom_id(card, :tags) %>" class="card__tags">
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" <%= "hidden" if card.closed? %>>
<button class="tag-picker__button btn card__hide-on-index" data-action="click->dialog#open:stop" style="--btn-background: var(--card-bg-color);" data-controller="tooltip">
<div class="position-relative" data-controller="dialog"
data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" <%= "hidden" if card.closed? %>>
<button class="tag-picker__button btn card__hide-on-index" style="--btn-background: var(--card-bg-color);"
data-controller="tooltip" data-action="click->dialog#open:stop">
<%= icon_tag "tag" %>
<span class="for-screen-reader">Add a tag</span>
</button>
<dialog class="popup panel flex-column align-start justify-start fill-white shadow txt-small" data-dialog-target="dialog" data-action="turbo:before-morph-attribute->dialog#preventCloseOnMorphing turbo:submit-end->dialog#close">
<%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card), refresh: "morph" %>
<dialog class="popup panel flex-column align-start justify-start fill-white shadow txt-small"
data-dialog-target="dialog"
data-action="turbo:before-morph-attribute->dialog#preventCloseOnMorphing turbo:submit-end->dialog#close">
<%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card), loading: :lazy, refresh: :morph %>
</dialog>
</div>
+1 -1
View File
@@ -1,7 +1,7 @@
<%= tag.aside \
class: class_names("filters margin-block-end", { "filters--expanded": user_filtering.expanded? }),
data: {
controller: "toggle-enable toggle-class filter-settings",
controller: "toggle-enable toggle-class filter-settings dialog-manager",
toggle_class_toggle_class: "filters--expanded",
filter_settings_filters_set_class: "filters--has-filters-set",
filter_settings_no_filtering_url_value: no_filtering_url,
@@ -1,5 +1,5 @@
<% filter = user_filtering.filter %>
<%= tag.div class: "quick-filter position-relative",
<%= tag.div class: "quick-filter",
data: {
controller: "dialog multi-selection-combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
+1 -1
View File
@@ -1,6 +1,6 @@
<% filter = user_filtering.filter %>
<%= tag.div class: "quick-filter position-relative",
<%= tag.div class: "quick-filter",
data: {
controller: "dialog multi-selection-combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
+1 -1
View File
@@ -1,6 +1,6 @@
<% filter = user_filtering.filter %>
<%= tag.div class: "quick-filter position-relative",
<%= tag.div class: "quick-filter",
data: {
controller: "dialog multi-selection-combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
@@ -1,6 +1,6 @@
<% filter = user_filtering.filter %>
<%= tag.div class: "quick-filter position-relative",
<%= tag.div class: "quick-filter",
data: {
controller: "dialog multi-selection-combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
@@ -1,6 +1,6 @@
<% filter = user_filtering.filter %>
<%= tag.div class: class_names("quick-filter position-relative"),
<%= tag.div class: class_names("quick-filter"),
data: { controller: "dialog combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
filter_show: user_filtering.show_indexed_by?,
@@ -1,6 +1,6 @@
<% filter = user_filtering.filter %>
<%= tag.div class: class_names("quick-filter position-relative"),
<%= tag.div class: class_names("quick-filter"),
data: {
controller: "dialog combobox",
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
@@ -1,5 +1,5 @@
<% filter = user_filtering.filter %>
<%= tag.div class: "quick-filter position-relative",
<%= tag.div class: "quick-filter",
data: { controller: "dialog", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", filter_show: filter.public_send("#{name}_window").present? } do %>
<button class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
+3 -8
View File
@@ -1,12 +1,7 @@
<% @page_title = "Join #{@account_name}" %>
<% @page_title = "Join #{@account_name} in Fizzy" %>
<div class="panel panel--centered flex flex-column gap-half">
<header>
<h1 class="txt-x-large font-weight-black margin-none">
<%= @page_title %>
</h1>
<p class="margin-none-block-start">Enter your email address to continue</p>
</header>
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<%= form_with url: join_path(code: params[:code], tenant: params[:tenant]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %>
<div class="flex align-center gap">
@@ -16,7 +11,7 @@
</div>
<button type="submit" id="log_in" class="btn btn--link center" data-form-target="submit">
<span>Join</span>
<span>Continue</span>
<%= icon_tag "arrow-right" %>
</button>
<% end %>
+1 -1
View File
@@ -5,7 +5,7 @@
<body data-controller="local-time timezone-cookie turbo-navigation" data-action="turbo:morph@window->local-time#refreshAll turbo:before-visit@document->turbo-navigation#rememberLocation">
<header class="header <%= @header_class %>" id="header">
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
<%= render "my/menus/menu" if Current.user %>
<%= render "my/menus/nav" if Current.user %>
<%= yield :header %>
</header>
+14 -12
View File
@@ -43,27 +43,29 @@
width: 100% !important;
}
.avatar {
border-radius: 50%;
height: 2.75em;
margin-right: 0.2.75em;
max-height: 2.75em;
min-height: 2.75em;
overflow: hidden;
width: 2.75em;
}
.avatar__container {
padding-top: 1em;
vertical-align: top;
width: 3.25em;
}
.avatar {
border-radius: 50%;
color: white;
display: block;
font-weight: 600;
height: 2.75em;
line-height: 2.75em;
mso-line-height-rule: exactly;
overflow: hidden;
text-align: center;
width: 2.75em;
}
.btn {
background: #2d71e5;
border-color: #2d71e5;
border-radius: 3rem;
color: #ffffff;
color: white !important;
font-weight: 500;
padding: 0.5em 1em;
text-decoration: none;
+5 -3
View File
@@ -1,16 +1,18 @@
<!DOCTYPE html>
<html>
<html lang="en">
<%= render "layouts/shared/head" %>
<body class="public" data-controller="local-time timezone-cookie" data-action="turbo:morph@window->local-time#refreshAll">
<header class="header" id="header">
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
<nav>
<%= link_to "https://box-car.com", class: "boxcar-header-logo center flex-inline align-center txt-normal" do %>
<%= link_to "https://fizzy.do", class: "header__logo center flex-inline align-center txt-large" do %>
<%= image_tag "logo.png" %>
<strong class="txt-medium overflow-ellipsis margin-none">Fizzy</strong>
<strong class="txt-large overflow-ellipsis margin-none">Fizzy</strong>
<% end %>
</nav>
<%= yield :header %>
</header>
<main id="main">
+1 -2
View File
@@ -1,8 +1,7 @@
<head>
<%= page_title_tag %>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<% unless @disable_view_transition %>
<meta name="view-transition" content="same-origin">
<% end %>
@@ -1,5 +1,4 @@
<h1 class="title">Confirm your email address change</h1>
<p class="subtitle">Hit the button below to use this email address in Fizzy.</p>
<p class="subtitle">Confirm your email address change</p>
<%= link_to "Yes use use this email address", email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token), class: "btn" %>
@@ -1,11 +1,13 @@
<h1 class="title">Sign in to Fizzy</h1>
<p class="subtitle">Hit the button below to sign in to Fizzy on this device</p>
<%= link_to "Sign in to Fizzy", session_magic_link_url(code: @magic_link.code), class: "btn" %>
<% if @identity.memberships.any? %>
<h1 class="title">Fizzy verification code</h1>
<p class="subtitle">Please enter this 6-character verification code on the Fizzy sign-in page:</p>
<% else %>
<h1 class="title">Welcome to Fizzy!</h1>
<p class="subtitle">Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:</p>
<% end %>
<p class="margin-block-start-double">If youre signing in on another device, enter this special code:
<br><strong class="txt-large"><%= @magic_link.code %></strong>
</p>
<strong class="txt-large"><%= @magic_link.code %></strong>
<p class="footer">Need help? <%= mail_to "support@37signals.com", "Email us"%>.
</p>
@@ -1,8 +1,7 @@
Sign in to Fizzy
<%= "=" * 80 %>
<% if @identity.memberships.any? %>
Please enter this 6-character verification code on the Fizzy sign-in page:
<% else %>
Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:
<% end %>
Open this link in your browser to login on this device:
<%= session_magic_link_url(code: @magic_link.code) %>
If you're signing in on another device, enter this code:
<%= @magic_link.code %>
@@ -1,8 +1,15 @@
<table class="notification">
<tr>
<td class="avatar__container"><%= image_tag user_avatar_url(notification.creator), alt: notification.creator.name, class: "avatar", size: 48 %></td>
<td>&nbsp;</td>
<td>
<%= render "notification/bundle_mailer/header", notification: notification %>
</td>
</tr>
<tr>
<td class="avatar__container">
<%= mail_avatar_tag(notification.creator) %>
</td>
<td class="what">
<%= render "notification/bundle_mailer/#{notification.source_type.underscore}/body", notification: notification %>
</td>
</tr>
@@ -13,7 +13,7 @@
<h1 class="txt-x-large font-weight-black margin-none">
<%= @page_title %>
</h1>
<p class="margin-none">Enter your new email address. We'll send you a confirmation to verify it.</p>
<p class="margin-none">Enter your new email address, then hit the link to confirm it</p>
</header>
<%= form_with url: email_addresses_path(membership_id: @membership.id), method: :post, class: "flex flex-column gap", data: { controller: "form", turbo: false } do |form| %>
@@ -23,8 +23,8 @@
</label>
</div>
<button type="submit" class="btn btn--reversed center" data-form-target="submit">
<span>Next</span>
<button type="submit" class="btn btn--link center" data-form-target="submit">
<span>Continue</span>
<%= icon_tag "arrow-right" %>
</button>
<% end %>
+1 -1
View File
@@ -1,4 +1,4 @@
<%= tag.button class:"boxcar-menu-trigger input input--select center flex-inline align-center txt-normal",
<%= tag.button class:"nav__trigger input input--select center flex-inline align-center txt-normal",
data: {
action: "click->dialog#open:stop keydown.j@document->hotkey#click keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
controller: "hotkey" } do %>
+1 -1
View File
@@ -1,3 +1,3 @@
<footer class="popup__footer">
<footer class="nav__footer">
Fizzy&trade; is designed, built, and backed by <strong class="txt-nowrap"><%= icon_tag "37signals", class: "v-align-middle" %> <a href="https://37signals.com" target="_blank" class="txt-ink">37signals&trade;</a></strong>
</footer>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="boxcar-menu__hotkeys margin-block-end-half" role="list">
<div class="nav__hotkeys margin-block-end-half" role="list">
<%= filter_hotkey_link "Home", root_path, 1, "home" %>
<%= filter_hotkey_link "New board", new_board_path, 2, "board-add" %>
<%= filter_hotkey_link "Assigned to me", cards_path(assignee_ids: [Current.user.id]), 3, "clipboard" %>
@@ -1,7 +1,7 @@
<nav data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<nav class="nav" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<%= render "my/menus/button" %>
<%= tag.dialog class: "boxcar-menu filter popup panel margin-block-start-half", data: {
<%= tag.dialog class: "nav__menu filter popup popup--animated panel margin-block-start-half", data: {
action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate filter:changed->navigable-list#reset filter:changed->nav-section-expander#showWhileFiltering toggle->filter#filter",
controller: "filter navigable-list nav-section-expander",
dialog_target: "dialog",
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="flex gap">
<%= jump_field_tag %>
<button class="boxcar-menu__close btn borderless txt-small" data-action="dialog#close">
<button class="nav__close btn borderless txt-small" data-action="dialog#close">
<%= icon_tag "close" %>
<span class="for-screen-reader">Close menu</span>
</button>
+3 -3
View File
@@ -13,13 +13,13 @@
<% end %>
<% content_for :header do %>
<h1 class="header__title">
<%= @page_title %>
<h1 class="header__title divider divider--fade full-width">
<span class="overflow-ellipsis"><%= @page_title %></span>
</h1>
<% end %>
<% if @board.public_description.present? %>
<div class="card__board-public-description txt-align-center center margin-block-end">
<div class="card__board-public-description rich-text-content txt-align-center center margin-block-end" data-controller="syntax-highlight">
<%= @board.public_description %>
</div>
<% end %>
@@ -2,10 +2,12 @@
data-collapsible-columns-target="column"
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle"
>
<div class="cards__transition-container">
<header class="cards__header">
<%= render "boards/show/expander", title: "Done", count: board.cards.closed.count, column_id: "closed-cards" %>
</header>
<%= column_frame_tag :closed_column, src: public_board_columns_closed_path(board.publication.key) %>
</div>
</section>
@@ -8,6 +8,7 @@
<header class="cards__header">
<%= render "boards/show/expander", title: column.name, count: column.cards.active.count, column_id: dom_id(column) %>
</header>
<%= column_frame_tag dom_id(column, :cards), src: public_board_column_path(column.board.publication.key, column) %>
</div>
</section>
@@ -2,10 +2,12 @@
data-collapsible-columns-target="column"
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle"
>
<div class="cards__transition-container">
<header class="cards__header">
<%= render "boards/show/expander", title: "Not Now", count: board.cards.postponed.count, column_id: "not-now" %>
</header>
<%= column_frame_tag :not_now_column, src: public_board_columns_not_now_path(board.publication.key) %>
</div>
</section>
+8 -6
View File
@@ -13,12 +13,14 @@
<% end %>
<% content_for :header do %>
<%= link_to published_board_url(@card.board), class: "header__title btn borderless txt-large", style: "--btn-padding: 0.25ch 1ch 0.25ch 0.75ch; view-transistion-name: card-board-title;", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">
&larr;
<strong class="font-black"><%= @card.board.name %></strong>
</span>
<% end %>
<div class="header__actions header__actions--start">
<%= link_to published_board_url(@card.board), class: "btn borderless txt-medium", style: "view-transistion-name: card-board-title;", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">
&larr;
<strong class="font-black"><%= @card.board.name %></strong>
</span>
<% end %>
</div>
<% end %>
<section id="<%= dom_id(@card, :card_container) %>"
+9 -5
View File
@@ -3,19 +3,23 @@
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] %>">
<header>
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<p class="margin-none-block-start txt-medium">We just emailed you a link to sign in.</p>
<p class="margin-none-block-start txt-medium">Then enter the verfifcation code included in the email below:</p>
</header>
<p class="margin-none-block-start margin-block-end-half txt-small txt-subtle">If your email is on a different device, enter the code included in the email below.</p>
<%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: token_list("form", "auto-submit" => params[:code].present?)} } do |form| %>
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-normal",
autofocus: false, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true,
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large",
autofocus: true, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true,
autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code],
data: { form_target: "input", action: "paste->form#debouncedSubmit" } %>
<% end %>
</div>
<% if Rails.env.development? %>
<script>
console.log("Magic link code: <%= flash[:magic_link_code].presence || "not generated" %>");
</script>
<% end %>
<% content_for :footer do %>
<%= render "sessions/footer" %>
<% end %>
+1 -1
View File
@@ -24,7 +24,7 @@
<p class="margin-none-block-start">You dont have any Fizzy accounts.</p>
<% end %>
<% if defined?(saas) %>
<% if signups_allowed? %>
<div class="margin-block-start">
<%= link_to saas.new_signup_membership_path, class: "btn btn--plain txt-link center txt-small", data: { turbo_prefetch: false } do %>
<span>Sign up for a new Fizzy account</span>
+8 -6
View File
@@ -1,17 +1,19 @@
<% @page_title = "Sign in" %>
<% @page_title = "Enter your email" %>
<div class="panel panel--centered flex flex-column gap-half">
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<div class="panel panel--centered flex flex-column gap-half" style="--panel-size: 65ch;">
<h1 class="txt-x-large font-weight-black margin-block-end">Lets get you in to Fizzy</h1>
<%= form_with url: session_path, class: "flex flex-column gap txt-medium" do |form| %>
<div class="flex align-center gap">
<label class="flex align-center gap input input--actor">
<%= form.email_field :email_address, required: true, class: "input full-width", autofocus: true, autocomplete: "username", placeholder: "Email address" %>
<%= form.email_field :email_address, required: true, class: "input txt-large full-width", autofocus: true, autocomplete: "username", placeholder: "Enter your email address" %>
</label>
</div>
<button type="submit" id="log_in" class="btn btn--link center">
<span>Continue</span>
<p>If youve already got an account well get you signed in. If youre new here, well set you up with your own Fizzy account.</p>
<button type="submit" id="log_in" class="btn btn--link center txt-medium">
<span>Lets go</span>
<%= icon_tag "arrow-right" %>
</button>
<% end %>
+4 -11
View File
@@ -1,24 +1,17 @@
<% @page_title = "Join #{Account.sole.name}" %>
<% @page_title = "Great! Now enter your name" %>
<div class="panel panel--centered flex flex-column gap-half">
<header>
<h1 class="txt-x-large font-weight-black margin-none">
<%= @page_title %>
</h1>
<p class="margin-none-block-start">Enter your name to continue</p>
</header>
<h1 class="txt-large font-weight-black margin-none"><%= @page_title %></h1>
<%= form_with scope: "user", url: users_joins_path(membership: params[:membership]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %>
<div class="flex align-center gap">
<label class="flex align-center gap input input--actor">
<%= form.text_field :name, class: "input full-width", autocomplete: "name", placeholder: "Name", autofocus: true, required: true, data: { "1p-ignore": true } %>
<%= icon_tag "person", class: "txt-large" %>
<%= form.text_field :name, class: "input full-width", autocomplete: "name", placeholder: "Full name", autofocus: true, required: true, data: { "1p-ignore": true } %>
</label>
</div>
<button type="submit" id="log_in" class="btn btn--link center" data-form-target="submit">
<span>Join</span>
<span>Continue</span>
<%= icon_tag "arrow-right" %>
</button>
<% end %>
+4 -2
View File
@@ -20,8 +20,10 @@ Rails.application.configure do
# key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true
# Disable serving static files from `public/`, relying on NGINX/Apache to do so instead.
# config.public_file_server.enabled = false
config.public_file_server.enabled = true
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{1.year.to_i}"
}
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com"
@@ -0,0 +1,5 @@
class AddUserAndCardIndexToWatches < ActiveRecord::Migration[8.2]
def change
add_index :watches, %i[ user_id card_id ]
end
end
Generated
+2 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.2].define(version: 2025_11_05_122933) do
ActiveRecord::Schema[8.2].define(version: 2025_11_06_154151) do
create_table "accesses", force: :cascade do |t|
t.datetime "accessed_at"
t.integer "board_id", null: false
@@ -404,6 +404,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_05_122933) do
t.integer "user_id", null: false
t.boolean "watching", default: true, null: false
t.index ["card_id"], name: "index_watches_on_card_id"
t.index ["user_id", "card_id"], name: "index_watches_on_user_id_and_card_id"
t.index ["user_id"], name: "index_watches_on_user_id"
end
+18 -1
View File
@@ -2760,6 +2760,23 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: watches
name: index_watches_on_user_id_and_card_id
unique: false
columns:
- user_id
- card_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
webhook_delinquency_trackers:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: webhook_delinquency_trackers
@@ -2843,4 +2860,4 @@ indexes:
nulls_not_distinct:
comment:
valid: true
version: 20251105122933
version: 20251106154151
@@ -1,11 +0,0 @@
module Restricted
extend ActiveSupport::Concern
included do
unless Rails.env.development?
http_basic_authenticate_with \
name: Rails.env.test? ? "testname" : Rails.application.credentials.account_signup_http_basic_auth.name,
password: Rails.env.test? ? "testpassword" : Rails.application.credentials.account_signup_http_basic_auth.password
end
end
end
@@ -1,6 +1,4 @@
class Signups::CompletionsController < ApplicationController
include Restricted
class Signup::CompletionsController < ApplicationController
require_untenanted_access
layout "public"
@@ -1,6 +1,4 @@
class Signups::MembershipsController < ApplicationController
include Restricted
class Signup::MembershipsController < ApplicationController
require_untenanted_access
layout "public"
@@ -1,33 +0,0 @@
class SignupsController < ApplicationController
include Restricted
require_untenanted_access
require_unauthenticated_access
layout "public"
rate_limit only: :create, name: "short-term", to: 5, within: 3.minutes,
with: -> { redirect_to saas.new_signup_path, alert: "Try again later." }
rate_limit only: :create, name: "long-term", to: 10, within: 30.minutes,
with: -> { redirect_to saas.new_signup_path, alert: "Try again later." }
def new
@signup = Signup.new
end
def create
@signup = Signup.new(signup_params)
if @signup.create_identity
session[:return_to_after_authenticating] = saas.new_signup_membership_path
redirect_to session_magic_link_path
else
render :new, status: :unprocessable_entity
end
end
private
def signup_params
params.expect(signup: %i[ email_address ])
end
end
-6
View File
@@ -8,10 +8,6 @@ class Signup
attr_accessor :full_name, :email_address, :identity, :membership_id, :account_name
attr_reader :queenbee_account, :account, :user, :tenant, :membership
with_options on: :identity_creation do
validates_presence_of :email_address
end
with_options on: :membership_creation do
validates_presence_of :full_name, :identity
end
@@ -42,8 +38,6 @@ class Signup
end
def create_identity
return false unless valid?(:identity_creation)
@identity = Identity.find_or_create_by!(email_address: email_address)
@identity.send_magic_link
end
@@ -1,7 +1,7 @@
<% @page_title = "Creating your account" %>
<% @page_title = "Creating your account" %>
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] %>">
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<h1 class="txt-x-large font-weight-black margin-block-end"><%= @page_title %></h1>
<%= form_with model: @signup, url: saas.signup_completion_url(script_name: "/#{@signup.tenant}"), scope: "signup", class: "flex flex-column gap", data: { turbo: false, controller: @signup.errors.blank? ? "form auto-submit" : "form" } do |form| %>
<%= form.hidden_field :membership_id %>
@@ -19,8 +19,7 @@
<% end %>
<button type="submit" class="btn btn--link center" data-form-target="submit">
<span>Continue</span>
<%= icon_tag "arrow-right" %>
<span>Done</span>
</button>
<% end %>
</div>
@@ -1,12 +1,15 @@
<% @page_title = "Create your account" %>
<% @page_title = "Complete your sign-up" %>
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] %>">
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<h1 class="txt-x-large font-weight-black margin-block-end"><%= @page_title %></h1>
<%= form_with model: @signup, url: saas.signup_membership_path, scope: "signup", class: "flex flex-column gap", data: { controller: "form" } do |form| %>
<%= form.hidden_field :new_user %>
<%= form.text_field :full_name, class: "input", autocomplete: "name", placeholder: "Your name", autofocus: true, required: true %>
<%= form.text_field :full_name, class: "input txt-large", autocomplete: "name", placeholder: "Enter your full name", autofocus: true, required: true %>
<p>Youre one step away. Just enter your name to get your own Fizzy account.</p>
<% if @signup.errors.any? %>
<div class="margin-block-half">
+5 -7
View File
@@ -1,11 +1,9 @@
Fizzy::Saas::Engine.routes.draw do
resource :signup, only: %i[ new create ] do
scope module: :signups, as: :signup do
collection do
resource :membership, only: %i[ new create ]
resource :completion, only: %i[ new create ]
end
end
get "/signup/new", to: redirect("/session/new")
namespace :signup do
resource :membership, only: %i[ new create ]
resource :completion, only: %i[ new create ]
end
Queenbee.routes(self)
@@ -1,6 +1,6 @@
require "test_helper"
class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest
class Signup::CompletionsControllerTest < ActionDispatch::IntegrationTest
setup do
@signup = Signup.new(email_address: "newuser@example.com", full_name: "New User")
@@ -16,7 +16,7 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest
get saas.new_signup_completion_path(signup: {
membership_id: @signup.membership_id,
full_name: @signup.full_name,
account_name: @signup.account_name }), headers: http_basic_auth_headers
account_name: @signup.account_name })
end
assert_response :success
@@ -30,7 +30,7 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest
full_name: @signup.full_name,
account_name: @signup.account_name
}
}, headers: http_basic_auth_headers
}
end
assert_redirected_to landing_path(script_name: "/#{@signup.tenant}"), "Successful completion should redirect to root in new tenant"
@@ -42,14 +42,9 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest
full_name: "",
account_name: ""
}
}, headers: http_basic_auth_headers
}
end
assert_response :unprocessable_entity, "Invalid params should return unprocessable entity"
end
private
def http_basic_auth_headers
{ "Authorization" => ActionController::HttpAuthentication::Basic.encode_credentials("testname", "testpassword") }
end
end
@@ -1,6 +1,6 @@
require "test_helper"
class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest
class Signup::MembershipsControllerTest < ActionDispatch::IntegrationTest
setup do
@identity = Identity.create!(email_address: "newuser@example.com")
magic_link = @identity.send_magic_link
@@ -16,7 +16,7 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest
test "new" do
untenanted do
get saas.new_signup_membership_path, headers: http_basic_auth_headers
get saas.new_signup_membership_path
assert_response :success
end
@@ -24,7 +24,7 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest
test "new with new_user param" do
untenanted do
get saas.new_signup_membership_path(signup: { new_user: true }), headers: http_basic_auth_headers
get saas.new_signup_membership_path(signup: { new_user: true })
assert_response :success
end
@@ -37,7 +37,7 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest
signup: {
full_name: "New User"
}
}, headers: http_basic_auth_headers
}
end
membership = Membership.last
@@ -58,15 +58,10 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest
signup: {
full_name: ""
}
}, headers: http_basic_auth_headers
}
end
assert_response :unprocessable_entity, "Invalid params should return unprocessable entity"
end
end
private
def http_basic_auth_headers
{ "Authorization" => ActionController::HttpAuthentication::Basic.encode_credentials("testname", "testpassword") }
end
end
@@ -1,54 +0,0 @@
require "test_helper"
class SignupsControllerTest < ActionDispatch::IntegrationTest
setup do
@signup_params = {
full_name: "Brian Wilson",
email_address: "brian@example.com",
company_name: "Beach Boys"
}
@starting_tenants = ApplicationRecord.tenants
# Clear script_name for untenanted signup tests
integration_session.default_url_options[:script_name] = nil
end
test "should require http basic authentication" do
get saas.new_signup_url
assert_response :unauthorized
end
test "should get new" do
get saas.new_signup_url, headers: http_basic_auth_headers
assert_response :success
assert_select "h1", "Sign up"
assert_select "input[name='signup[email_address]']"
end
test "should create signup and redirect to magic link page" do
assert_no_difference -> { ApplicationRecord.tenants.count } do
post saas.signup_url, params: { signup: { email_address: @signup_params[:email_address] } }, headers: http_basic_auth_headers
end
assert_redirected_to session_magic_link_path
end
test "should render new with errors when signup fails validation" do
invalid_params = { email_address: "" }
assert_no_difference -> { ApplicationRecord.tenants.count } do
post saas.signup_url, params: { signup: invalid_params }, headers: http_basic_auth_headers
end
assert_response :unprocessable_entity
assert_select ".txt-negative"
end
private
def http_basic_auth_headers
credentials = ActionController::HttpAuthentication::Basic.encode_credentials("testname", "testpassword")
{ "HTTP_AUTHORIZATION" => credentials }
end
end
+3 -3
View File
@@ -27,8 +27,9 @@ class SignupTest < ActiveSupport::TestCase
end
signup_invalid = Signup.new(email_address: "")
assert_not signup_invalid.create_identity, "Should fail with invalid email"
assert_not_empty signup_invalid.errors[:email_address], "Should have validation error for email_address"
assert_raises do
signup_invalid.create_identity
end
end
test "#create_membership" do
@@ -56,7 +57,6 @@ class SignupTest < ActiveSupport::TestCase
test "#complete" do
Account.any_instance.expects(:setup_customer_template).once
# First create the membership
signup_for_membership = Signup.new(
full_name: "Kevin",
identity: identities(:kevin)
+41 -28
View File
@@ -1,10 +1,46 @@
require "test_helper"
class SessionsControllerTest < ActionDispatch::IntegrationTest
test "destroy" do
test "new" do
untenanted do
sign_in_as :kevin
get new_session_path
end
assert_response :success
end
test "create" do
identity = identities(:kevin)
untenanted do
assert_difference -> { MagicLink.count }, 1 do
post session_path, params: { email_address: identity.email_address }
end
assert_redirected_to session_magic_link_path
end
end
unless Bootstrap.oss_config?
test "create for a new user" do
untenanted do
assert_difference -> { Identity.count }, +1 do
assert_difference -> { MagicLink.count }, +1 do
post session_path,
params: { email_address: "nonexistent-#{SecureRandom.hex(6)}@example.com" },
headers: http_basic_auth_headers("testname", "testpassword")
end
end
assert_redirected_to session_magic_link_path
end
end
end
test "destroy" do
sign_in_as :kevin
untenanted do
delete session_path
assert_redirected_to new_session_path
@@ -12,31 +48,8 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
end
end
test "new" do
untenanted do
get new_session_path
assert_response :success
private
def http_basic_auth_headers(user, password)
{ "Authorization" => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) }
end
end
test "create" do
untenanted do
identity = identities(:kevin)
assert_difference -> { MagicLink.count }, 1 do
post session_path, params: { email_address: identity.email_address }
end
assert_redirected_to session_magic_link_path
end
untenanted do
assert_no_difference -> { MagicLink.count } do
post session_path, params: { email_address: "nonexistent@example.com" }
end
assert_redirected_to session_magic_link_path
end
end
end
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

+1 -1
View File
@@ -10,7 +10,7 @@ class MagicLinkMailerTest < ActionMailer::TestCase
end
assert_equal [ "kevin@37signals.com" ], email.to
assert_equal "Sign in to Fizzy", email.subject
assert_equal "Your Fizzy verification code", email.subject
assert_match magic_link.code, email.body.encoded
end
end
@@ -0,0 +1,44 @@
require "test_helper"
class Notification::BundleMailerTest < ActionMailer::TestCase
setup do
@user = users(:david)
@bundle = Notification::Bundle.create!(
user: @user,
starts_at: 1.hour.ago,
ends_at: 1.hour.from_now
)
end
test "renders avatar with initials in span when avatar is not attached" do
create_notification(@user)
email = Notification::BundleMailer.notification(@bundle)
assert_match /<span[^>]*class="avatar"[^>]*>/, email.html_part.body.to_s
assert_match /#{@user.initials}/, email.html_part.body.to_s
assert_match /style="background-color: #[A-F0-9]{6};?"/, email.html_part.body.to_s
end
test "renders avatar with external image URL when avatar is attached" do
@user.avatar.attach(
io: File.open(Rails.root.join("test", "fixtures", "files", "avatar.png")),
filename: "avatar.png",
content_type: "image/png"
)
create_notification(@user)
email = Notification::BundleMailer.notification(@bundle)
assert_match /<img[^>]*class="avatar"[^>]*>/, email.html_part.body.to_s
assert_match /<img[^>]*class="avatar"[^>]*src="[^"]*"/, email.html_part.body.to_s
assert_match /alt="#{@user.name}"/, email.html_part.body.to_s
end
private
def create_notification(user)
Notification.create!(user: user, creator: user, source: events(:logo_published), created_at: 30.minutes.ago)
end
end
@@ -0,0 +1,17 @@
class IdentityMailerPreview < ActionMailer::Preview
def email_change_confirmation
ApplicationRecord.current_tenant = "897362094"
identity = Identity.find_by(email_address: "david@37signals.com")
membership = identity&.memberships&.find_by(tenant: ApplicationRecord.current_tenant)
new_email_address = "david.new@example.com"
token = membership.send(:generate_email_address_change_token, to: new_email_address)
IdentityMailer.email_change_confirmation(
email_address: new_email_address,
token: token,
membership: membership
)
end
end
@@ -1,6 +1,5 @@
class Notification::BundleMailerPreview < ActionMailer::Preview
def notification
ApplicationRecord.current_tenant = "1065895976"
Notification::BundleMailer.notification Notification::Bundle.take!
end
end
@@ -1,18 +0,0 @@
class UserMailerPreview < ActionMailer::Preview
def email_change_confirmation
ApplicationRecord.current_tenant = "897362094"
user = User.find_by(email_address: "david@37signals.com") || User.new(
name: "David",
email_address: "david@37signals.com"
)
new_email_address = "david.new@example.com"
token = user.generate_email_address_change_token(to: new_email_address)
UserMailer.email_change_confirmation(
user: user,
email_address: new_email_address,
token: token
)
end
end
+16
View File
@@ -64,4 +64,20 @@ class AccessTest < ActiveSupport::TestCase
assert_empty remaining_mentions
end
test "watches are destroyed when access is lost" do
kevin = users(:kevin)
board = boards(:writebook)
card = board.cards.first
assert card.watched_by?(kevin)
kevin_access = accesses(:writebook_kevin)
perform_enqueued_jobs only: Board::CleanInaccessibleDataJob do
kevin_access.destroy
end
assert_not card.watched_by?(kevin)
end
end
+2
View File
@@ -38,4 +38,6 @@ class Board::AccessibleTest < ActiveSupport::TestCase
boards(:writebook).access_for(users(:kevin)).access_only!
assert_not_includes boards(:writebook).reload.watchers, users(:kevin)
end
# NOTE: The tests for clearing inaccessible data are in +AccessTest+
end