Merge branch 'main' into mobile/bridge-components
* main: (65 commits) 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 Use notch class so Save button is positioned correctly Go back to board after clearing filters Revert "Use existing no_filtering_url to direct back to the board when clearing filters" Use existing no_filtering_url to direct back to the board when clearing filters Ensure filters sit on top of cards Fix public boards Fix card grid layout Fix Chinese/Japanese characters missing from printed PDFs on macOS Add SQLite FTS5 support for CJK search Add CJK (Chinese, Japanese, Korean) search support Fix scrolling on mobile Remember expanded state on navigation ...
This commit is contained in:
@@ -207,7 +207,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));
|
||||
|
||||
@@ -102,7 +102,8 @@
|
||||
}
|
||||
|
||||
/* Turbo */
|
||||
turbo-frame {
|
||||
turbo-frame,
|
||||
turbo-cable-stream-source {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cards--considering & {
|
||||
.cards--maybe & {
|
||||
background-color: var(--card-bg-color) !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,5 @@
|
||||
outline-offset: -2px;
|
||||
transition: background-color 200ms;
|
||||
z-index: 1;
|
||||
|
||||
.cards__heading {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
}
|
||||
|
||||
.notifications__status {
|
||||
--panel-border-radius: 0.5em;
|
||||
--panel-padding: var(--block-space);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
--color-canvas: white;
|
||||
--border-dark: 1px solid var(--color-ink);
|
||||
--border-light: 1px solid color-mix(in oklch, var(--color-ink), transparent 75%);
|
||||
--font-sans: "Adwaita Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Hiragino Sans GB", "Hiragino Sans", "Apple SD Gothic Neo", "Microsoft YaHei", "Meiryo", "Malgun Gothic";
|
||||
}
|
||||
|
||||
@page {
|
||||
@@ -125,7 +126,7 @@
|
||||
.card--new,
|
||||
.cards__decoration,
|
||||
.card-columns:before,
|
||||
.cards--considering:before {
|
||||
.cards--maybe:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -140,7 +141,7 @@
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.cards--considering {
|
||||
.cards--maybe {
|
||||
background: none;
|
||||
margin: 0;
|
||||
padding-inline: 1ch;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -12,7 +12,7 @@ class CardsController < ApplicationController
|
||||
def create
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
card = @board.cards.find_or_create_by!(creator: Current.user, status: "drafted")
|
||||
card = Current.user.draft_new_card_in(@board)
|
||||
redirect_to card
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" }) +
|
||||
|
||||
@@ -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"]')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
module Search
|
||||
CJK_PATTERN = /\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}/
|
||||
|
||||
def self.table_name_prefix
|
||||
"search_"
|
||||
end
|
||||
|
||||
@@ -13,8 +13,14 @@ class Search::Highlighter
|
||||
result = text.dup
|
||||
|
||||
terms.each do |term|
|
||||
result.gsub!(/\b(#{Regexp.escape(term)}\w*)\b/i) do |match|
|
||||
"#{OPENING_MARK}#{match}#{CLOSING_MARK}"
|
||||
if term.match?(Search::CJK_PATTERN)
|
||||
result.gsub!(/(#{Regexp.escape(term)})/i) do |match|
|
||||
"#{OPENING_MARK}#{match}#{CLOSING_MARK}"
|
||||
end
|
||||
else
|
||||
result.gsub!(/\b(#{Regexp.escape(term)}\w*)\b/i) do |match|
|
||||
"#{OPENING_MARK}#{match}#{CLOSING_MARK}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class Search::Query < ApplicationRecord
|
||||
end
|
||||
|
||||
def remove_invalid_search_characters(terms)
|
||||
terms.gsub(/[^\w"]/, " ")
|
||||
terms.gsub(/[^\p{L}\p{N}_"]/, " ")
|
||||
end
|
||||
|
||||
def remove_unbalanced_quotes(terms)
|
||||
|
||||
@@ -8,9 +8,10 @@ module Search::Record::SQLite
|
||||
has_one :search_records_fts, -> { with_rowid },
|
||||
class_name: "Search::Record::SQLite::Fts", foreign_key: :rowid, primary_key: :id, dependent: :destroy
|
||||
|
||||
before_save :stem_content
|
||||
after_save :upsert_to_fts5_table
|
||||
|
||||
scope :matching, ->(query, account_id) { joins(:search_records_fts).where("search_records_fts MATCH ?", query) }
|
||||
scope :matching, ->(query, account_id) { joins(:search_records_fts).where("search_records_fts MATCH ?", Search::Stemmer.stem(query.to_s)) }
|
||||
end
|
||||
|
||||
class_methods do
|
||||
@@ -42,6 +43,11 @@ module Search::Record::SQLite
|
||||
end
|
||||
|
||||
private
|
||||
def stem_content
|
||||
self.title = Search::Stemmer.stem(title) if title_changed?
|
||||
self.content = Search::Stemmer.stem(content) if content_changed?
|
||||
end
|
||||
|
||||
def escape_fts_highlight(html)
|
||||
return nil unless html.present?
|
||||
|
||||
|
||||
@@ -5,9 +5,43 @@ module Search::Stemmer
|
||||
|
||||
def stem(value)
|
||||
if value.present?
|
||||
value.gsub(/[^\w\s]/, "").split(/\s+/).map { |word| STEMMER.stem(word.downcase) }.join(" ")
|
||||
tokenize(value).join(" ")
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def tokenize(value)
|
||||
tokens = []
|
||||
current_word = +""
|
||||
|
||||
value.each_char do |char|
|
||||
if cjk_character?(char)
|
||||
if current_word.present?
|
||||
tokens << stem_word(current_word)
|
||||
current_word = +""
|
||||
end
|
||||
tokens << char
|
||||
elsif char =~ /[\p{L}\p{N}_]/
|
||||
current_word << char
|
||||
else
|
||||
if current_word.present?
|
||||
tokens << stem_word(current_word)
|
||||
current_word = +""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
tokens << stem_word(current_word) if current_word.present?
|
||||
tokens
|
||||
end
|
||||
|
||||
def cjk_character?(char)
|
||||
char.match?(Search::CJK_PATTERN)
|
||||
end
|
||||
|
||||
def stem_word(word)
|
||||
STEMMER.stem(word.downcase)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,6 +11,12 @@ module User::Accessor
|
||||
after_create_commit :grant_access_to_boards, unless: :system?
|
||||
end
|
||||
|
||||
def draft_new_card_in(board)
|
||||
board.cards.find_or_initialize_by(creator: self, status: "drafted").tap do |card|
|
||||
card.update!(created_at: Time.current, updated_at: Time.current, last_active_at: Time.current)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def grant_access_to_boards
|
||||
Access.insert_all account.boards.all_access.ids.collect { |board_id| { id: ActiveRecord::Type::Uuid.generate, board_id: board_id, user_id: id, account_id: account.id } }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<% @page_title = @board.name %>
|
||||
<% @body_class = "contained-scrolling" %>
|
||||
<% turbo_exempts_page_from_cache %>
|
||||
|
||||
<%= turbo_stream_from @board %>
|
||||
@@ -27,7 +28,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 %>
|
||||
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="flex gap-half">
|
||||
<div class="card-perma__notch card-perma__notch--bottom flex-column">
|
||||
<%= button_tag type: :submit, form: dom_id(card, :edit_form), class: "btn borderless",
|
||||
title: "Save changes (#{ hotkey_label(["ctrl", "enter"]) })",
|
||||
data: { controller: "hotkey", action: "keydown.ctrl+enter@document->hotkey#click keydown.meta+enter@document->hotkey#click" } do %>
|
||||
|
||||
@@ -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 @@
|
||||
<%= 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,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 %>
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
<html lang="en">
|
||||
<%= render "layouts/shared/head" %>
|
||||
|
||||
<body data-controller="bridge--page bridge--insets 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 %>" <%= "data-bridge--page-title-value=#{@page_title}" if @page_title %>>
|
||||
<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="bridge--page bridge--insets 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 %>" <%= "data-bridge--page-title-value=#{@page_title}" if @page_title %>>
|
||||
<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 %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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" data-bridge--page-target="header"><%= @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">You’re unsubscribed</h1>
|
||||
<p class="margin-block-start-half">Thanks, <%= @user.first_name %>! Fizzy won’t 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 won’t 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,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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
logo:
|
||||
id: <%= ActiveRecord::FixtureSet.identify("logo_engagement", :uuid) %>
|
||||
account: 37s_uuid
|
||||
card: logo_uuid
|
||||
@@ -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,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
|
||||
|
||||
@@ -82,6 +82,34 @@ class Search::HighlighterTest < ActiveSupport::TestCase
|
||||
assert_equal "<script>#{mark('test')}</script>", result
|
||||
end
|
||||
|
||||
test "highlight Chinese characters" do
|
||||
highlighter = Search::Highlighter.new("测试")
|
||||
result = highlighter.highlight("这是一个测试文本")
|
||||
|
||||
assert_equal "这是一个#{mark('测试')}文本", result
|
||||
end
|
||||
|
||||
test "highlight Japanese characters" do
|
||||
highlighter = Search::Highlighter.new("テスト")
|
||||
result = highlighter.highlight("これはテストです")
|
||||
|
||||
assert_equal "これは#{mark('テスト')}です", result
|
||||
end
|
||||
|
||||
test "highlight Korean characters" do
|
||||
highlighter = Search::Highlighter.new("테스트")
|
||||
result = highlighter.highlight("이것은 테스트입니다")
|
||||
|
||||
assert_equal "이것은 #{mark('테스트')}입니다", result
|
||||
end
|
||||
|
||||
test "highlight mixed CJK and English" do
|
||||
highlighter = Search::Highlighter.new("world 世界")
|
||||
result = highlighter.highlight("hello world 你好世界")
|
||||
|
||||
assert_equal "hello #{mark('world')} 你好#{mark('世界')}", result
|
||||
end
|
||||
|
||||
private
|
||||
def mark(text)
|
||||
"#{Search::Highlighter::OPENING_MARK}#{text}#{Search::Highlighter::CLOSING_MARK}"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
require "test_helper"
|
||||
|
||||
class Search::QueryTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@account = accounts(:"37s")
|
||||
Current.account = @account
|
||||
end
|
||||
|
||||
test "sanitize preserves ASCII words" do
|
||||
query = build_query("hello world")
|
||||
|
||||
assert_equal "hello world", query.terms
|
||||
end
|
||||
|
||||
test "sanitize preserves Chinese characters" do
|
||||
query = build_query("测试文本")
|
||||
|
||||
assert_equal "测试文本", query.terms
|
||||
end
|
||||
|
||||
test "sanitize preserves Japanese characters" do
|
||||
query = build_query("テスト")
|
||||
|
||||
assert_equal "テスト", query.terms
|
||||
end
|
||||
|
||||
test "sanitize preserves Korean characters" do
|
||||
query = build_query("테스트")
|
||||
|
||||
assert_equal "테스트", query.terms
|
||||
end
|
||||
|
||||
test "sanitize preserves mixed CJK and English" do
|
||||
query = build_query("hello 世界 test")
|
||||
|
||||
assert_equal "hello 世界 test", query.terms
|
||||
end
|
||||
|
||||
test "sanitize removes special characters but preserves CJK" do
|
||||
query = build_query("测试@文本")
|
||||
|
||||
assert_equal "测试 文本", query.terms
|
||||
end
|
||||
|
||||
test "sanitize preserves quoted phrases with CJK" do
|
||||
query = build_query('"你好世界"')
|
||||
|
||||
assert_equal '"你好世界"', query.terms
|
||||
end
|
||||
|
||||
private
|
||||
def build_query(terms)
|
||||
query = Search::Query.wrap(terms)
|
||||
query.validate
|
||||
query
|
||||
end
|
||||
end
|
||||
@@ -12,4 +12,40 @@ class Search::StemmerTest < ActiveSupport::TestCase
|
||||
|
||||
assert_equal "test run jump walk", result
|
||||
end
|
||||
|
||||
test "split Chinese characters for FTS indexing" do
|
||||
result = Search::Stemmer.stem("测试")
|
||||
|
||||
assert_equal "测 试", result
|
||||
end
|
||||
|
||||
test "split Japanese characters for FTS indexing" do
|
||||
result = Search::Stemmer.stem("テスト")
|
||||
|
||||
assert_equal "テ ス ト", result
|
||||
end
|
||||
|
||||
test "split Korean characters for FTS indexing" do
|
||||
result = Search::Stemmer.stem("테스트")
|
||||
|
||||
assert_equal "테 스 트", result
|
||||
end
|
||||
|
||||
test "mixed CJK and English" do
|
||||
result = Search::Stemmer.stem("running 测试 test")
|
||||
|
||||
assert_equal "run 测 试 test", result
|
||||
end
|
||||
|
||||
test "mixed CJK and English without spaces" do
|
||||
result = Search::Stemmer.stem("hello世界test")
|
||||
|
||||
assert_equal "hello 世 界 test", result
|
||||
end
|
||||
|
||||
test "CJK punctuation is treated as separator" do
|
||||
result = Search::Stemmer.stem("你好。世界")
|
||||
|
||||
assert_equal "你 好 世 界", result
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,4 +12,36 @@ class User::AccessorTest < ActiveSupport::TestCase
|
||||
system_user = User.create!(account: accounts("37s"), role: "system", name: "Test System User")
|
||||
assert_empty system_user.boards
|
||||
end
|
||||
|
||||
test "creating a new card draft sets current timestamps" do
|
||||
user = users(:david)
|
||||
board = boards(:writebook)
|
||||
|
||||
freeze_time do
|
||||
card = user.draft_new_card_in(board)
|
||||
|
||||
assert card.persisted?
|
||||
assert card.drafted?
|
||||
assert_equal user, card.creator
|
||||
assert_equal board, card.board
|
||||
assert_equal Time.current, card.created_at
|
||||
assert_equal Time.current, card.updated_at
|
||||
assert_equal Time.current, card.last_active_at
|
||||
end
|
||||
end
|
||||
|
||||
test "reusing an existing card draft refreshes timestamps" do
|
||||
existing_draft = cards(:unfinished_thoughts)
|
||||
user = existing_draft.creator
|
||||
board = existing_draft.board
|
||||
|
||||
freeze_time do
|
||||
card = user.draft_new_card_in(board)
|
||||
|
||||
assert_equal existing_draft, card
|
||||
assert_equal Time.current, card.created_at
|
||||
assert_equal Time.current, card.updated_at
|
||||
assert_equal Time.current, card.last_active_at
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user