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:
+1
-1
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -498,5 +498,9 @@
|
||||
margin-inline: auto;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
code {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
.boxcar-menu,
|
||||
.boxcar-menu-trigger,
|
||||
.nav__menu,
|
||||
.nav__trigger,
|
||||
.header__actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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,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,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: "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">
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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 you’re 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> </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,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,3 +1,3 @@
|
||||
<footer class="popup__footer">
|
||||
<footer class="nav__footer">
|
||||
Fizzy™ 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™</a></strong>
|
||||
</footer>
|
||||
|
||||
@@ -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",
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
←
|
||||
<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">
|
||||
←
|
||||
<strong class="font-black"><%= @card.board.name %></strong>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<section id="<%= dom_id(@card, :card_container) %>"
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<p class="margin-none-block-start">You don’t 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>
|
||||
|
||||
@@ -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">Let’s 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 you’ve already got an account we’ll get you signed in. If you’re new here, we’ll set you up with your own Fizzy account.</p>
|
||||
|
||||
<button type="submit" id="log_in" class="btn btn--link center txt-medium">
|
||||
<span>Let’s go</span>
|
||||
<%= icon_tag "arrow-right" %>
|
||||
</button>
|
||||
<% end %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
-3
@@ -1,6 +1,4 @@
|
||||
class Signups::CompletionsController < ApplicationController
|
||||
include Restricted
|
||||
|
||||
class Signup::CompletionsController < ApplicationController
|
||||
require_untenanted_access
|
||||
|
||||
layout "public"
|
||||
+1
-3
@@ -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
|
||||
@@ -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
|
||||
|
||||
+3
-4
@@ -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>
|
||||
+6
-3
@@ -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>You’re one step away. Just enter your name to get your own Fizzy account.</p>
|
||||
|
||||
|
||||
<% if @signup.errors.any? %>
|
||||
<div class="margin-block-half">
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 339 KiB |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user