Merge branch 'main' into mobile/prepare-webviews-2

* main: (73 commits)
  Fix that caching should be disabled in dev by default. References 3cf841d463.
  Revert "Add CJK (Chinese, Japanese, Korean) search support"
  Account for new button sizes on mobile
  Bump filter z-index when filters are open
  Use a dedicated resource for showing cards in draft mode
  Refactor popup.css styles for focus state
  Add Segoe UI Variable Fizzy font face configuration comments
  Update @layer name to `base` in font-face definition for "Segoe UI Variable Fizzy"
  Repair scope for card styles within the columns view
  Add missing list to cards page
  Account for simpler public views
  Fix public layout
  Pull board-tools outside of the list for mobile
  saas: move log_level setting into an environment variable
  Restore log level configurability in production environment
  Remove engagements
  Go back to board after clearing filters
  Ensure filters sit on top of cards
  Fix public boards
  Fix card grid layout
  ...
This commit is contained in:
Adrien Maston
2026-01-09 10:58:21 +01:00
83 changed files with 666 additions and 455 deletions
+2 -2
View File
@@ -16,7 +16,7 @@
--block-space-double: calc(var(--block-space) * 2);
/* Text */
--font-sans: "Adwaita Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
--font-sans: "Adwaita Sans", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Fizzy", "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
--font-serif: ui-serif, serif;
--font-mono: ui-monospace, monospace;
@@ -213,7 +213,7 @@
--color-terminal-text: var(--color-ink);
--color-terminal-text-light: var(--color-ink-lighter);
--color-golden: oklch(89.1% 0.178 95.7);
--color-considering: oklch(var(--lch-blue-medium));
--color-maybe: oklch(var(--lch-blue-medium));
/* Colors: Cards */
--color-card-default: oklch(var(--lch-blue-dark));
+2 -1
View File
@@ -102,7 +102,8 @@
}
/* Turbo */
turbo-frame {
turbo-frame,
turbo-cable-stream-source {
display: contents;
}
+1 -1
View File
@@ -11,7 +11,7 @@
white-space: nowrap;
}
.cards--considering & {
.cards--maybe & {
background-color: var(--card-bg-color) !important;
}
+176 -162
View File
@@ -1,4 +1,33 @@
@layer components {
/* Layout adjustments for contained scrolling
/* ------------------------------------------------------------------------ */
/* Scroll columns individually on mobile */
@media (max-width: 639px) {
body.contained-scrolling {
block-size: 100dvh;
grid-template-rows: 1fr var(--footer-height);
#global-container {
display: grid;
grid-template-rows: auto 1fr;
overflow: hidden;
}
#main {
display: grid;
grid-template-rows: auto auto 1fr;
overflow: auto;
padding: 0;
}
/* Adapt the grid to public views (no filters or watchers sections) */
&.public #main {
grid-template-rows: 1fr;
}
}
}
/* Column container
/* ------------------------------------------------------------------------ */
@@ -10,7 +39,7 @@
--bubble-size: 3.5rem;
--cards-gap: min(1.2cqi, 1.7rem);
--column-gap: 8px;
--column-padding-expanded: calc(var(--column-gap) * 2);
--column-padding: calc(var(--column-gap) * 2);
--column-transition-duration: 300ms;
--column-width-collapsed: 40px;
--column-width-expanded: 450px;
@@ -21,17 +50,22 @@
container-type: inline-size;
display: grid;
gap: var(--column-gap);
grid-template-columns: 1fr var(--column-width-expanded) 1fr;
grid-template-columns: 1fr auto 1fr;
inline-size: 100%;
margin-inline: auto;
max-inline-size: var(--main-width);
outline: none;
overflow-x: auto;
overflow-y: hidden;
padding-block-end: var(--column-width-collapsed);
position: relative;
/* When it has something expanded */
&:has(.card-columns__left .cards:not(.is-collapsed), .card-columns__right .cards:not(.is-collapsed)) {
grid-template-columns: auto var(--column-width-expanded) auto;
&:has(.card-columns__left .is-expanded, .card-columns__right .is-expanded) {
grid-template-columns: auto auto auto;
@media (min-width: 640px) {
grid-template-columns: auto var(--column-width-expanded) auto;
}
}
&:has(.cards) {
@@ -39,11 +73,19 @@
min-block-size: 20lh;
}
body:not(.public) & {
@media (max-width: 519px) {
display: none;
@media (max-width: 639px) {
--column-width-expanded: calc(100vw - var(--column-gap) * 4);
scroll-snap-type: inline mandatory;
&:not(:has(.is-expanded)) {
grid-template-columns: auto var(--column-width-collapsed) auto;
}
}
@media (min-width: 640px) {
padding-block-end: var(--column-width-collapsed);
}
}
.card-columns__left,
@@ -51,16 +93,27 @@
align-items: stretch;
display: flex;
gap: var(--column-gap);
position: relative;
@media (max-width: 639px) {
min-block-size: 0;
}
}
.card-columns__left {
justify-content: end;
margin-inline-start: auto;
padding-inline-start: var(--column-gap);
@media (max-width: 639px) {
padding-inline-start: calc(var(--column-gap) * 2);
}
}
.card-columns__right {
justify-content: start;
padding-inline-end: var(--column-gap);
margin-inline-end: auto;
}
/* Column
@@ -72,7 +125,13 @@
inline-size: var(--column-width-expanded);
outline: none;
position: relative;
scroll-snap-align: start;
scroll-snap-align: center;
&.is-expanded {
@media (max-width: 639px) {
overflow: hidden;
}
}
&.is-collapsed {
inline-size: var(--column-width-collapsed);
@@ -126,15 +185,17 @@
.cards__transition-container {
block-size: 100%;
border-radius: calc(var(--column-width-collapsed) / 2);
translate: 0;
translate: 0 0.5ch; /* Allow a little room for the mini bubble */
transition: translate var(--column-transition-duration) var(--ease-out-overshoot-subtle);
.is-collapsed:not(.cards--considering) & {
translate: 0 var(--column-width-collapsed);
}
@media (min-width: 640px) {
.is-expanded & {
translate: 0 0.5ch; /* Allow a little room for the mini bubble */
}
.cards:not(.is-collapsed) & {
padding-inline: var(--column-padding-expanded);
.is-collapsed & {
translate: 0 var(--column-width-collapsed);
}
}
.drag-and-drop__hover-container & {
@@ -151,29 +212,33 @@
.no-transitions & {
transition: none;
}
/* Use flex so the __list container can take up the remaining space for scrolling */
@media (max-width: 639px) {
.is-expanded & {
display: flex;
flex-direction: column;
}
}
}
/* The wrapper around the cards used to clip overflow while transitioning.
* Also, don't resize cards while transitioning to avoid reflow. */
.cards__list {
align-items: flex-end; /* use flex-start to wipe from left */
display: flex;
flex-direction: column;
gap: var(--cards-gap);
margin-block-start: -1ch;
margin-inline: -1ch;
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
.cards:not(.is-collapsed) & {
padding: 1ch;
}
.is-expanded & {
padding: var(--column-padding);
.card {
inline-size: calc(var(--column-width-expanded) - var(--column-padding-expanded) * 2);
}
.cards--grid & {
display: contents;
/* Use the rest of the column height for scrolling */
@media (max-width: 639px) {
flex: 1;
padding-inline: calc(var(--column-padding) / 4);
}
}
[aria-selected] & .card[aria-selected] {
@@ -193,7 +258,17 @@
}
.cards__new-column {
margin-block-start: var(--column-width-collapsed);
position: relative;
@media (max-width: 639px) {
inset-inline-end: 0;
position: absolute;
translate: 100%;
}
@media (min-width: 640px) {
margin-block-start: var(--column-width-collapsed);
}
}
/* Cards grid; used when filtering
@@ -204,11 +279,7 @@
--card-grid-columns: 1;
container-type: inline-size;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--cards-gap);
justify-content: center;
inline-size: 100%;
margin-inline: auto;
max-inline-size: var(--main-width);
@@ -220,6 +291,15 @@
--card-grid-columns: 3;
}
.cards__list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--cards-gap);
justify-content: center;
padding: 1ch;
}
.card {
inline-size: calc((100% - var(--cards-gap) * (var(--card-grid-columns) - 1) ) / var(--card-grid-columns));
}
@@ -245,11 +325,11 @@
block-size: 100%;
}
.cards:not(.is-collapsed) & {
.cards.is-expanded & {
display: grid;
grid-template-areas: "menu expander maximize";
grid-template-columns: var(--column-width-collapsed) 1fr var(--column-width-collapsed);
margin-block-end: calc(0.5 * var(--cards-gap));
padding-inline: var(--column-padding);
}
}
@@ -292,12 +372,18 @@
font-weight: 600;
gap: 0.5ch;
grid-area: expander;
justify-content: center;
outline: none;
outline-offset: -2px;
position: relative;
text-transform: uppercase;
&[disabled] {
opacity: 1;
}
@media (any-hover: hover) {
&:hover {
.is-collapsed:hover {
filter: brightness(0.9);
}
}
@@ -338,7 +424,7 @@
block-size: 100%;
content: "";
inline-size: 1px;
inset: 0 auto;
inset-block: calc(var(--column-width-collapsed) + var(--card-count) * var(--progress-increment)) 0;
position: absolute;
z-index: -2;
}
@@ -351,22 +437,17 @@
}
}
.cards:not(.is-collapsed) & {
.cards.is-expanded & {
inline-size: 100%;
justify-content: center;
}
.cards:is(.cards--considering) &:hover {
cursor: unset;
filter: none;
}
}
.cards__expander-count {
line-height: var(--column-width-collapsed);
inline-size: var(--column-width-collapsed);
.cards:not(.is-collapsed) & {
.cards.is-expanded & {
display: none;
}
}
@@ -384,7 +465,7 @@
writing-mode: vertical-rl;
}
.cards:not(.is-collapsed, .cards--considering) & {
.cards.is-expanded & {
align-items: center;
display: flex;
gap: 0.25ch;
@@ -394,74 +475,40 @@
.icon--collapse {
--icon-size: 1.15em;
opacity: 0;
scale: 1.5;
opacity: 0.66;
transition: 150ms ease-out;
transition-property: opacity, scale;
@media (min-width: 640px) {
opacity: 0;
scale: 1.5;
}
.cards.is-collapsed & {
display: none;
}
.cards:not(.is-collapsed) .cards__expander:hover & {
.cards.is-expanded .cards__expander:hover & {
opacity: 0.66;
scale: 1;
}
}
}
/* TODO: I think this is legacy now? */
.cards__heading {
display: flex;
font-size: var(--text-medium);
inset-block-start: 0;
justify-content: center;
margin-block-start: 1px;
margin-inline: -8px; /* enough to cover card shadows, but avoid overlapping bubbles */
padding-block: var(--cards-gap) 0;
position: sticky;
z-index: 3;
/* Cover bubbles and cards on small viewports */
@media (max-width: 639px) {
margin-inline: calc(-1 * var(--cards-gap) - var(--main-padding));
}
&, .cards__filter {
font-weight: bold;
text-transform: uppercase;
}
.cards__filter {
--filter-block-padding: 0.5rem;
background-color: var(--color-canvas);
border-radius: 99rem;
inline-size: auto;
margin-block: calc(-1 * var(--filter-block-padding));
padding-block: var(--filter-block-padding);
}
dialog {
font-weight: normal;
text-transform: none;
}
}
/* Override card styles within columns
/* Adding .board-tools here since it sits outside the cards container on mobile */
/* ------------------------------------------------------------------------ */
.cards {
.card {
--block-space: 1em;
--block-space-half: 0.5em;
--card-padding-inline: 1em;
--text-xx-large: 1.6em;
--text-x-small: 1em;
.cards .card,
.board-tools {
--block-space: 1em;
--block-space-half: 0.5em;
--card-padding-inline: 1em;
--text-xx-large: 1.6em;
--text-x-small: 1em;
/* Set lower limit for font size */
font-size: clamp(0.6rem, 0.85cqi, 100px);
}
/* Set lower limit for font size */
font-size: clamp(0.6rem, 0.85cqi, 100px);
.card__comments {
--column-gap: 0.8ch;
@@ -544,7 +591,7 @@
/* Considering
/* ------------------------------------------------------------------------ */
.cards--considering {
.cards--maybe {
--card-color: oklch(var(--lch-blue-medium));
position: relative;
@@ -598,15 +645,37 @@
--card-padding-block: var(--block-space);
border: 1px solid var(--border-color);
margin-block-end: var(--cards-gap);
inline-size: auto;
text-align: center;
@media (max-width: 639px) {
/* On mobile, hide the tool card inside the Maybe column */
.cards & {
display: none;
}
#cards_container > & {
margin: 0 3ch 1ch;
}
}
@media (min-width: 640px) {
/* On desktop, hide the tool card above the columns */
#cards_container > & {
display: none;
}
}
@media (min-width: 800px) {
margin: var(--column-padding) var(--column-padding) 0;
}
&:has(dialog[open]) {
z-index: 5;
}
.separator--horizontal {
margin-block: var(--block-space);
.divider {
--divider-color: oklch(var(--lch-blue-light));
}
.btn--link {
@@ -620,7 +689,7 @@
footer {
font-size: var(--text-x-small);
margin-block-end: calc(var(--block-space-half) * -1);
margin-block: 1ch calc(var(--block-space-half) * -1);
}
.overflow-count {
@@ -671,18 +740,12 @@
}
}
/* Closed (Done)
/* ------------------------------------------------------------------------ */
.cards--closed {
}
/* Doing
/* -------------------------------------------------------------------------- */
/* Surface a mini bubble if there are cards with bubbles inside */
.cards--considering:has(.bubble:not([hidden])) .cards__expander-title,
.cards--maybe:has(.bubble:not([hidden])) .cards__expander-title,
.cards--maybe.is-collapsed:has(.bubble:not([hidden])) .cards__transition-container,
.cards--doing.is-collapsed:has(.bubble:not([hidden])) .cards__transition-container {
--bubble-color: var(--card-color, oklch(var(--lch-blue-medium)));
--bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%;
@@ -703,13 +766,14 @@
}
/* Maybe column: position bubble relative to the title, not the container */
.cards--considering & {
.cards--maybe.is-expanded & {
overflow: visible;
position: relative;
&:before {
inset-block-start: 50%;
translate: 125% -75%;
inset-inline-start: 0;
translate: -125% -75%;
z-index: -1;
}
}
@@ -749,54 +813,4 @@
}
}
}
/* Mobile columns
/* -------------------------------------------------------------------------- */
.mobile-card-columns {
--column-gap: 8px;
--column-padding-expanded: calc(var(--column-gap) * 2);
--column-width-collapsed: 40px;
--progress-increment: var(--progress-max-width) / var(--progress-max-cards);
--progress-max-cards: 15; /* should match first geared pagination page size */
--progress-max-width: 100%;
padding-inline: 3vw;
/* Hide on larger devices with cursors */
@media (min-width: 520px) {
display: none;
}
.cards.is-collapsed {
inline-size: auto;
display: block;
padding-block: 0.5ch;
.cards__expander {
--gradient-direction: to right;
flex-direction: row;
inline-size: auto;
&:before {
block-size: 1px;
inline-size: 100%;
inset: 50% 0 auto;
translate: 0 -50%;
}
&:after {
block-size: var(--column-width-collapsed);
inline-size: calc(var(--column-width-collapsed) + var(--card-count) * var(--progress-increment));
margin-inline-start: 0;
max-inline-size: var(--progress-max-width);
}
}
.cards__expander-title {
writing-mode: revert;
}
}
}
}
+5 -1
View File
@@ -4,7 +4,7 @@
--comment-padding-block: var(--block-space-half);
--comment-padding-inline: var(--inline-space-double);
--comment-max: 70ch;
--reaction-size: 1.6875rem;
--reaction-size: 2rem;
display: flex;
flex-direction: column;
@@ -15,6 +15,10 @@
@media (min-width: 160ch) {
padding-inline: var(--tray-size);
}
@media (min-width: 640px) {
--reaction-size: 1.6875rem;
}
}
.comments__subscribers {
-4
View File
@@ -15,9 +15,5 @@
outline-offset: -2px;
transition: background-color 200ms;
z-index: 1;
.cards__heading {
background-color: transparent;
}
}
}
+4 -2
View File
@@ -7,17 +7,19 @@
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 1ch;
gap: var(--inline-space-half);
justify-content: center;
padding-block-start: 2px; /* prevents input focus-ring clipping on mobile */
position: relative;
view-transition-name: "filters";
view-transition-name: filters;
z-index: 1;
.btn {
--btn-border-color: var(--color-ink-medium);
--input-background: var(--color-canvas);
}
&:has(dialog[open]),
&:has([data-controller~="tooltip"]:hover) {
z-index: calc(var(--z-nav) + 1);
}
+41
View File
@@ -0,0 +1,41 @@
@layer base {
/*
Segoe UI Variable Fizzy font face configuration.
1. Segoe UI Variable (Weights 100-700):
Leverages variable font features to:
- Automatically adjust Weight (wght) dynamically within the 100-700 range.
- Automatically manage Optical Size (opsz) based on font-size.
2. Segoe UI Black (Weights 800-900):
Used as a fallback because Segoe UI Variable does not natively support 900 weight.
This ensures a consistent bold experience across all weights.
*/
@font-face {
font-family: "Segoe UI Variable Fizzy";
src: local("Segoe UI Variable");
font-weight: 100 700;
font-style: normal;
}
@font-face {
font-family: "Segoe UI Variable Fizzy";
src: local("Segoe UI Variable");
font-weight: 100 700;
font-style: italic;
}
@font-face {
font-family: "Segoe UI Variable Fizzy";
src: local("Segoe UI Black");
font-weight: 800 900;
font-style: normal;
}
@font-face {
font-family: "Segoe UI Variable Fizzy";
src: local("Segoe UI Black Italic");
font-weight: 800 900;
font-style: italic;
}
}
+10
View File
@@ -8,6 +8,16 @@
}
}
/* Required for the card column page on mobile, but not needed otherwise */
:where(#global-container) {
display: contents;
}
:where(#header) {
position: relative;
z-index: var(--z-nav);
}
:where(#main) {
inline-size: 100dvw;
margin-inline: auto;
+1 -1
View File
@@ -108,7 +108,7 @@
}
.notifications__status {
--panel-border-radius: 0.5em;
--panel-padding: var(--block-space);
}
}
+4
View File
@@ -155,6 +155,10 @@
max-inline-size: 100%;
padding: var(--inline-space-half) var(--popup-item-padding-inline);
text-align: start;
&:focus-visible {
z-index: 1;
}
}
.popup__icon {
+2 -2
View File
@@ -126,7 +126,7 @@
.card--new,
.cards__decoration,
.card-columns:before,
.cards--considering:before {
.cards--maybe:before {
display: none;
}
@@ -141,7 +141,7 @@
padding-inline: 0;
}
.cards--considering {
.cards--maybe {
background: none;
margin: 0;
padding-inline: 1ch;
+5 -2
View File
@@ -10,7 +10,7 @@
flex-wrap: wrap;
gap: var(--inline-space-half);
inline-size: 100%;
margin-block-start: calc(var(--block-space-half) / 2);
margin-block-start: var(--block-space-half);
margin-inline: calc(var(--column-gap) / -1);
&:has([open]) {
@@ -44,6 +44,7 @@
}
.reactions__trigger {
--btn-size: var(--reaction-size);
--btn-border-color: var(--reaction-border-color);
img {
@@ -115,11 +116,13 @@
/* Make the avatar and delete buttons fit nicely within the reaction */
.reaction__avatar,
.reaction__avatar .avatar,
.reaction__form-label,
.reaction form {
block-size: var(--btn-size);
block-size: 100%;
}
.reaction__delete {
display: none;
-6
View File
@@ -96,12 +96,6 @@
.overflow-clip { text-overflow: clip; white-space: nowrap; overflow: hidden; }
.overflow-ellipsis { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
.overflow-hide-scrollbar::-webkit-scrollbar {
@media (pointer: course) {
display: none;
}
}
.overflow-line-clamp {
-webkit-line-clamp: var(--lines, 2);
-webkit-box-orient: vertical;
@@ -0,0 +1,13 @@
class Cards::DraftsController < ApplicationController
include CardScoped
before_action :redirect_if_published
def show
end
private
def redirect_if_published
redirect_to @card unless @card.drafted?
end
end
@@ -1,18 +0,0 @@
class Cards::EngagementsController < ApplicationController
include CardScoped
def create
case params[:engagement]
when "doing"
@card.engage
when "on_deck"
@card.move_to_on_deck
end
render_card_replacement
end
def destroy
@card.reconsider
render_card_replacement
end
end
@@ -5,7 +5,8 @@ class Cards::PublishesController < ApplicationController
@card.publish
if add_another_param?
redirect_to @board.cards.create!, notice: "Card added"
card = @board.cards.create!(status: :drafted)
redirect_to card_draft_path(card), notice: "Card added"
else
redirect_to @card.board
end
+6 -1
View File
@@ -3,6 +3,7 @@ class CardsController < ApplicationController
before_action :set_board, only: %i[ create ]
before_action :set_card, only: %i[ show edit update destroy ]
before_action :redirect_if_drafted, only: :show
before_action :ensure_permission_to_administer_card, only: %i[ destroy ]
def index
@@ -13,7 +14,7 @@ class CardsController < ApplicationController
respond_to do |format|
format.html do
card = Current.user.draft_new_card_in(@board)
redirect_to card
redirect_to card_draft_path(card)
end
format.json do
@@ -56,6 +57,10 @@ class CardsController < ApplicationController
@card = Current.user.accessible_cards.find_by!(number: params[:id])
end
def redirect_if_drafted
redirect_to card_draft_path(@card) if @card.drafted?
end
def ensure_permission_to_administer_card
head :forbidden unless Current.user.can_administer_card?(@card)
end
+1 -1
View File
@@ -4,7 +4,7 @@ class Users::RolesController < ApplicationController
def update
@user.update!(role_params)
redirect_to users_path
redirect_to account_settings_path
end
private
+1 -1
View File
@@ -30,7 +30,7 @@ class UsersController < ApplicationController
@user.deactivate
respond_to do |format|
format.html { redirect_to users_path }
format.html { redirect_to account_settings_path }
format.json { head :no_content }
end
end
+3 -1
View File
@@ -30,7 +30,9 @@ module AccessesHelper
displayed_watchers = watchers.first(8)
overflow_count = watchers.size - 8
tag.strong(watchers.any? ? "Watching for new cards" : "No one is watching for new cards", class: "txt-uppercase") +
tag.div(class: "divider divider--fade") do
tag.strong(watchers.any? ? "Watching for new cards" : "No one is watching for new cards", class: "txt-uppercase")
end +
tag.div(avatar_tags(displayed_watchers), class: "board-tools__watching") do
tag.div(data: { controller: "dialog", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" }) do
tag.button("+#{overflow_count}", class: "overflow-count btn btn--circle borderless", data: { action: "dialog#open" }, aria: { label: "Show #{overflow_count} more watchers" }) +
+1 -1
View File
@@ -11,7 +11,7 @@ module ColumnsHelper
end
def column_tag(id:, name:, drop_url:, collapsed: true, selected: nil, card_color: "var(--color-card-default)", data: {}, **properties, &block)
classes = token_list("cards", properties.delete(:class), "is-collapsed": collapsed)
classes = token_list("cards", properties.delete(:class), "is-collapsed": collapsed, "is-expanded": !collapsed)
hotkeys_disabled = data[:card_hotkeys_disabled]
data = {
@@ -34,6 +34,7 @@ export default class extends Controller {
showModalAndSubmit(event) {
this.showModal()
this.formTarget.requestSubmit()
this.#restoreFocusAfterTurboFrameLoads()
}
showModal() {
@@ -51,6 +52,12 @@ export default class extends Controller {
}
}
#restoreFocusAfterTurboFrameLoads() {
this.turboFrameTarget.addEventListener("turbo:frame-load", () => {
this.searchInputTarget.focus()
}, { once: true })
}
#loadTurboFrame() {
this.turboFrameTarget.src = this.searchUrlValue
}
@@ -2,10 +2,11 @@ import { Controller } from "@hotwired/stimulus"
import { nextFrame, debounce } from "helpers/timing_helpers";
export default class extends Controller {
static classes = [ "collapsed", "noTransitions", "titleNotVisible" ]
static targets = [ "column", "button", "title" ]
static classes = [ "collapsed", "expanded", "noTransitions", "titleNotVisible" ]
static targets = [ "column", "button", "title", "maybeColumn" ]
static values = {
board: String
board: String,
desktopBreakpoint: { type: String, default: "(min-width: 640px)" }
}
initialize() {
@@ -15,6 +16,11 @@ export default class extends Controller {
async connect() {
await this.#restoreColumnsDisablingTransitions()
this.#setupIntersectionObserver()
this.mediaQuery = window.matchMedia(this.desktopBreakpointValue)
this.handleDesktop = this.#handleDesktop.bind(this)
this.mediaQuery.addEventListener("change", this.handleDesktop)
this.handleDesktop(this.mediaQuery)
}
disconnect() {
@@ -22,10 +28,11 @@ export default class extends Controller {
this._intersectionObserver.disconnect()
this._intersectionObserver = null
}
this.mediaQuery.removeEventListener("change", this.handleDesktop)
}
toggle({ target }) {
const column = target.closest('[data-collapsible-columns-target="column"]')
const column = target.closest('[data-collapsible-columns-target~="column"]')
this.#toggleColumn(column);
}
@@ -41,7 +48,7 @@ export default class extends Controller {
}
focusOnColumn({ target }) {
if (this.#isCollapsed(target)) {
if (this.#isDesktop && this.#isCollapsed(target)) {
this.#collapseAllExcept(target)
this.#expand(target)
}
@@ -74,7 +81,9 @@ export default class extends Controller {
}
#collapseAllExcept(clickedColumn) {
this.columnTargets.forEach(column => {
const columns = this.#isDesktop ? this.columnTargets.filter(c => c !== this.maybeColumnTarget) : this.columnTargets
columns.forEach(column => {
if (column !== clickedColumn) {
this.#collapse(column)
}
@@ -88,17 +97,25 @@ export default class extends Controller {
#collapse(column) {
const key = this.#localStorageKeyFor(column)
this.#buttonFor(column).setAttribute("aria-expanded", "false")
this.#buttonFor(column)?.setAttribute("aria-expanded", "false")
column.classList.remove(this.expandedClass)
column.classList.add(this.collapsedClass)
localStorage.removeItem(key)
}
#expand(column) {
const key = this.#localStorageKeyFor(column)
this.#buttonFor(column).setAttribute("aria-expanded", "true")
#expand(column, saveState = true) {
this.#buttonFor(column)?.setAttribute("aria-expanded", "true")
column.classList.remove(this.collapsedClass)
localStorage.setItem(key, true)
column.classList.add(this.expandedClass)
if (saveState) {
const key = this.#localStorageKeyFor(column)
localStorage.setItem(key, true)
}
if (window.matchMedia('(max-width: 639px)').matches) {
column.scrollIntoView({ behavior: "smooth", inline: "center" })
}
}
#buttonFor(column) {
@@ -114,6 +131,7 @@ export default class extends Controller {
#restoreColumn(column) {
const key = this.#localStorageKeyFor(column)
if (localStorage.getItem(key)) {
this.#collapseAllExcept(column)
this.#expand(column)
}
}
@@ -140,4 +158,33 @@ export default class extends Controller {
this.titleTargets.forEach(title => this._intersectionObserver.observe(title))
}
get #isDesktop() {
return this.mediaQuery?.matches
}
#handleDesktop() {
this.#isDesktop ? this.#handleDesktopMode() : this.#handleMobileMode()
}
async #handleDesktopMode() {
this.#expand(this.maybeColumnTarget, false)
this.#maybeButton.setAttribute("disabled", true)
}
#handleMobileMode() {
this.#maybeButton.removeAttribute("disabled")
const expandedColumn = this.columnTargets.find(column => column !== this.maybeColumnTarget && !this.#isCollapsed(column))
if (expandedColumn) {
this.#collapseAllExcept(expandedColumn)
} else {
this.#collapseAllExcept(this.maybeColumnTarget)
}
}
get #maybeButton() {
return this.maybeColumnTarget.querySelector('[data-collapsible-columns-target="button"]')
}
}
+1
View File
@@ -31,6 +31,7 @@ module Card::Closeable
def close(user: Current.user)
unless closed?
transaction do
not_now&.destroy
create_closure! user: user
track_event :closed, creator: user
end
-6
View File
@@ -1,6 +0,0 @@
class Card::Engagement < ApplicationRecord
belongs_to :account, default: -> { card.account }
belongs_to :card, class_name: "::Card", touch: true
validates :status, presence: true, inclusion: { in: %w[doing on_deck] }
end
+5 -1
View File
@@ -14,7 +14,7 @@ class Event::Description
end
def to_plain_text
to_sentence(creator_name, h(card.title))
to_sentence(creator_name, quoted(card.title))
end
private
@@ -41,6 +41,10 @@ class Event::Description
h(event.creator.name)
end
def quoted(text)
%("#{h text}")
end
def card
@card ||= event.action.comment_created? ? event.eventable.card : event.eventable
end
+15 -2
View File
@@ -1,4 +1,6 @@
class Webhook::Delivery < ApplicationRecord
include Rails.application.routes.url_helpers
class ResponseTooLarge < StandardError; end
STALE_TRESHOLD = 7.days
@@ -130,8 +132,7 @@ class Webhook::Delivery < ApplicationRecord
elsif webhook.for_campfire?
render_payload(formats: :html)
elsif webhook.for_slack?
html = render_payload(formats: :html)
{ text: convert_html_to_mrkdwn(html) }.to_json
slack_payload
else
render_payload(formats: :json)
end
@@ -158,4 +159,16 @@ class Webhook::Delivery < ApplicationRecord
document.text
end
def slack_payload
text = event.description_for(nil).to_plain_text
url = polymorphic_url(event.eventable, base_url_options.merge(script_name: account.slug))
{ text: "#{text} <#{url}|Open in Fizzy>" }.to_json
end
def base_url_options
Rails.application.routes.default_url_options.presence ||
Rails.application.config.action_mailer.default_url_options
end
end
@@ -1,6 +1,4 @@
<div class="cards__list" data-drag-drop-item-container>
<div class="blank-slate blank-slate--drag card card--hide-unless-empty">
<p>Drag cards here</p>
</div>
<div class="blank-slate blank-slate--empty card--hide-unless-empty">No cards here</div>
<div class="blank-slate blank-slate--drag card card--hide-unless-empty">
<p>Drag cards here</p>
</div>
<div class="blank-slate blank-slate--empty card--hide-unless-empty">No cards here</div>
-3
View File
@@ -1,3 +0,0 @@
<div class="cards__list" data-drag-drop-item-container >
<%= render "cards/display/previews", cards: cards, draggable: draggable %>
</div>
@@ -12,12 +12,14 @@
<section class="cards cards--grid">
<%= turbo_frame_tag :closed_column do %>
<% if @page.used? %>
<%= with_automatic_pagination :closed_column, @page do %>
<%= render "boards/columns/list", cards: @page.records, draggable: true %>
<div class="cards__list hide-scrollbar" data-drag-drop-item-container>
<% if @page.used? %>
<%= with_automatic_pagination :closed_column, @page do %>
<%= render "cards/display/previews", cards: @page.records, draggable: true %>
<% end %>
<% else %>
<%= render "boards/columns/empty_placeholder" %>
<% end %>
<% else %>
<%= render "boards/columns/empty_placeholder" %>
<% end %>
</div>
<% end %>
</section>
@@ -12,12 +12,14 @@
<section class="cards cards--grid">
<%= turbo_frame_tag :not_now_column do %>
<% if @page.used? %>
<%= with_automatic_pagination :not_now_column, @page do %>
<%= render "boards/columns/list", cards: @page.records, draggable: true %>
<div class="cards__list hide-scrollbar" data-drag-drop-item-container>
<% if @page.used? %>
<%= with_automatic_pagination :not_now_column, @page do %>
<%= render "cards/display/previews", cards: @page.records, draggable: true %>
<% end %>
<% else %>
<%= render "boards/columns/empty_placeholder" %>
<% end %>
<% else %>
<%= render "boards/columns/empty_placeholder" %>
<% end %>
</div>
<% end %>
</section>
+8 -6
View File
@@ -12,12 +12,14 @@
<section class="cards cards--grid">
<%= turbo_frame_tag @column, :cards do %>
<% if @page.used? %>
<%= with_automatic_pagination dom_id(@column, :cards), @page do %>
<%= render "boards/columns/list", cards: @page.records, draggable: true %>
<div class="cards__list hide-scrollbar" data-drag-drop-item-container>
<% if @page.used? %>
<%= with_automatic_pagination dom_id(@column, :cards), @page do %>
<%= render "cards/display/previews", cards: @page.records, draggable: true %>
<% end %>
<% else %>
<%= render "boards/columns/empty_placeholder" %>
<% end %>
<% else %>
<%= render "boards/columns/empty_placeholder" %>
<% end %>
</div>
<% end %>
</section>
@@ -12,12 +12,14 @@
<section class="cards cards--grid">
<%= turbo_frame_tag :stream_column do %>
<% if @page.used? %>
<%= with_automatic_pagination :stream_column, @page do %>
<%= render "boards/columns/list", cards: @page.records, draggable: true %>
<div class="cards__list hide-scrollbar" data-drag-drop-item-container>
<% if @page.used? %>
<%= with_automatic_pagination :stream_column, @page do %>
<%= render "cards/display/previews", cards: @page.records, draggable: true %>
<% end %>
<% else %>
<%= render "boards/columns/empty_placeholder" %>
<% end %>
<% else %>
<%= render "boards/columns/empty_placeholder" %>
<% end %>
</div>
<% end %>
</section>
+2 -1
View File
@@ -1,4 +1,5 @@
<% @page_title = @board.name %>
<% @body_class = "contained-scrolling" %>
<% turbo_exempts_page_from_cache %>
<%= turbo_stream_from @board %>
@@ -25,7 +26,7 @@
<% if @filter.used?(ignore_boards: true) %>
<%= render "boards/show/filtered_cards", page: @page %>
<% else %>
<%= render "columns/show/add_card_button", board: @board %>
<%= render "boards/show/columns", page: @page, board: @board %>
<%= render "boards/show/mobile_columns", page: @page, board: @board %>
<% end %>
<% end %>
+1
View File
@@ -4,6 +4,7 @@
drag_and_drop_hover_container_class: "drag-and-drop__hover-container",
collapsible_columns_board_value: board.id,
collapsible_columns_collapsed_class: "is-collapsed",
collapsible_columns_expanded_class: "is-expanded",
collapsible_columns_no_transitions_class: "no-transitions",
collapsible_columns_title_not_visible_class: "is-off-screen",
navigable_list_supports_vertical_navigation_value: false,
@@ -1,6 +1,8 @@
<section class="cards cards--grid">
<%= with_automatic_pagination :filtered_cards_paginated_container, page do %>
<%= render "cards/display/previews", cards: page.records, draggable: true %>
<% end %>
<div class="blank-slate blank-slate--empty blank-slate--filters">No cards match this filter</div>
<div class="cards__list hide-scrollbar">
<%= with_automatic_pagination :filtered_cards_paginated_container, page do %>
<%= render "cards/display/previews", cards: page.records, draggable: true %>
<% end %>
<div class="blank-slate blank-slate--empty blank-slate--filters">No cards match this filter</div>
</div>
</section>
@@ -1,48 +0,0 @@
<% postponed_count = board.cards.postponed.size %>
<% awaiting_triage_count = board.cards.awaiting_triage.size %>
<% closed_count = board.cards.closed.size %>
<section class="mobile-card-columns">
<div class="cards margin-block-end-half">
<%= render "columns/show/add_card_button", board: board %>
</div>
<%= link_to board_columns_not_now_path(board), class: "cards cards--on-deck is-collapsed", style: "--card-color: var(--color-card-complete);", data: { turbo_frame: "_top" } do %>
<div class="cards__expander btn btn--plain" style="--card-count: <%= postponed_count %>">
<span class="cards__expander-count"><%= postponed_count %></span>
<h2 class="cards__expander-title">
Not Now
</h2>
</div>
<% end %>
<%= link_to board_columns_stream_path(board), class: "cards cards--considering is-collapsed", data: { turbo_frame: "_top" } do %>
<div class="cards__expander btn btn--plain" style="--card-count: <%= awaiting_triage_count %>">
<span class="cards__expander-count"><%= awaiting_triage_count %></span>
<h2 class="cards__expander-title">
Maybe?
</h2>
</div>
<% end %>
<% board.columns.sorted.each do |column| %>
<% active_count = column.cards.active.size %>
<%= link_to board_column_path(column.board, column), class: "cards cards--doing is-collapsed", style: "--card-color: #{column.color}", data: { turbo_frame: "_top" } do %>
<div class="cards__expander btn btn--plain" style="--card-count: <%= active_count %>">
<span class="cards__expander-count"><%= active_count %></span>
<h2 class="cards__expander-title">
<%= column.name %>
</h2>
</div>
<% end %>
<% end %>
<%= link_to board_columns_closed_path(board), class: "cards cards--closed is-collapsed", style: "--card-color: var(--color-card-complete);", data: { turbo_frame: "_top" } do %>
<div class="cards__expander btn btn--plain" style="--card-count: <%= closed_count %>">
<span class="cards__expander-count"><%= closed_count %></span>
<h2 class="cards__expander-title">
Done
</h2>
</div>
<% end %>
</section>
+13 -15
View File
@@ -1,20 +1,18 @@
<%= column_tag id: "the-stream", name: "Maybe?", drop_url: columns_card_drops_stream_path("__id__"), collapsed: false, selected: "true", class: "cards--considering" do %>
<%= column_tag id: "maybe", name: "Maybe?", drop_url: columns_card_drops_stream_path("__id__"), collapsed: false, selected: "true", class: "cards--maybe", data: {
drag_and_strum_target: "container",
collapsible_columns_target: "column maybeColumn",
action: "focus->collapsible-columns#focusOnColumn"
} do %>
<header class="cards__header">
<div class="cards__expander">
<h2 class="cards__expander-title" data-collapsible-columns-target="title">Maybe?</h2>
</div>
<%= link_to board_columns_stream_path(board), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %>
<%= icon_tag "grid", class: "translucent" %>
<span class="for-screen-reader">Expand column</span>
<% end %>
<%= render "boards/show/expander", title: "Maybe?", count: board.cards.awaiting_triage.count, column_id: "maybe" %>
<%= render "boards/show/menu/maximize", column_path: board_columns_stream_path(board) %>
</header>
<%= render "columns/show/add_card_button", board: board %>
<% if page.used? %>
<%= with_automatic_pagination "the-stream", @page do %>
<%= render "boards/columns/list", cards: page.records, draggable: true %>
<div class="cards__list hide-scrollbar" data-drag-drop-item-container>
<% if page.used? %>
<%= with_automatic_pagination "maybe", @page do %>
<%= render "cards/display/previews", cards: page.records, draggable: true %>
<% end %>
<% end %>
<% else %>
<div class="cards__list" data-drag-drop-item-container>
</div>
<% end %>
</div>
<% end %>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="cards__new-column position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<div class="cards__new-column" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<button class="btn btn--circle txt-x-small borderless" data-controller="tooltip" data-action="click->dialog#open:stop">
<%= icon_tag "add", class: "translucent" %>
<span class="for-screen-reader">Add a column</span>
+2
View File
@@ -16,6 +16,8 @@ json.cache! card do
json.board card.board, partial: "boards/board", as: :board
json.column card.column, partial: "columns/column", as: :column if card.column
json.creator card.creator, partial: "users/user", as: :user
json.assignees card.assignees.limit(5), partial: "users/user", as: :user
json.has_more_assignees card.assignees.size > 5
json.comments_url card_comments_url(card)
end
-6
View File
@@ -37,11 +37,5 @@
<% if card.published? %>
<%= render "cards/container/footer/published", card: card %>
<% elsif card.drafted? %>
<% if Fizzy.saas? %>
<%= render "cards/container/footer/saas/create", card: card %>
<% else %>
<%= render "cards/container/footer/create", card: card %>
<% end %>
<% end %>
</section>
@@ -0,0 +1,34 @@
<section id="<%= dom_id(card, :card_container) %>" class="card-perma" style="--card-color: <%= card.color %>;">
<% cache card do %>
<div class="card-perma__actions card-perma__actions--left">
<%= render "cards/container/image", card: card %>
</div>
<div class="card-perma__bg">
<%= card_article_tag card, class: "card" do %>
<header class="card__header">
<%= render "cards/display/perma/board", card: card %>
<%= render "cards/display/perma/tags", card: card %>
</header>
<div class="card__body justify-space-between">
<div class="card__content">
<%= render "cards/container/content", card: card %>
<%= render "cards/display/perma/steps", card: card %>
</div>
</div>
<footer class="card__footer full-width flex align-start gap">
<%= render "cards/display/perma/meta", card: card %>
<%= render "cards/display/perma/background", card: card %>
</footer>
<% end %>
</div>
<% end %>
<% if Fizzy.saas? %>
<%= render "cards/container/footer/saas/create", card: card %>
<% else %>
<%= render "cards/container/footer/create", card: card %>
<% end %>
</section>
+16
View File
@@ -0,0 +1,16 @@
<% @page_title = @card.title %>
<% @header_class = "header--card" %>
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_back_to_board(@card.board) %>
</div>
<% end %>
<div data-controller="beacon lightbox" data-beacon-url-value="<%= card_reading_path(@card) %>">
<%= render "cards/drafts/container", card: @card %>
<%= render "layouts/lightbox" do %>
<%= button_to_remove_card_image(@card) if @card.image.attached? %>
<% end %>
</div>
+6 -4
View File
@@ -19,9 +19,11 @@
<%= turbo_frame_tag :cards_container do %>
<section class="cards cards--grid">
<%= with_automatic_pagination :cards_paginated_container, @page do %>
<%= render "cards/display/previews", cards: @page.records, draggable: true %>
<% end %>
<div class="blank-slate blank-slate--empty blank-slate--filters">No cards match this filter</div>
<div class="cards__list hide-scrollbar">
<%= with_automatic_pagination :cards_paginated_container, @page do %>
<%= render "cards/display/previews", cards: @page.records, draggable: true %>
<% end %>
<div class="blank-slate blank-slate--empty blank-slate--filters">No cards match this filter</div>
</div>
</section>
<% end %>
+1 -1
View File
@@ -16,7 +16,7 @@
<div data-controller="beacon lightbox" data-beacon-url-value="<%= card_reading_path(@card) %>">
<%= render "cards/container", card: @card %>
<%= render "cards/messages", card: @card unless @card.drafted? %>
<%= render "cards/messages", card: @card %>
<%= render "layouts/lightbox" do %>
<%= button_to_remove_card_image(@card) if @card.image.attached? %>
@@ -1 +1 @@
<%= turbo_stream.replace("the-stream", partial: "boards/show/stream", method: :morph, locals:{ board: @card.board, page: @page }) %>
<%= turbo_stream.replace("maybe", partial: "boards/show/stream", method: :morph, locals:{ board: @card.board, page: @page }) %>
@@ -5,8 +5,6 @@
<kbd class="hide-on-touch">C</kbd>
<% end %>
<hr class="separator--horizontal full-width" aria-hidden="true">
<footer>
<%= access_involvement_advance_button(board, Current.user, show_watchers: true) %>
</footer>
+1 -1
View File
@@ -1,7 +1,7 @@
<% filter = user_filtering.filter %>
<div class="filters__manage gap-half">
<%= link_to root_path, class: "btn btn--remove txt-x-small", data: { controller: "hotkey tooltip", action: "keydown.esc@document->hotkey#click"} do %>
<%= link_to no_filtering_url, class: "btn btn--remove txt-x-small", data: { controller: "hotkey tooltip", action: "keydown.esc@document->hotkey#click"} do %>
<%= icon_tag "close" %>
<span class="for-screen-reader">Clear all</span>
<% end %>
+13 -11
View File
@@ -2,19 +2,21 @@
<html lang="en">
<%= render "layouts/shared/head" %>
<body data-controller="local-time timezone-cookie turbo-navigation theme" data-action="turbo:morph@window->local-time#refreshAll turbo:before-visit@document->turbo-navigation#rememberLocation" data-platform="<%= platform.type %>">
<header class="header header--mobile-actions-stack <%= @header_class %>" id="header">
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
<%= render "my/menu" if Current.user %>
<%= yield :header %>
</header>
<body class="<%= @body_class %>" data-controller="local-time timezone-cookie turbo-navigation theme" data-action="turbo:morph@window->local-time#refreshAll turbo:before-visit@document->turbo-navigation#rememberLocation" data-platform="<%= platform.type %>">
<div id="global-container">
<header class="header header--mobile-actions-stack <%= @header_class %>" id="header">
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
<%= render "my/menu" if Current.user %>
<%= yield :header %>
</header>
<%= render "layouts/shared/flash" %>
<%= render "layouts/shared/time_zone" if Current.user %>
<%= render "layouts/shared/flash" %>
<%= render "layouts/shared/time_zone" if Current.user %>
<main id="main">
<%= yield %>
</main>
<main id="main">
<%= yield %>
</main>
</div>
<footer id="footer" class="hide-on-native">
<%= yield :footer %>
+17 -15
View File
@@ -2,24 +2,26 @@
<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://fizzy.do", class: "header__logo center flex-inline align-center" do %>
<span><%= image_tag "logo.png", alt: "" %></span>
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg" title="Fizzy"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
<% end %>
</nav>
<body class="public <%= @body_class %>" data-controller="local-time timezone-cookie" data-action="turbo:morph@window->local-time#refreshAll">
<div id="global-container">
<header class="header" id="header">
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
<nav>
<%= link_to "https://fizzy.do", class: "header__logo center flex-inline align-center" do %>
<span><%= image_tag "logo.png", alt: "" %></span>
<svg height="30" viewBox="0 0 96 30" width="96" xmlns="http://www.w3.org/2000/svg" title="Fizzy"><path clip-rule="evenodd" d="m93.5609 8.52856c.9033.04314 1.3769.47352 1.4199 1.33398.1291 2.40966.0859 5.24976.1289 7.48726l.1289 7.3575c0 15.4902-10.2406 15.8354-16.8242 14.7168-.8606-.1722-1.2482-.7322-1.1621-1.5928l.3867-3.7002c.086-1.0327.732-1.2905 1.6787-.9463 4.3889 1.5919 9.122-.4737 9.0791-4.7334 0-1.0757-.6026-1.2052-1.2481-.3877-.9036 1.0757-2.2802 2.1943-4.3886 2.1944-3.0552 0-8.2188-1.2046-8.2188-11.962 0-2.6677-.086-5.5076-.0429-8.47652 0-.90362.5162-1.37702 1.4199-1.33399 1.2048.04302 2.3665.00006 3.5283-.04297.9036 0 1.4199.47328 1.4199 1.41992 0 3.22686-.0859 5.63626-.0859 6.45406 0 7.7023 1.7647 8.2187 3.7871 8.2618 2.1512.0427 3.5709-1.5491 3.8291-5.2061v-.9902c0-4.604.0861-6.2821-.043-8.47659-.086-.9036.3444-1.41986 1.2481-1.46289zm-43.4629-.21484c1.1186-.04303 1.5064.68823.9472 1.63476-1.8502 3.05502-5.3796 8.73462-8.3486 13.42482-.4733.7745-.1713 1.334.7754 1.334 2.1511.043 2.9261.0431 5.5937-.086.9464-.0429 1.4199.4306 1.42 1.377-.0431.8605-.0001 1.8504 0 2.7109 0 .9036-.4305 1.3769-1.334 1.377-3.9588.043-9.4238-.1721-15.6201-.043-1.1618 0-1.5487-.6881-.9463-1.6348l8.3906-13.2099c.4733-.7315.1721-1.3769-.7315-1.377-2.4096-.043-4.9915.0429-6.8418.1289-.9036.0431-1.4199-.3874-1.4199-1.291-.043-.9035 0-1.9792 0-2.92576 0-.86059.7316-1.33399 2.0225-1.33399 6.1531.04303 12.1341-.04291 16.0928-.08593zm21.1367 0c1.1186-.04294 1.5056.68817.9463 1.63476-1.8503 3.05502-5.3787 8.73462-8.3477 13.42482-.4733.7745-.1721 1.3339.7744 1.334 2.1514.043 2.9261.0431 5.5938-.086.9465-.043 1.4198.4305 1.4199 1.377-.043.8605 0 1.8504 0 2.7109 0 .9036-.4304 1.377-1.334 1.377-3.9586.043-9.4232-.1721-15.6191-.043-1.1617 0-1.5494-.6883-.9473-1.6348l8.3916-13.2099c.4733-.7315.1712-1.377-.7324-1.377-2.4096-.043-4.9916.0429-6.8418.1289-.9033.0429-1.4199-.3875-1.4199-1.291-.0431-.9035 0-1.9792 0-2.92576 0-.86051.7318-1.3339 2.0224-1.33399 6.1533.04303 12.135-.04291 16.0938-.08593zm-43.3252.25781c.9035-.04295 1.4199.38753 1.4199 1.24805.043 1.50602 0 3.65782 0 12.39262 0 5.6796-.086 5.4215 0 6.4111.1291.9036-.3444 1.4198-1.248 1.4629l-3.9161-.086c-.8604-.0431-1.3769-.4735-1.4199-1.3339-.1291-2.4097-.0859-5.2499-.1289-7.4874-.1291-5.5505-.0429-7.8742-.0859-11.23042-.043-.90357.4733-1.37695 1.4199-1.37695 1.7212.08606 2.8832.08606 3.959 0zm-21.88185-8.56250162c2.36657.04302752 4.77645.00000269 8.30465 0 1.2909 0 3.0554.04302142 4.6905 0 .7314 0 1.119.38683562 1.1191 1.16113162-.043 1.46292-.086 3.3134-.043 4.77637 0 .77451-.4305 1.11924-1.205 1.0332-1.2909-.12909-3.3564-.25866-5.5079-.34473-2.0653-.12908-2.1945.04296-4.25972.04297-.8606 0-1.42065.43055-1.59277 1.20508v.04297c-.12909.64544-.12891 1.03327-.12891 1.67871v.51567c.04303.9035.51653 1.3338 1.37696 1.3769 2.40954.043 5.46464.0002 7.70214-.1289.7315-.043 1.1621.3016 1.1621 1.0762l-.0859 4.3886c0 .7315-.4306 1.1192-1.1621 1.0762-2.2374-.043-4.7329-.0439-7.35746-.0869-.9035-.043-1.37677.4736-1.41992 1.334l-.17285 4.3467c0 2.6244.04379 4.0872.17285 5.292.12909.7315-.25872 1.1621-.99023 1.1621l-5.29297.0429c-.731221-.0001-1.075196-.3877-1.075196-1.1191-.000045-1.2478-.2149575-2.8399-.128907-5.7656.129086-5.7659.085256-13.7261-.12988242-21.81546-.04302878-.774508.34469842-1.162087 1.07617542-1.162105 1.592-.0430291 3.70034-.1719109 4.94824-.12890662zm19.90235.34374962c2.6247.00004 3.5283 1.377542 3.5283 3.055662-.0001 1.72099-.9038 3.09762-3.5283 3.09766-2.6677 0-3.5712-1.37665-3.5713-3.09766 0-1.67815.9034-3.055662 3.5713-3.055662z" fill="currentColor" fill-rule="evenodd"/></svg>
<% end %>
</nav>
<%= yield :header %>
</header>
<%= yield :header %>
</header>
<%= render "layouts/shared/flash" %>
<%= render "layouts/shared/flash" %>
<main id="main">
<%= yield %>
</main>
<main id="main">
<%= yield %>
</main>
</div>
<footer id="footer">
<%= yield :footer %>
+1 -1
View File
@@ -16,7 +16,7 @@
<div data-navigable-list-target="item" class="full-width">
<%= link_to notifications_settings_path,
class: "btn borderless tray__notification-settings",
title: "Notification settings",
title: "Notification Settings",
data: { action: "dialog#close" } do %>
<%= icon_tag "settings" %>
<span>Settings</span>
@@ -70,7 +70,7 @@
<li>Click <em aria-label="the Apple menu"></em> in the top left.</li>
<li>Click <em>System Settings…</em>.</li>
<li>Click <em>Notifications</em>.</li>
<li>Click <em><%= platform.browser.capitalize %></em>.</li>
<li>Click <em><%= platform.browser == "Chrome" ? "Google Chrome" : platform.browser.capitalize %></em>.</li>
<li>Click <em><%= icon_tag "switch", alt: "the switch" %></em> to <em>Allow notifications</em>.</li>
<% end %>
</ol>
@@ -1,10 +1,10 @@
<% @page_title = "Notifications settings" %>
<% @page_title = "Notification Settings" %>
<% content_for :header do %>
<h1 class="header__title"><%= @page_title %></h1>
<% end %>
<section class="settings">
<section class="settings margin-block-start-half">
<div class="settings__panel settings__panel--users panel shadow center">
<h2 class="txt-medium txt-uppercase divider">Boards</h2>
<div class="settings__user-list flex flex-column gap-half">
@@ -1,6 +1,6 @@
<div class="panel margin-block-double center shadow">
<h1 class="font-black txt-x-large margin-none-block-end">Youre unsubscribed</h1>
<p class="margin-block-start-half">Thanks, <%= @user.first_name %>! Fizzy wont send you any more email notifications. If you change your mind you can turn them back on in <%= link_to "Notifications Settings", notifications_settings_path %>.</p>
<p class="margin-block-start-half">Thanks, <%= @user.first_name %>! Fizzy wont send you any more email notifications. If you change your mind, you can turn them back on in <%= link_to "Notification Settings", notifications_settings_path %>.</p>
<%= link_to root_path, class: "btn btn--link margin-block-start" do %>
<%= icon_tag "arrow-left" %>
@@ -1,3 +0,0 @@
<div class="cards__list">
<%= render "cards/display/public_previews", cards: cards %>
</div>
@@ -12,13 +12,15 @@
<section class="cards cards--grid">
<%= turbo_frame_tag :closed_column do %>
<% if @page.used? %>
<%= with_automatic_pagination :closed_column, @page do %>
<%= render "public/boards/columns/list", cards: @page.records %>
<div class="cards__list hide-scrollbar">
<% if @page.used? %>
<%= with_automatic_pagination :closed_column, @page do %>
<%= render "cards/display/public_previews", cards: @page.records %>
<% end %>
<% else %>
<div class="blank-slate blank-slate--empty">No cards here</div>
<% end %>
<% else %>
<div class="blank-slate blank-slate--empty">No cards here</div>
<% end %>
</div>
<% end %>
</section>
@@ -12,13 +12,15 @@
<section class="cards cards--grid">
<%= turbo_frame_tag :not_now_column do %>
<% if @page.used? %>
<%= with_automatic_pagination :not_now_column, @page do %>
<%= render "public/boards/columns/list", cards: @page.records %>
<div class="cards__list hide-scrollbar">
<% if @page.used? %>
<%= with_automatic_pagination :not_now_column, @page do %>
<%= render "cards/display/public_previews", cards: @page.records %>
<% end %>
<% else %>
<div class="blank-slate blank-slate--empty">No cards here</div>
<% end %>
<% else %>
<div class="blank-slate blank-slate--empty">No cards here</div>
<% end %>
</div>
<% end %>
</section>
@@ -12,13 +12,15 @@
<section class="cards cards--grid">
<%= turbo_frame_tag @column, :cards do %>
<% if @page.used? %>
<%= with_automatic_pagination dom_id(@column, :cards), @page do %>
<%= render "public/boards/columns/list", cards: @page.records %>
<div class="cards__list hide-scrollbar">
<% if @page.used? %>
<%= with_automatic_pagination dom_id(@column, :cards), @page do %>
<%= render "cards/display/public_previews", cards: @page.records %>
<% end %>
<% else %>
<div class="blank-slate blank-slate--empty">No cards here</div>
<% end %>
<% else %>
<div class="blank-slate blank-slate--empty">No cards here</div>
<% end %>
</div>
<% end %>
</section>
@@ -12,13 +12,15 @@
<section class="cards cards--grid">
<%= turbo_frame_tag :stream_column do %>
<% if @page.used? %>
<%= with_automatic_pagination :stream_column, @page do %>
<%= render "public/boards/columns/list", cards: @page.records %>
<div class="cards__list hide-scrollbar">
<% if @page.used? %>
<%= with_automatic_pagination :stream_column, @page do %>
<%= render "cards/display/public_previews", cards: @page.records %>
<% end %>
<% else %>
<div class="blank-slate blank-slate--empty">No cards here</div>
<% end %>
<% else %>
<div class="blank-slate blank-slate--empty">No cards here</div>
<% end %>
</div>
<% end %>
</section>
+1
View File
@@ -1,4 +1,5 @@
<% @page_title = @board.name %>
<% @body_class = "contained-scrolling" %>
<% content_for :head do %>
<%= tag.meta property: "og:title", content: "#{@board.name} | #{Current.account.name}" %>
@@ -3,6 +3,7 @@
data-controller="collapsible-columns"
data-collapsible-columns-board-value="<%= board.id %>"
data-collapsible-columns-collapsed-class="is-collapsed"
data-collapsible-columns-expanded-class="is-expanded"
data-collapsible-columns-no-transitions-class="no-transitions"
data-collapsible-columns-title-not-visible-class="is-off-screen">
@@ -1,11 +1,17 @@
<section id="the-stream" class="cards cards--considering" data-column-name="Maybe?"
<section id="maybe"
class="cards cards--maybe is-expanded"
data-column-name="Maybe?"
data-collapsible-columns-target="column maybeColumn"
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle">
<div class="cards__transition-container">
<header class="cards__header">
<div class="cards__expander">
<div hidden class="cards__expander">
<h2 class="cards__expander-title" data-collapsible-columns-target="title">Maybe?</h2>
</div>
<%# render "boards/show/expander", title: "Maybe?", count: column.cards.active.count, column_id: dom_id(column) %>
<%= render "boards/show/expander", title: "Maybe?", count: 2, column_id: "maybe" %>
<%= link_to public_board_columns_stream_url(board.publication.key), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %>
<%= icon_tag "grid", class: "translucent" %>
<span class="for-screen-reader">Maximize column</span>
+1 -1
View File
@@ -26,7 +26,7 @@ Rails.application.configure do
config.cache_store = :memory_store
config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" }
else
config.action_controller.perform_caching = true
config.action_controller.perform_caching = false
config.cache_store = :null_store
end
+5
View File
@@ -90,6 +90,11 @@ Rails.application.configure do
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]
# "info" includes generic and useful information about system operation, but avoids logging too much
# information to avoid inadvertent exposure of personally identifiable information (PII). If you
# want to log everything, set the level to "debug".
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
# Use a different cache store in production.
config.cache_store = :solid_cache_store
+1
View File
@@ -71,6 +71,7 @@ Rails.application.routes.draw do
resources :cards do
scope module: :cards do
resource :draft, only: :show
resource :board
resource :closure
resource :column
@@ -0,0 +1,16 @@
class DropCardEngagements < ActiveRecord::Migration[8.2]
def up
drop_table :card_engagements
end
def down
create_table :card_engagements, id: :uuid do |t|
t.references :account, type: :uuid, null: false
t.references :card, type: :uuid, null: false, index: true
t.string :status, null: false
t.timestamps
end
add_index :card_engagements, [ :account_id, :status ]
end
end
Generated
+1 -11
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_12_10_054934) do
ActiveRecord::Schema[8.2].define(version: 2025_12_19_120755) do
create_table "accesses", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.datetime "accessed_at"
t.uuid "account_id", null: false
@@ -169,16 +169,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_12_10_054934) do
t.index ["card_id"], name: "index_card_activity_spikes_on_card_id", unique: true
end
create_table "card_engagements", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id"
t.datetime "created_at", null: false
t.string "status", default: "doing", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "status"], name: "index_card_engagements_on_account_id_and_status"
t.index ["card_id"], name: "index_card_engagements_on_card_id"
end
create_table "card_goldnesses", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id", null: false
+1 -11
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_12_10_054934) do
ActiveRecord::Schema[8.2].define(version: 2025_12_19_120755) do
create_table "accesses", id: :uuid, force: :cascade do |t|
t.datetime "accessed_at"
t.uuid "account_id", null: false
@@ -169,16 +169,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_12_10_054934) do
t.index ["card_id"], name: "index_card_activity_spikes_on_card_id", unique: true
end
create_table "card_engagements", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id"
t.datetime "created_at", null: false
t.string "status", limit: 255, default: "doing", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "status"], name: "index_card_engagements_on_account_id_and_status"
t.index ["card_id"], name: "index_card_engagements_on_card_id"
end
create_table "card_goldnesses", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id", null: false
+4 -4
View File
@@ -45,7 +45,7 @@ bin/rails secret
Once you have one, set it in the `SECRET_KEY_BASE` environment variable:
```sh
docker run --environment SECRET_KEY_BASE=abcdefabcdef ...
docker run --env SECRET_KEY_BASE=abcdefabcdef ...
```
#### SSL
@@ -56,7 +56,7 @@ Note that if you're using SSL, you'll want to allow traffic on ports 80 and 443.
So if you were running on `fizzy.example.com` you could enable SSL like this:
```sh
docker run --publish 80:80 --publish 443:443 --environment TLS_DOMAIN=fizzy.example.com ...
docker run --publish 80:80 --publish 443:443 --env TLS_DOMAIN=fizzy.example.com ...
```
If you are terminating SSL in some other proxy in front of Fizzy, then you don't need to set `TLS_DOMAIN`, and can just publish port 80:
@@ -67,7 +67,7 @@ docker run --publish 80:80 ...
If you aren't using SSL at all (for example, if you want to run it locally on your laptop) then you should specify `DISABLE_SSL=true` instead:
```sh
docker run --publish 80:80 --environment DISABLE_SSL=true ...
docker run --publish 80:80 --env DISABLE_SSL=true ...
```
#### SMTP Email
@@ -96,7 +96,7 @@ Fizzy needs to know the public URL of your instance so it can generate correct l
Set `BASE_URL` to the full URL where your Fizzy instance is accessible:
```sh
docker run --environment BASE_URL=https://fizzy.example.com ...
docker run --env BASE_URL=https://fizzy.example.com ...
```
#### VAPID keys
+1
View File
@@ -76,6 +76,7 @@ env:
clear:
APP_FQDN: beta<%= @beta_number %>.fizzy-beta.com
RAILS_ENV: beta
RAILS_LOG_LEVEL: fatal # suppress unstructured log lines
MYSQL_DATABASE_HOST: fizzy-mysql-primary
MYSQL_DATABASE_REPLICA_HOST: fizzy-mysql-replica
MYSQL_SOLID_CABLE_HOST: fizzy-mysql-primary
+1
View File
@@ -27,6 +27,7 @@ ssh:
env:
clear:
RAILS_ENV: production
RAILS_LOG_LEVEL: fatal # suppress unstructured log lines
MYSQL_DATABASE_HOST: fizzy-mysql-primary
MYSQL_DATABASE_REPLICA_HOST: fizzy-mysql-replica
MYSQL_SOLID_CABLE_HOST: fizzy-mysql-primary
+1
View File
@@ -27,6 +27,7 @@ ssh:
env:
clear:
RAILS_ENV: staging
RAILS_LOG_LEVEL: fatal # suppress unstructured log lines
MYSQL_DATABASE_HOST: fizzy-staging-mysql-primary
MYSQL_DATABASE_REPLICA_HOST: fizzy-staging-mysql-replica
MYSQL_SOLID_CABLE_HOST: fizzy-staging-mysql-primary
+1 -2
View File
@@ -1,9 +1,8 @@
Rails.application.configure do
config.active_storage.service = :purestorage
# Enable structured logging, suppress unstructured log lines
# Enable structured logging
config.structured_logging.logger = ActiveSupport::Logger.new(STDOUT)
config.log_level = :fatal
config.action_controller.default_url_options = { host: "app.fizzy.do", protocol: "https" }
config.action_mailer.default_url_options = { host: "app.fizzy.do", protocol: "https" }
@@ -10,7 +10,7 @@ class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
test "admin sees nearing card limit notice" do
accounts(:initech).update_column(:cards_count, 950)
get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
assert_response :success
assert_match /upgrade to unlimited/i, response.body
@@ -19,7 +19,7 @@ class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
test "admin sees nearing storage limit notice" do
Account.any_instance.stubs(:bytes_used).returns(600.megabytes)
get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
assert_response :success
assert_match /upgrade to get more/i, response.body
@@ -30,7 +30,7 @@ class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
test "admin sees exceeding card limit notice" do
accounts(:initech).update_column(:cards_count, 1001)
get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
assert_response :success
assert_match /youve used your.*free cards/i, response.body
@@ -39,7 +39,7 @@ class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
test "admin sees exceeding storage limit notice" do
Account.any_instance.stubs(:bytes_used).returns(1.1.gigabytes)
get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
assert_response :success
assert_match /youve run out of.*free storage/i, response.body
@@ -64,7 +64,7 @@ class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
test "comped account under limits sees no notices" do
accounts(:initech).comp
get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
assert_response :success
assert_no_match /upgrade/i, response.body
@@ -0,0 +1,21 @@
require "test_helper"
class Cards::DraftsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in_as :kevin
end
test "show" do
card = boards(:writebook).cards.create!(creator: users(:kevin), status: :drafted)
get card_draft_path(card)
assert_response :success
end
test "show redirects to card when published" do
card = cards(:logo)
get card_draft_path(card)
assert_redirected_to card
end
end
@@ -28,6 +28,6 @@ class Cards::PublishesControllerTest < ActionDispatch::IntegrationTest
new_card = Card.last
assert new_card.drafted?
assert_redirected_to new_card
assert_redirected_to card_draft_path(new_card)
end
end
+9 -2
View File
@@ -21,7 +21,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
end
card = Card.last
assert_redirected_to card
assert_redirected_to card_draft_path(card)
assert card.drafted?
end
@@ -31,10 +31,17 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
assert_no_difference -> { Card.count } do
post board_cards_path(boards(:writebook))
assert_redirected_to draft
assert_redirected_to card_draft_path(draft)
end
end
test "show redirects to draft when card is drafted" do
card = boards(:writebook).cards.create!(creator: users(:kevin), status: :drafted)
get card_path(card)
assert_redirected_to card_draft_path(card)
end
test "show" do
get card_path(cards(:logo))
assert_response :success
@@ -10,7 +10,7 @@ class Users::RolesControllerTest < ActionDispatch::IntegrationTest
put user_role_path(users(:david)), params: { user: { role: "admin" } }
assert_redirected_to users_path
assert_redirected_to account_settings_path
assert users(:david).reload.admin?
end
+1 -1
View File
@@ -37,7 +37,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
delete user_path(users(:david))
end
assert_redirected_to users_path
assert_redirected_to account_settings_path
assert_nil User.active.find_by(id: users(:david).id)
end
-4
View File
@@ -1,4 +0,0 @@
logo:
id: <%= ActiveRecord::FixtureSet.identify("logo_engagement", :uuid) %>
account: 37s_uuid
card: logo_uuid
+28
View File
@@ -31,4 +31,32 @@ class Card::CloseableTest < ActiveSupport::TestCase
assert cards(:shipping).reload.open?
assert cards(:shipping).events.last.action.card_reopened?
end
test "close card from triage column" do
card = cards(:logo)
assert_equal columns(:writebook_triage), card.column
card.close
assert card.closed?
end
test "close card from active column" do
card = cards(:text)
assert_equal columns(:writebook_in_progress), card.column
card.close
assert card.closed?
end
test "close card from NOT NOW" do
card = cards(:logo)
card.postpone
assert card.postponed?
assert card.not_now.present?
card.close
assert card.closed?
assert_nil card.reload.not_now
end
end
+4 -7
View File
@@ -4,15 +4,12 @@ class Filter::SearchTest < ActiveSupport::TestCase
include SearchTestHelper
test "deduplicate multiple results" do
user = users(:david)
board = boards(:writebook)
card = board.cards.create!(title: "Duplicate results test", description: "Have you had any haggis today?", creator: user)
card = @board.cards.create!(title: "Duplicate results test", description: "Have you had any haggis today?", creator: @user)
card.published!
card.comments.create(body: "I hate haggis.", creator: user)
card.comments.create(body: "I love haggis.", creator: user)
card.comments.create(body: "I hate haggis.", creator: @user)
card.comments.create(body: "I love haggis.", creator: @user)
filter = user.filters.new(terms: [ "haggis" ], indexed_by: "all", sorted_by: "latest")
filter = @user.filters.new(terms: [ "haggis" ], indexed_by: "all", sorted_by: "latest")
assert_equal [ card ], filter.cards.to_a
end