Merge branch 'main' into focus-hover-styles
This commit is contained in:
+30
-30
@@ -1,13 +1,13 @@
|
||||
GIT
|
||||
remote: https://github.com/basecamp/actiontext-lexical
|
||||
revision: 8a2cede49ca51e40004a0f03100b3f189b60fdd8
|
||||
revision: 38e8d4e5781667b7d1c17eaa5cca503bbc63ee67
|
||||
specs:
|
||||
actiontext-lexical (0.1.0)
|
||||
rails (>= 8.0.2)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/active_record-tenanted
|
||||
revision: df260821fcc6fa7aee355623c70cdd4fe21d2d71
|
||||
revision: 8e33088cdbd69ab7b4cf4100d4202a54aa97e4c0
|
||||
branch: flavorjones/path-based-tenanting
|
||||
specs:
|
||||
active_record-tenanted (0.1.0)
|
||||
@@ -18,9 +18,9 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/file_repository
|
||||
revision: 17e90fc35ccb9f0a0dae1ed8623f01386696002d
|
||||
revision: 8c8d1594f3ec7e8c0938e6a7f7def521692344e4
|
||||
specs:
|
||||
file_repository (1.4.5)
|
||||
file_repository (1.4.6)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/kamal
|
||||
@@ -41,7 +41,7 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/portfolio
|
||||
revision: 26dd0ccbe4aad5a5c539f90c00e82b6175443636
|
||||
revision: 34b48dd3c6dc78abd162ef26481971a228084d04
|
||||
specs:
|
||||
portfolio (5.2.0)
|
||||
activesupport
|
||||
@@ -82,7 +82,7 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/signal_id
|
||||
revision: 8908b2550ff55325b04c108146e00b4408a64e93
|
||||
revision: 32758d0bff9f17e938905ff5bec53fff359e21b0
|
||||
branch: rails4
|
||||
specs:
|
||||
signal_id (4.3.2)
|
||||
@@ -112,7 +112,7 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rails/rails.git
|
||||
revision: 53a9a3181a59f4e6dff73ba9453734edd7f7e076
|
||||
revision: 58a2187f7b2ac391eba841e9ec6bb1a6d4e486dd
|
||||
branch: main
|
||||
specs:
|
||||
actioncable (8.1.0.alpha)
|
||||
@@ -227,18 +227,18 @@ GEM
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ast (2.4.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1119.0)
|
||||
aws-sdk-core (3.226.0)
|
||||
aws-partitions (1.1124.0)
|
||||
aws-sdk-core (3.226.2)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.105.0)
|
||||
aws-sdk-kms (1.106.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.190.0)
|
||||
aws-sdk-s3 (1.192.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -291,11 +291,11 @@ GEM
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-multipart (1.1.0)
|
||||
faraday-multipart (1.1.1)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (3.4.0)
|
||||
faraday-net_http (3.4.1)
|
||||
net-http (>= 0.5.0)
|
||||
faraday-retry (2.3.1)
|
||||
faraday-retry (2.3.2)
|
||||
faraday (~> 2.0)
|
||||
ffi (1.17.2)
|
||||
ffi (1.17.2-arm64-darwin)
|
||||
@@ -331,7 +331,7 @@ GEM
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.12.2)
|
||||
jwt (2.10.1)
|
||||
jwt (3.1.2)
|
||||
base64
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
@@ -345,7 +345,7 @@ GEM
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
matrix (0.4.3)
|
||||
mini_magick (5.2.0)
|
||||
benchmark
|
||||
logger
|
||||
@@ -394,7 +394,7 @@ GEM
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
ostruct (0.6.1)
|
||||
ostruct (0.6.2)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.8.0)
|
||||
ast (~> 2.4.1)
|
||||
@@ -457,7 +457,7 @@ GEM
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.0.0)
|
||||
rubocop (1.75.8)
|
||||
rubocop (1.77.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -465,10 +465,10 @@ GEM
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-ast (>= 1.45.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.44.1)
|
||||
rubocop-ast (1.45.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-performance (1.25.0)
|
||||
@@ -486,25 +486,25 @@ GEM
|
||||
rubocop-performance (>= 1.24)
|
||||
rubocop-rails (>= 2.30)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.3)
|
||||
ruby-vips (2.2.4)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.33.0)
|
||||
selenium-webdriver (4.34.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
sentry-rails (5.25.0)
|
||||
sentry-rails (5.26.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.25.0)
|
||||
sentry-ruby (5.25.0)
|
||||
sentry-ruby (~> 5.26.0)
|
||||
sentry-ruby (5.26.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
solid_cable (3.0.8)
|
||||
solid_cable (3.0.11)
|
||||
actioncable (>= 7.2)
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
@@ -520,11 +520,11 @@ GEM
|
||||
fugit (~> 1.11.0)
|
||||
railties (>= 7.1)
|
||||
thor (~> 1.3.1)
|
||||
sqlite3 (2.7.0)
|
||||
sqlite3 (2.7.1)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
sqlite3 (2.7.0-arm64-darwin)
|
||||
sqlite3 (2.7.0-x86_64-darwin)
|
||||
sqlite3 (2.7.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.7.1-arm64-darwin)
|
||||
sqlite3 (2.7.1-x86_64-darwin)
|
||||
sqlite3 (2.7.1-x86_64-linux-gnu)
|
||||
sshkit (1.24.0)
|
||||
base64
|
||||
logger
|
||||
|
||||
@@ -42,8 +42,8 @@ Comment.find_each(&:refresh_search_embedding)
|
||||
### Tests
|
||||
|
||||
For testing OpenAI API requests, we use [VCR](https://github.com/vcr/vcr). If you want to test AI features exercising the API, you need to place the `config/credentials/test.key`
|
||||
that you can get from 1Password in "Fizzy - test.key". Then, when running tests that use Open AI API, you must either set the env variable VCR_RECORD=1
|
||||
or to add `vcr_record!` to the test. See `VcrTestHelper`.
|
||||
that you can get from 1Password in "Fizzy - test.key". Then, when running tests that use Open AI API, you must either set the env variable `VCR_RECORD=1`
|
||||
or to add `vcr_record!` to the test. See `VcrTestHelper`. Due to Open AI quotas, you may need to limit the parallelization. E.g: `PARALLEL_WORKERS=2`
|
||||
|
||||
## Environments
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
|
||||
.node--selected:not(.attachment--custom) {
|
||||
figure.node--selected {
|
||||
&:not(:has(img)) {
|
||||
box-shadow: 0 0 0 var(--focus-ring-size) var(--focus-ring-color);
|
||||
}
|
||||
@@ -148,6 +148,8 @@
|
||||
background-color: var(--color-canvas);
|
||||
border-radius: calc(var(--lexical-prompt-padding) * 2);
|
||||
box-shadow: var(--shadow);
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-small);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
@@ -155,6 +157,9 @@
|
||||
padding: var(--lexical-prompt-padding);
|
||||
visibility: hidden;
|
||||
z-index: var(--z-popup);
|
||||
|
||||
max-height: 200px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.lexical-prompt-menu--visible {
|
||||
@@ -188,6 +193,13 @@
|
||||
+ & {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--color-terminal-text-light);
|
||||
border-radius: 0.25ch;
|
||||
font-size: 0.95em;
|
||||
padding-inline: 0.5ch;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
|
||||
@@ -50,4 +50,10 @@
|
||||
@keyframes zoom-fade {
|
||||
100% { transform: translateY(-1.5em); scale: 2; opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
.card__background {
|
||||
.card:has(& img:not([src=""])) {
|
||||
.card__body,
|
||||
.card__content,
|
||||
.card__meta,
|
||||
.workflow-stage:not(.workflow-stage--current) {
|
||||
opacity: 0;
|
||||
@@ -108,6 +108,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card__steps {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* Column elements
|
||||
@@ -200,7 +204,7 @@
|
||||
|
||||
.card__header {
|
||||
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
|
||||
margin-inline-start: calc(-0.5 * var(--card-padding-inline));
|
||||
margin-inline: calc(-0.5 * var(--card-padding-inline)) calc(-0.25 * var(--card-padding-inline));
|
||||
}
|
||||
|
||||
.card__people-label {
|
||||
|
||||
@@ -136,8 +136,8 @@
|
||||
flex-wrap: nowrap;
|
||||
gap: var(--inline-space);
|
||||
margin-block-start: calc(-1 * var(--card-padding-block));
|
||||
margin-inline-start: calc(-1 * var(--card-padding-inline));
|
||||
max-inline-size: 100%;
|
||||
margin-inline: calc(-1 * var(--card-padding-inline)) calc(-0.5 * var(--card-padding-inline));
|
||||
max-inline-size: unset;
|
||||
min-inline-size: 0;
|
||||
|
||||
.workflow-stage {
|
||||
|
||||
@@ -37,8 +37,14 @@
|
||||
}
|
||||
|
||||
.comment__author {
|
||||
.comment--system & strong {
|
||||
display: none;
|
||||
.comment--system & {
|
||||
h3 {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
.icon--camera { --svg: url("camera.svg "); }
|
||||
.icon--caret-down { --svg: url("caret-down.svg "); }
|
||||
.icon--check { --svg: url("check.svg "); }
|
||||
.icon--check-all { --svg: url("check-all.svg "); }
|
||||
.icon--clipboard { --svg: url("clipboard.svg "); }
|
||||
.icon--close { --svg: url("close.svg "); }
|
||||
.icon--collection { --svg: url("collection.svg "); }
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
}
|
||||
|
||||
.input--file {
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
inline-size: auto;
|
||||
@@ -46,6 +45,7 @@
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
--hover-size: 0;
|
||||
--input-border-color: transparent;
|
||||
--input-border-radius: 8px;
|
||||
|
||||
@@ -63,9 +63,9 @@
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
--input-border-color: var(--color-selected-dark);
|
||||
--input-border-radius: 8px;
|
||||
--input-border-size: 0.15rem;
|
||||
.input--file:has(&) {
|
||||
outline: 0.15rem solid var(--color-selected-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
:where(h5) { font-size: 0.83em; }
|
||||
:where(h6) { font-size: 0.67em; }
|
||||
|
||||
:where(p, ul, ol, dl, blockquote, figure:not(.attachment--custom), .attachment:not(.attachment--custom)) {
|
||||
:where(p, ul, ol, dl, blockquote, figure, .attachment) {
|
||||
margin-block: var(--block-margin);
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
@@ -53,167 +53,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Attachments */
|
||||
.attachment:not(.attachment--custom) {
|
||||
block-size: auto;
|
||||
display: inline-block;
|
||||
inline-size: 100%;
|
||||
position: relative;
|
||||
margin-inline: 0;
|
||||
max-inline-size: 100%;
|
||||
|
||||
&:is(figure) img,
|
||||
&:is(figure) video {
|
||||
block-size: auto;
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
max-inline-size: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:where(progress) {
|
||||
inline-size: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment__caption {
|
||||
color: var(--color-ink-dark);
|
||||
display: flex;
|
||||
font-size: var(--text-small);
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.3;
|
||||
column-gap: 0.5ch;
|
||||
justify-content: center;
|
||||
margin: var(--block-space-half) var(--inline-space);
|
||||
text-align: center;
|
||||
|
||||
.input {
|
||||
inline-size: 100%;
|
||||
max-inline-size: 100%;
|
||||
text-align: center;
|
||||
|
||||
@supports (field-sizing: content) {
|
||||
field-sizing: content;
|
||||
inline-size: auto;
|
||||
min-inline-size: 20ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment--file {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
|
||||
.attachment__caption {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
text-align: start;
|
||||
|
||||
strong {
|
||||
color: var(--color-ink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment__icon {
|
||||
aspect-ratio: 4/5;
|
||||
background-color: var(--color-canvas);
|
||||
block-size: calc(2.5lh + 0.5ch);
|
||||
border: 2px solid var(--color-ink-medium);
|
||||
border-radius: 0.3em;
|
||||
color: var(--color-ink-dark);
|
||||
display: inline-grid;
|
||||
font-size: var(--text-small);
|
||||
font-weight: 800;
|
||||
inline-size: auto;
|
||||
overflow: clip;
|
||||
padding-block-start: 0.4lh;
|
||||
place-content: center;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-ink-medium);
|
||||
block-size: 0.5lh;
|
||||
content: "";
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
inset: 0 0 auto;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.attachment--psd &,
|
||||
.attachment--key &,
|
||||
.attachment--sketch &,
|
||||
.attachment--ai &,
|
||||
.attachment--eps &,
|
||||
.attachment--indd &,
|
||||
.attachment--svg &,
|
||||
.attachment--ppt &,
|
||||
.attachment--pptx & {
|
||||
color: oklch(var(--lch-red-dark));
|
||||
background-color: oklch(var(--lch-red-lightest));
|
||||
border-color: oklch(var(--lch-red-medium));
|
||||
|
||||
&::before {
|
||||
background-color: oklch(var(--lch-red-medium));
|
||||
}
|
||||
}
|
||||
|
||||
.attachment--css &,
|
||||
.attachment--php &,
|
||||
.attachment--json &,
|
||||
.attachment--htm &,
|
||||
.attachment--html &,
|
||||
.attachment--rb &,
|
||||
.attachment--erb &,
|
||||
.attachment--ts &,
|
||||
.attachment--js & {
|
||||
color: oklch(var(--lch-purple-dark));
|
||||
background-color: oklch(var(--lch-purple-lightest));
|
||||
border-color: oklch(var(--lch-purple-medium));
|
||||
|
||||
&::before {
|
||||
background-color: oklch(var(--lch-purple-medium));
|
||||
}
|
||||
}
|
||||
|
||||
.attachment--txt &,
|
||||
.attachment--pages &,
|
||||
.attachment--rtf &,
|
||||
.attachment--md &,
|
||||
.attachment--doc &,
|
||||
.attachment--docx & {
|
||||
color: oklch(var(--lch-blue-dark));
|
||||
background-color: oklch(var(--lch-blue-lightest));
|
||||
border-color: oklch(var(--lch-blue-medium));
|
||||
|
||||
&::before {
|
||||
background-color: oklch(var(--lch-blue-medium));
|
||||
}
|
||||
}
|
||||
|
||||
.attachment--csv &,
|
||||
.attachment--numbers &,
|
||||
.attachment--xls &,
|
||||
.attachment--xlsx & {
|
||||
color: oklch(var(--lch-green-dark));
|
||||
background-color: oklch(var(--lch-green-lightest));
|
||||
border-color: oklch(var(--lch-green-medium));
|
||||
|
||||
&::before {
|
||||
background-color: oklch(var(--lch-green-medium));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment__link {
|
||||
color: var(--color-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Avoid extra space due to empty paragraphs */
|
||||
p:empty {
|
||||
display: none;
|
||||
@@ -314,44 +153,221 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom attachments such as mentions, etc. */
|
||||
.attachment--custom {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
white-space: normal;
|
||||
/* Attachments
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
.attachment {
|
||||
block-size: auto;
|
||||
display: inline-block;
|
||||
inline-size: 100%;
|
||||
position: relative;
|
||||
margin-inline: 0;
|
||||
max-inline-size: 100%;
|
||||
|
||||
&:is(figure) img,
|
||||
&:is(figure) video {
|
||||
block-size: auto;
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
max-inline-size: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.attachment--custom,
|
||||
action-text-attachment[content-type^='application/vnd.actiontext'] {
|
||||
--attachment-image-size: 1em;
|
||||
--attachment-selected-color: oklch(var(--lch-blue-dark));
|
||||
:where(progress) {
|
||||
inline-size: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: 0.25ch;
|
||||
position: relative;
|
||||
vertical-align: bottom;
|
||||
.attachment__caption {
|
||||
color: var(--color-ink-dark);
|
||||
display: flex;
|
||||
font-size: var(--text-small);
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.3;
|
||||
column-gap: 0.5ch;
|
||||
justify-content: center;
|
||||
margin: var(--block-space-half) var(--inline-space);
|
||||
text-align: center;
|
||||
|
||||
lexical-editor & {
|
||||
cursor: pointer;
|
||||
}
|
||||
.input {
|
||||
--input-border-radius: 0.3em;
|
||||
--input-border-size: 0;
|
||||
--input-padding: 0;
|
||||
|
||||
img {
|
||||
block-size: var(--attachment-image-size);
|
||||
border-radius: 50%;
|
||||
inline-size: var(--attachment-image-size);
|
||||
}
|
||||
color: var(--color-ink-dark);
|
||||
inline-size: 100%;
|
||||
max-inline-size: 100%;
|
||||
text-align: center;
|
||||
|
||||
&.node--selected {
|
||||
background: var(--attachment-selected-color);
|
||||
box-shadow:
|
||||
-0.25ch 0 0 var(--attachment-selected-color),
|
||||
0.5ch 0 0 var(--attachment-selected-color);
|
||||
border-radius: 99rem;
|
||||
color: var(--color-ink-inverted);
|
||||
@supports (field-sizing: content) {
|
||||
field-sizing: content;
|
||||
inline-size: auto;
|
||||
min-inline-size: 20ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment--file {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
|
||||
.attachment__caption {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
text-align: start;
|
||||
|
||||
strong {
|
||||
color: var(--color-ink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment__icon {
|
||||
aspect-ratio: 4/5;
|
||||
background-color: var(--color-canvas);
|
||||
block-size: calc(2.5lh + 0.5ch);
|
||||
border: 2px solid var(--color-ink-medium);
|
||||
border-radius: 0.3em;
|
||||
color: var(--color-ink-dark);
|
||||
display: inline-grid;
|
||||
font-size: var(--text-small);
|
||||
font-weight: 800;
|
||||
inline-size: auto;
|
||||
overflow: clip;
|
||||
padding-block-start: 0.4lh;
|
||||
place-content: center;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-ink-medium);
|
||||
block-size: 0.5lh;
|
||||
content: "";
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
inset: 0 0 auto;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.attachment--psd &,
|
||||
.attachment--key &,
|
||||
.attachment--sketch &,
|
||||
.attachment--ai &,
|
||||
.attachment--eps &,
|
||||
.attachment--indd &,
|
||||
.attachment--svg &,
|
||||
.attachment--ppt &,
|
||||
.attachment--pptx & {
|
||||
color: oklch(var(--lch-red-dark));
|
||||
background-color: oklch(var(--lch-red-lightest));
|
||||
border-color: oklch(var(--lch-red-medium));
|
||||
|
||||
&::before {
|
||||
background-color: oklch(var(--lch-red-medium));
|
||||
}
|
||||
}
|
||||
|
||||
.attachment--css &,
|
||||
.attachment--php &,
|
||||
.attachment--json &,
|
||||
.attachment--htm &,
|
||||
.attachment--html &,
|
||||
.attachment--rb &,
|
||||
.attachment--erb &,
|
||||
.attachment--ts &,
|
||||
.attachment--js & {
|
||||
color: oklch(var(--lch-purple-dark));
|
||||
background-color: oklch(var(--lch-purple-lightest));
|
||||
border-color: oklch(var(--lch-purple-medium));
|
||||
|
||||
&::before {
|
||||
background-color: oklch(var(--lch-purple-medium));
|
||||
}
|
||||
}
|
||||
|
||||
.attachment--txt &,
|
||||
.attachment--pages &,
|
||||
.attachment--rtf &,
|
||||
.attachment--md &,
|
||||
.attachment--doc &,
|
||||
.attachment--docx & {
|
||||
color: oklch(var(--lch-blue-dark));
|
||||
background-color: oklch(var(--lch-blue-lightest));
|
||||
border-color: oklch(var(--lch-blue-medium));
|
||||
|
||||
&::before {
|
||||
background-color: oklch(var(--lch-blue-medium));
|
||||
}
|
||||
}
|
||||
|
||||
.attachment--csv &,
|
||||
.attachment--numbers &,
|
||||
.attachment--xls &,
|
||||
.attachment--xlsx & {
|
||||
color: oklch(var(--lch-green-dark));
|
||||
background-color: oklch(var(--lch-green-lightest));
|
||||
border-color: oklch(var(--lch-green-medium));
|
||||
|
||||
&::before {
|
||||
background-color: oklch(var(--lch-green-medium));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment__link {
|
||||
color: var(--color-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Custom attachments such as mentions, etc. */
|
||||
action-text-attachment[content-type^='application/vnd.actiontext'] {
|
||||
--attachment-bg-color: transparent;
|
||||
--attachment-image-size: 1em;
|
||||
--attachment-text-color: currentColor;
|
||||
|
||||
display: inline;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
white-space: normal;
|
||||
|
||||
align-items: center;
|
||||
color: var(--attachment-text-color);
|
||||
display: inline-flex;
|
||||
gap: 0.25ch;
|
||||
position: relative;
|
||||
vertical-align: bottom;
|
||||
|
||||
background: var(--attachment-bg-color);
|
||||
box-shadow:
|
||||
-0.25ch 0 0 var(--attachment-bg-color),
|
||||
0.5ch 0 0 var(--attachment-bg-color);
|
||||
border-radius: 99rem;
|
||||
|
||||
lexical-editor & {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img {
|
||||
block-size: var(--attachment-image-size);
|
||||
border-radius: 50%;
|
||||
inline-size: var(--attachment-image-size);
|
||||
}
|
||||
|
||||
&.node--selected {
|
||||
--attachment-bg-color: oklch(var(--lch-blue-dark));
|
||||
--attachment-text-color: var(--color-ink-inverted);
|
||||
}
|
||||
}
|
||||
|
||||
[data-lexical-cursor] {
|
||||
animation: blink 1s step-end infinite;
|
||||
block-size: 1lh;
|
||||
border-inline-start: 1px solid currentColor;
|
||||
line-height: inherit;
|
||||
margin-block: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
@layer components {
|
||||
.step {
|
||||
display: grid;
|
||||
grid-template-columns: 1em auto auto;
|
||||
gap: calc(var(--inline-space) * 2/3);
|
||||
inline-size: auto;
|
||||
}
|
||||
|
||||
.step__checkbox {
|
||||
--hover-color: var(--card-color);
|
||||
|
||||
appearance: none;
|
||||
background-color: var(--color-canvas);
|
||||
block-size: 1.1em;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 0.15em;
|
||||
color: currentColor;
|
||||
display: grid;
|
||||
font: inherit;
|
||||
inline-size: 1.1em;
|
||||
margin: 0;
|
||||
place-content: center;
|
||||
transform: translateY(0.1em);
|
||||
|
||||
&::before {
|
||||
background-color: CanvasText;
|
||||
block-size: 0.65em;
|
||||
box-shadow: inset 1em 1em currentColor;
|
||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
||||
content: "";
|
||||
inline-size: 0.65em;
|
||||
transform: scale(0);
|
||||
transform-origin: center;
|
||||
transition: 150ms transform ease-in-out;
|
||||
}
|
||||
|
||||
&:checked::before {
|
||||
transform: scale(1) rotate(10deg);
|
||||
}
|
||||
|
||||
&:where([disabled]):not(:hover):not(:active) {
|
||||
filter: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.step__content {
|
||||
--input-border-radius: 0;
|
||||
--input-border-size: 0;
|
||||
--input-padding: 0;
|
||||
|
||||
border-bottom: 1px solid transparent;
|
||||
color: currentColor;
|
||||
font-weight: 500;
|
||||
margin-block-end: calc(var(--block-space) * 1/3);
|
||||
|
||||
&:is(a, input[type=text]) {
|
||||
--hover-size: 0;
|
||||
}
|
||||
|
||||
.step:has(:checked) & {
|
||||
opacity: 0.7;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--card-color);
|
||||
}
|
||||
|
||||
&:is(input) {
|
||||
max-inline-size: 70ch;
|
||||
min-inline-size: 30ch;
|
||||
|
||||
@supports (field-sizing: content) {
|
||||
field-sizing: content;
|
||||
min-inline-size: 15ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step__content--edit {
|
||||
border-bottom-color: currentColor;
|
||||
}
|
||||
|
||||
.steps {
|
||||
contain: inline-size;
|
||||
display: grid;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
max-inline-size: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.steps__icon {
|
||||
background-color: var(--card-color);
|
||||
block-size: 1.3em;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
inline-size: 1.3em;
|
||||
place-content: center;
|
||||
|
||||
.icon {
|
||||
background-color: var(--color-ink-inverted);
|
||||
block-size: 0.8em;
|
||||
inline-size: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,22 @@
|
||||
caret-color: var(--color-negative);
|
||||
color: var(--color-negative);
|
||||
}
|
||||
|
||||
.lexical-editor__content {
|
||||
min-block-size: initial;
|
||||
margin: 0;
|
||||
|
||||
b, strong, i, em {
|
||||
font-weight: inherit;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
ul, li {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.terminal__label {
|
||||
@@ -183,4 +199,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Lexical prompt insertions
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
.terminal {
|
||||
action-text-attachment:not(.node--selected) {
|
||||
--attachment-bg-color: oklch(var(--lch-white) / 15%);
|
||||
--attachment-text-color: var(--color-terminal-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +310,7 @@
|
||||
.card__meta .btn,
|
||||
.card__meta-item:not(.card__meta-item--updated),
|
||||
.card__stages,
|
||||
.card__steps,
|
||||
.card__closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
class Cards::StepsController < ApplicationController
|
||||
include CardScoped
|
||||
|
||||
before_action :set_step, only: %i[ show edit update destroy ]
|
||||
|
||||
def create
|
||||
@step = @card.steps.create!(step_params)
|
||||
render_card_replacement
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@step.update!(step_params)
|
||||
render_card_replacement
|
||||
end
|
||||
|
||||
def destroy
|
||||
@step.destroy!
|
||||
render_card_replacement
|
||||
end
|
||||
|
||||
private
|
||||
def set_step
|
||||
@step = @card.steps.find(params[:id])
|
||||
end
|
||||
|
||||
def step_params
|
||||
params.expect(step: [ :content, :completed ])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
class Collections::EntropyConfigurationsController < ApplicationController
|
||||
include CollectionScoped
|
||||
|
||||
def update
|
||||
@collection.entropy_configuration.update!(entropy_configuration_params)
|
||||
|
||||
render turbo_stream: turbo_stream.replace([ @collection, :entropy_configuration ], partial: "collections/edit/auto_close", locals: { collection: @collection })
|
||||
end
|
||||
|
||||
private
|
||||
def entropy_configuration_params
|
||||
params.expect(collection: [ :auto_close_period, :auto_reconsider_period ])
|
||||
end
|
||||
end
|
||||
@@ -3,11 +3,12 @@ class Collections::PublicationsController < ApplicationController
|
||||
|
||||
def create
|
||||
@collection.publish
|
||||
redirect_to edit_collection_path(@collection)
|
||||
render turbo_stream: turbo_stream.replace([ @collection, :publication ], partial: "collections/edit/publication", locals: { collection: @collection })
|
||||
end
|
||||
|
||||
def destroy
|
||||
@collection.unpublish
|
||||
redirect_to edit_collection_path(@collection)
|
||||
@collection.reload
|
||||
render turbo_stream: turbo_stream.replace([ @collection, :publication ], partial: "collections/edit/publication", locals: { collection: @collection })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,11 +5,12 @@ class Collections::WorkflowsController < ApplicationController
|
||||
|
||||
def update
|
||||
@collection.update! workflow: @workflow
|
||||
redirect_to cards_path(collection_ids: [ @collection ])
|
||||
render turbo_stream: turbo_stream.replace([ @collection, :workflows ], partial: "collections/edit/workflows", locals: { collection: @collection })
|
||||
end
|
||||
|
||||
private
|
||||
def set_workflow
|
||||
@workflow = Workflow.find(params[:collection][:workflow_id])
|
||||
workflow_id = params[:collection][:workflow_id]
|
||||
@workflow = workflow_id.present? ? Workflow.find(workflow_id) : nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,6 +37,9 @@ class CommandsController < ApplicationController
|
||||
end
|
||||
|
||||
def respond_with_execution_result(result)
|
||||
# This saves unnecessary back and forth between server and client (e.g: to redirect)-
|
||||
result = result.is_a?(Array) && result.one? ? result.first : result
|
||||
|
||||
case result
|
||||
when Array
|
||||
respond_with_composite_response(result)
|
||||
|
||||
@@ -2,6 +2,9 @@ module Authentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# Checking for tenant must happen first so we redirect before trying to access the db.
|
||||
before_action :require_tenant
|
||||
|
||||
before_action :set_current_account
|
||||
before_action :require_authentication
|
||||
helper_method :authenticated?
|
||||
@@ -19,6 +22,7 @@ module Authentication
|
||||
end
|
||||
|
||||
def require_untenanted_access(**options)
|
||||
skip_before_action :require_tenant, **options
|
||||
skip_before_action :set_current_account, **options
|
||||
skip_before_action :require_authentication, **options
|
||||
before_action :redirect_tenanted_request, **options
|
||||
@@ -30,6 +34,10 @@ module Authentication
|
||||
Current.session.present?
|
||||
end
|
||||
|
||||
def require_tenant
|
||||
ApplicationRecord.current_tenant.present? || request_authentication
|
||||
end
|
||||
|
||||
def require_authentication
|
||||
resume_session || request_authentication
|
||||
end
|
||||
@@ -48,7 +56,7 @@ module Authentication
|
||||
def request_authentication
|
||||
session[:return_to_after_authenticating] = request.url
|
||||
|
||||
redirect_to Launchpad.login_url(account: Current.account), allow_other_host: true
|
||||
redirect_to Launchpad.login_url(product: true, account: Current.account), allow_other_host: true
|
||||
end
|
||||
|
||||
def after_authentication_url
|
||||
|
||||
@@ -6,6 +6,9 @@ class Notifications::ReadingsController < ApplicationController
|
||||
|
||||
def create_all
|
||||
Current.user.notifications.unread.read_all
|
||||
redirect_to notifications_path
|
||||
respond_to do |format|
|
||||
format.html { redirect_to notifications_path }
|
||||
format.turbo_stream { } # No action needed, readings will have been broadcast
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ class Prompts::Collections::UsersController < ApplicationController
|
||||
include CollectionScoped
|
||||
|
||||
def index
|
||||
@users = @collection.users
|
||||
@users = @collection.users.alphabetically
|
||||
|
||||
if stale? etag: @users
|
||||
render layout: false
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
class Prompts::CommandsController < ApplicationController
|
||||
def index
|
||||
@commands = [
|
||||
[ "/add", "Add a new card", "/add " ],
|
||||
[ "/assign", "Assign cards to people", "/assign @" ],
|
||||
[ "/close", "Close cards (with optional reason)", "/close " ],
|
||||
[ "/reopen", "Reopen cards", "/reopen" ],
|
||||
[ "/clear", "Clear all filters", "/clear" ],
|
||||
[ "/consider", "Move cards back to Considering", "/consider" ],
|
||||
[ "/do", "Move cards to Doing", "/do" ],
|
||||
[ "/reconsider", "Move cards back to Considering", "/reconsider" ],
|
||||
[ "/search", "Search cards and comments", "/search " ],
|
||||
[ "/tag", "Tag selected cards", "/tag #" ],
|
||||
[ "/stage", "Move cards to a Workflow Stage", "/stage " ],
|
||||
[ "/help", "Show help menu", "/help" ]
|
||||
]
|
||||
|
||||
if stale? etag: @commands
|
||||
render layout: false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Prompts::TagsController < ApplicationController
|
||||
def index
|
||||
@tags = Tag.all.alphabetically
|
||||
|
||||
if stale? etag: @tags
|
||||
render layout: false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Prompts::UsersController < ApplicationController
|
||||
def index
|
||||
@users = User.all.alphabetically
|
||||
|
||||
if stale? etag: @users
|
||||
render layout: false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,6 @@ class SearchesController < ApplicationController
|
||||
|
||||
def show
|
||||
@search_results = Current.user.search(@query_terms).limit(50)
|
||||
@recent_search_queries = Current.user.search_queries.order(created_at: :desc).limit(10)
|
||||
@recent_search_queries = Current.user.search_queries.order(created_at: :desc).limit(50).uniq(&:terms)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,7 +37,7 @@ module EventsHelper
|
||||
|
||||
def event_cluster_tag(hour, col, &)
|
||||
row = 25 - hour
|
||||
tag.ul class: "events__time-block", style: "grid-area: #{row}/#{col}", &
|
||||
tag.div class: "events__time-block", style: "grid-area: #{row}/#{col}", &
|
||||
end
|
||||
|
||||
def event_next_page_link(next_day)
|
||||
|
||||
@@ -40,7 +40,7 @@ module FiltersHelper
|
||||
end
|
||||
|
||||
def any_filters?(filter)
|
||||
filter.tags.any? || filter.assignees.any? || filter.creators.any? ||
|
||||
filter.tags.any? || filter.assignees.any? || filter.creators.any? || filter.closers.any? ||
|
||||
filter.stages.any? || filter.terms.any? || filter.card_ids&.any? ||
|
||||
filter.assignment_status.unassigned? || !filter.indexed_by.latest?
|
||||
end
|
||||
|
||||
@@ -3,6 +3,18 @@ module RichTextHelper
|
||||
content_tag "lexical-prompt", "", trigger: "@", src: prompts_collection_users_path(collection), name: "mention"
|
||||
end
|
||||
|
||||
def global_mentions_prompt
|
||||
content_tag "lexical-prompt", "", trigger: "@", src: prompts_users_path, name: "mention"
|
||||
end
|
||||
|
||||
def tags_prompt
|
||||
content_tag "lexical-prompt", "", trigger: "#", src: prompts_tags_path, name: "tag"
|
||||
end
|
||||
|
||||
def commands_prompt
|
||||
content_tag "lexical-prompt", "", trigger: "/", src: prompts_commands_path, name: "command", "insert-editable-text": true
|
||||
end
|
||||
|
||||
def cards_prompt
|
||||
content_tag "lexical-prompt", "", trigger: "#", src: prompts_cards_path, name: "card", "insert-editable-text": true, "remote-filtering": true
|
||||
end
|
||||
|
||||
@@ -2,6 +2,6 @@ module TenantingHelper
|
||||
def tenanted_action_cable_meta_tag
|
||||
tag "meta",
|
||||
name: "action-cable-url",
|
||||
content: [ request.script_name, ActionCable.server.config.mount_path ].join("/")
|
||||
content: "#{request.script_name}#{ActionCable.server.config.mount_path}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,10 @@ export default class extends Controller {
|
||||
modal: { type: Boolean, default: false }
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.dialogTarget.setAttribute('aria-hidden', 'true')
|
||||
}
|
||||
|
||||
open() {
|
||||
const modal = this.modalValue
|
||||
|
||||
@@ -14,6 +18,7 @@ export default class extends Controller {
|
||||
} else {
|
||||
this.dialogTarget.show()
|
||||
}
|
||||
this.dialogTarget.setAttribute('aria-hidden', 'false')
|
||||
}
|
||||
|
||||
toggle() {
|
||||
@@ -26,6 +31,7 @@ export default class extends Controller {
|
||||
|
||||
close() {
|
||||
this.dialogTarget.close()
|
||||
this.dialogTarget.setAttribute('aria-hidden', 'true')
|
||||
this.dialogTarget.blur()
|
||||
}
|
||||
|
||||
|
||||
@@ -86,10 +86,10 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
#handleArrowKey(event, fn) {
|
||||
#handleArrowKey(event, fn, preventDefault = true) {
|
||||
if (event.shiftKey || event.metaKey || event.ctrlKey) { return }
|
||||
fn.call()
|
||||
event.preventDefault()
|
||||
if (preventDefault) { event.preventDefault() }
|
||||
}
|
||||
|
||||
#clickCurrentItem(event) {
|
||||
@@ -119,10 +119,10 @@ export default class extends Controller {
|
||||
this.#handleArrowKey(event, this.#selectPrevious.bind(this))
|
||||
},
|
||||
ArrowRight(event) {
|
||||
this.#handleArrowKey(event, this.#selectNext.bind(this))
|
||||
this.#handleArrowKey(event, this.#selectNext.bind(this), false)
|
||||
},
|
||||
ArrowLeft(event) {
|
||||
this.#handleArrowKey(event, this.#selectPrevious.bind(this))
|
||||
this.#handleArrowKey(event, this.#selectPrevious.bind(this), false)
|
||||
},
|
||||
Enter(event) {
|
||||
if (event.shiftKey) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Controller } from "@hotwired/stimulus"
|
||||
import { HttpStatus } from "helpers/http_helpers"
|
||||
import { isMultiLineString } from "helpers/text_helpers";
|
||||
import { marked } from "marked"
|
||||
import { nextFrame } from "helpers/timing_helpers";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "input", "form", "output", "confirmation", "recentCommands" ]
|
||||
@@ -14,9 +15,11 @@ export default class extends Controller {
|
||||
|
||||
// Actions
|
||||
|
||||
focus() {
|
||||
this.inputTarget.setSelectionRange(this.inputTarget.value.length, this.inputTarget.value.length)
|
||||
async focus() {
|
||||
await nextFrame()
|
||||
|
||||
this.inputTarget.focus()
|
||||
this.inputTarget.selection.placeCursorAtTheEnd()
|
||||
}
|
||||
|
||||
executeCommand(event) {
|
||||
@@ -38,6 +41,12 @@ export default class extends Controller {
|
||||
this.#hideOutput()
|
||||
}
|
||||
|
||||
submitCommand({ target }) {
|
||||
if (!this.#showHelpCommandEntered) {
|
||||
this.#submitCommand()
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyPress(event) {
|
||||
if (this.waitingForConfirmationValue) {
|
||||
this.#handleConfirmationKey(event.key.toLowerCase())
|
||||
@@ -84,7 +93,7 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
get #showHelpCommandEntered() {
|
||||
return [ "/help", "/?" ].includes(this.inputTarget.value)
|
||||
return [ "/help", "/?" ].find(command => this.inputTarget.value.includes(command))
|
||||
}
|
||||
|
||||
get #isHelpMenuOpened() {
|
||||
@@ -175,6 +184,10 @@ export default class extends Controller {
|
||||
this.inputTarget.value = this.originalInputValue
|
||||
this.confirmationTarget.value = "confirmed"
|
||||
this.#hideOutput()
|
||||
this.#submitCommand()
|
||||
}
|
||||
|
||||
#submitCommand() {
|
||||
this.formTarget.requestSubmit()
|
||||
this.#reset()
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
class Card < ApplicationRecord
|
||||
include Assignable, Colored, Engageable, Entropic, Eventable,
|
||||
Golden, Mentions, Pinnable, Closeable, Readable, Searchable,
|
||||
Golden, Mentions, Multistep, Pinnable, Closeable, Readable, Searchable,
|
||||
Staged, Stallable, Statuses, Taggable, Watchable
|
||||
|
||||
belongs_to :collection, touch: true
|
||||
|
||||
@@ -9,6 +9,7 @@ module Card::Closeable
|
||||
|
||||
scope :recently_closed_first, -> { closed.order("closures.created_at": :desc) }
|
||||
scope :closed_at_window, ->(window) { closed.where("closures.created_at": window) }
|
||||
scope :closed_by, ->(users) { closed.where("closures.user_id": Array(users)) }
|
||||
end
|
||||
|
||||
def closed?
|
||||
|
||||
@@ -44,6 +44,7 @@ module Card::Engageable
|
||||
transaction do
|
||||
reopen
|
||||
engagement&.destroy
|
||||
activity_spike&.destroy
|
||||
touch_last_active_at
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module Card::Multistep
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :steps, dependent: :destroy
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,8 @@ module Card::Statuses
|
||||
included do
|
||||
enum :status, %w[ creating drafted published ].index_by(&:itself)
|
||||
|
||||
after_create -> { track_event :published }, if: :published?
|
||||
|
||||
scope :published_or_drafted_by, ->(user) { where(status: :published).or(where(status: :drafted, creator: user)) }
|
||||
end
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class Command::Ai::Parser
|
||||
|
||||
def parse(query)
|
||||
normalized_query = resolve_named_params_to_ids command_translator.translate(query)
|
||||
build_composite_command_for normalized_query, query
|
||||
build_command_for normalized_query, query
|
||||
end
|
||||
|
||||
private
|
||||
@@ -20,7 +20,7 @@ class Command::Ai::Parser
|
||||
Command::Ai::Translator.new(context)
|
||||
end
|
||||
|
||||
def build_composite_command_for(normalized_query, query)
|
||||
def build_command_for(normalized_query, query)
|
||||
query_context = context_from_query(normalized_query)
|
||||
resolved_context = query_context || context
|
||||
|
||||
@@ -43,10 +43,11 @@ class Command::Ai::Parser
|
||||
def resolve_named_params_to_ids(normalized_query)
|
||||
normalized_query.tap do |query_json|
|
||||
if query_context = query_json[:context].presence
|
||||
query_context[:assignee_ids] = query_context[:assignee_ids]&.filter_map { |name| assignee_from(name)&.id }
|
||||
query_context[:creator_ids] = query_context[:creator_ids]&.filter_map { |name| assignee_from(name)&.id }
|
||||
query_context[:collection_ids] = query_context[:collection_ids]&.filter_map { |name| Collection.where("lower(name) like ?", "%#{name.downcase}%").first&.id }
|
||||
query_context[:tag_ids] = query_context[:tag_ids]&.filter_map { |name| ::Tag.find_by_title(name)&.id }
|
||||
query_context[:assignee_ids] = query_context[:assignee_ids]&.filter_map { |name| context.find_user(name)&.id }
|
||||
query_context[:creator_ids] = query_context[:creator_ids]&.filter_map { |name| context.find_user(name)&.id }
|
||||
query_context[:closer_ids] = query_context[:closer_ids]&.filter_map { |name| context.find_user(name)&.id }
|
||||
query_context[:collection_ids] = query_context[:collection_ids]&.filter_map { |name| context.find_collection(name)&.id }
|
||||
query_context[:tag_ids] = query_context[:tag_ids]&.filter_map { |name| context.find_tag(name)&.id }
|
||||
query_context.compact!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,9 +16,15 @@ class Command::Ai::Translator
|
||||
end
|
||||
|
||||
private
|
||||
# We don't inject +user.to_gid+ directly in the prompts because of testing and VCR. The URL changes
|
||||
# depending on the tenant, which is not deterministic during tests with parallel tests.
|
||||
ME_REFERENCE = "<fizzy:ME>"
|
||||
|
||||
def translate_query_with_llm(query)
|
||||
response = Rails.cache.fetch(cache_key_for(query)) { chat.ask query }
|
||||
response.content
|
||||
response
|
||||
.content
|
||||
.gsub(ME_REFERENCE, user.to_gid.to_s)
|
||||
end
|
||||
|
||||
def cache_key_for(query)
|
||||
@@ -45,6 +51,7 @@ class Command::Ai::Translator
|
||||
"assignment_status": "unassigned",
|
||||
"card_ids": number[],
|
||||
"creator_ids": string[],
|
||||
"closer_ids": string[],
|
||||
"collection_ids": string[],
|
||||
"tag_ids": string[],
|
||||
"creation": "today" | "yesterday" | "thisweek" | "thismonth" | "thisyear"
|
||||
@@ -77,7 +84,7 @@ class Command::Ai::Translator
|
||||
Cards have comments and live inside collections.
|
||||
|
||||
Context filters describe card state already true.
|
||||
Commands (/assign, /tag, /close, /search, /clear, /do, /consider, /stage, /visit, /add_card, /user) apply new actions.
|
||||
Commands (/assign, /tag, /close, /reopen, /search, /clear, /do, /consider, /stage, /visit, /add, /user) apply new actions.
|
||||
|
||||
Context properties you may use
|
||||
* terms — array of keywords
|
||||
@@ -86,6 +93,7 @@ class Command::Ai::Translator
|
||||
* assignment_status — "unassigned". Important: ONLY when the user asks for unassigned cards.
|
||||
* card_ids — array of card IDs
|
||||
* creator_ids — array of creator’s names
|
||||
* closer_ids — array of closer’s names: people who closed or completed the card
|
||||
* collection_ids — array of collections
|
||||
* tag_ids — array of tag names
|
||||
* creation — relative range when the card was **created** (values listed above). Use it only
|
||||
@@ -100,7 +108,7 @@ class Command::Ai::Translator
|
||||
|
||||
* Use terms only if the query explicitly refers to cards; plain-text searches go to /search.
|
||||
* Numbers without the word "card(s)" default to terms **unless the number is the direct object of an
|
||||
action verb that operates on cards (move, assign, tag, close, stage, consider, do, etc.).**
|
||||
action verb that operates on cards (move, assign, tag, close, reopen, stage, consider, do, etc.).**
|
||||
– "123" (with no action verb) → context: { terms: ["123"] }
|
||||
– "card 123" → context: { card_ids: [123] }
|
||||
– "card 1,2" → context: { card_ids: [1, 2] }
|
||||
@@ -125,14 +133,24 @@ class Command::Ai::Translator
|
||||
* "Stagnated or stalled cards" → indexed_by: "stalled"
|
||||
* "Closing soon" cards → indexed_by: "closing_soon"
|
||||
* "Falling back soon" cards → indexed_by: "falling_back_soon"
|
||||
* Completed by X → closer_ids: ["X"]
|
||||
* Cards I completed → closer_ids: ["#{ME_REFERENCE}"]
|
||||
* **Past-tense** “tagged with #X”, “#X cards” → tag_ids: ["X"] (filter)
|
||||
* **Imperative** “tag …”, “tag with #X”, “add the #X tag”, “apply #X” → command /tag #X (never a filter)
|
||||
* For command that acts on cards, you can reference those by their ID (number). Use the filter "card_ids" when the user passes numbers as command arguments.
|
||||
- User can reference with numbers to a single card or to a group of cards. E.g:
|
||||
- "close 123 and 456" → context: { card_ids: [123, 456] }, commands: [ "/close" ]
|
||||
- "assign 789 to jz" → context: { card_ids: [789] }, commands: [ "/assign jz" ]
|
||||
- "assign 3 and 4 to myself" → context: { card_ids: [3, 4] }, commands: [ "/assign #{ME_REFERENCE}" ]
|
||||
- "reopen 789" → context: { card_ids: [789] }, commands: [ "/reopen" ]
|
||||
- "close 321" → context: { card_ids: [321] }, commands: [ "/close" ]
|
||||
- "assign 5, 82 and 9 to jz" → context: { card_ids: [5, 82 and 9] }, commands: [ "/assign jz" ]
|
||||
* "Unassigned cards" (or “not assigned”, “with no assignee”) → assignment_status: "unassigned".
|
||||
– IMPORTANT: Only set assignment_status when the user **explicitly** asks for an unassigned state
|
||||
– Do NOT infer unassigned just because an assignment follows
|
||||
– Do NOT infer unassigned just because an assignment follows.
|
||||
* **Possessive “my” in front of “card” or “cards”***
|
||||
→ assignee_ids: [ #{user.name} ] — applies **even when other filters are present***
|
||||
(e.g., “my cards closing soon”, “my stalled cards”, “my cards created yesterday”).
|
||||
→ assignee_ids: [ "#{ME_REFERENCE}" ] — applies **even when other filters are present***
|
||||
(e.g., “my cards closing soon”, “my stalled cards”, “my cards created yesterday”, "cards assigned to me").
|
||||
* “Recent cards” (i.e., newly created) → indexed_by: "newest"
|
||||
* “Cards with recent activity”, “recently updated cards” → indexed_by: "latest"
|
||||
– Only use "latest" if the user mentions activity, updates, or changes
|
||||
@@ -146,25 +164,26 @@ class Command::Ai::Translator
|
||||
* ❗ Once you produce a valid context **or** command list, do not add a fallback /search.
|
||||
|
||||
-------------------- COMMAND INTERPRETATION RULES --------------------
|
||||
|
||||
* /user <Name> → open that person’s profile or activity feed.
|
||||
– Phrases like “visit user <Name>”, “view user <Name>”, “see <Name>’s profile” must map to **/user**, **never** to /visit.
|
||||
* /visit <url|path> → open any other URL or internal path (cards, settings, etc.).
|
||||
* /do → engage with card and move it to "doing"
|
||||
* /consider → move card back to "considering" (reconsider)
|
||||
* /reopen → reopen closed cards (moves them back to "open")
|
||||
* Unless a clear command applies, fallback to /search with the verbatim text.
|
||||
* When searching for nouns (non-person), prefer /search over terms.
|
||||
* Respect the spoken order of commands.
|
||||
|
||||
* "close as [reason]" or "close because [reason]" → /close [reason]
|
||||
– Remove "as" or "because" from the actual command
|
||||
* Lone "close" → /close (acts on current context)
|
||||
* Lone "reopen" → /reopen (acts on current context)
|
||||
* /close must **only** be produced if the request explicitly contains the verb “close”.
|
||||
* /reopen must **only** be produced if the request explicitly contains the verb “reopen”.
|
||||
* /stage [workflow stage]→ assign the card to the given stage (never takes card IDs).
|
||||
* “Move <ID(s)> to <Stage>” → context.card_ids = [IDs]; command /stage <Stage>
|
||||
* “Move <ID(s)> to doing” → context.card_ids = [IDs]; command /do
|
||||
* “Move <ID(s)> to considering” → context.card_ids = [IDs]; command /consider
|
||||
* /add_card → Create a new card with a blank title
|
||||
* /add_card [title] → Create a new card with the provided title
|
||||
* /add → Create a new card with a blank title
|
||||
* /add [title] → Create a new card with the provided title
|
||||
|
||||
---------------------------- VISIT SCREENS ---------------------------
|
||||
|
||||
@@ -178,18 +197,22 @@ class Command::Ai::Translator
|
||||
|
||||
------------------------- VISIT USER PROFILES ------------------------
|
||||
|
||||
Use **/user <Name>** (not /visit) whenever the request is about viewing a person’s profile or activity:
|
||||
Use **/user <Name>** (not /visit) whenever the request is about viewing a person’s profile, activity
|
||||
or what that person is up to:
|
||||
|
||||
• visit user mike → /user mike*
|
||||
• view user kevin → /user kevin*
|
||||
• see mike’s profile → /user mike
|
||||
• check what david has been up to → /user david
|
||||
|
||||
---------------------------- CRUCIAL DON’TS ---------------------------
|
||||
|
||||
* When the query contains active verbs such as "assign", "close", or "reopen", always use the corresponding command, NEVER a filter.
|
||||
* Don’t output “/visit /users/<name>”. Profile requests must use **/user <name>**.
|
||||
* Never use names, tags, or stage names mentioned **inside commands** (like /assign, /tag, /stage) as filters.
|
||||
* Never duplicate the assignee in both commands and context.
|
||||
* Never add properties tied to UI view ("card", "list", etc.).
|
||||
* When using /user DON'T add a filter "assignee_ids" with the same user.
|
||||
* To filter completed or closed cards, use "indexed_by: closed"; don't set a "closure" filter unless the user is asking for cards completed in a specific window of time.
|
||||
* When you see a word with a # prefix, assume it refers to a tag (either a filter or a command argument, but don't search for it).
|
||||
* All filters, including terms, must live **inside** context.
|
||||
@@ -223,6 +246,19 @@ class Command::Ai::Translator
|
||||
"context": { "assignee_ids": ["jz"] }
|
||||
}
|
||||
|
||||
User: assign to me
|
||||
Output:
|
||||
{
|
||||
"commands": ["/assign #{ME_REFERENCE}"]
|
||||
}
|
||||
|
||||
User: reopen cards closed this week
|
||||
Output:
|
||||
{
|
||||
"context": { "closure": "thisweek", "indexed_by": "closed" },
|
||||
"commands": ["/reopen"]
|
||||
}
|
||||
|
||||
User: tag with #design*
|
||||
Output:
|
||||
{
|
||||
@@ -299,7 +335,7 @@ class Command::Ai::Translator
|
||||
|
||||
def custom_context
|
||||
<<~PROMPT
|
||||
The name of the user making requests is #{user.first_name.downcase}.
|
||||
The user making requests is "#{ME_REFERENCE}".
|
||||
|
||||
## Current view:
|
||||
|
||||
|
||||
@@ -37,11 +37,11 @@ class Command::Assign < Command
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def assignees
|
||||
User.where(id: assignee_ids)
|
||||
end
|
||||
def assignees
|
||||
User.where(id: assignee_ids)
|
||||
end
|
||||
|
||||
private
|
||||
def assign(assignee, card, toggled_assignees_by_card)
|
||||
unless card.assigned_to?(assignee)
|
||||
toggled_assignees_by_card[card.id] << assignee.id
|
||||
|
||||
@@ -10,42 +10,66 @@ class Command::Parser
|
||||
def parse(string)
|
||||
parse_command(string).tap do |command|
|
||||
command.user = user
|
||||
command.line ||= string
|
||||
command.line ||= as_plain_text(string)
|
||||
command.context ||= context
|
||||
command.default_url_options[:script_name] = script_name
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def as_plain_text_with_attachable_references(string)
|
||||
ActionText::Content.new(string).render_attachments(&:to_gid).fragment.to_plain_text
|
||||
end
|
||||
|
||||
def as_plain_text(string)
|
||||
ActionText::Content.new(string).to_plain_text
|
||||
end
|
||||
|
||||
def parse_command(string)
|
||||
command_name, *command_arguments = string.strip.split(" ")
|
||||
combined_arguments = command_arguments.join(" ")
|
||||
rich_text_command = as_plain_text_with_attachable_references(string)
|
||||
plain_text_command = as_plain_text(string)
|
||||
|
||||
parse_plain_text_command(plain_text_command) || parse_rich_text_command(rich_text_command)
|
||||
end
|
||||
|
||||
def parse_plain_text_command(string)
|
||||
command_name, *_ = string.strip.split(" ")
|
||||
|
||||
case command_name
|
||||
when /^#/
|
||||
Command::FilterByTag.new(tag_title: tag_title_from(string), params: filter.as_params)
|
||||
when /^@/
|
||||
Command::GoToUser.new(user_id: assignee_from(command_name)&.id)
|
||||
Command::GoToUser.new(user_id: context.find_user(command_name)&.id)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_rich_text_command(string)
|
||||
command_name, *command_arguments = string.strip.split(" ")
|
||||
combined_arguments = command_arguments.join(" ")
|
||||
|
||||
case command_name
|
||||
when "/user"
|
||||
Command::GoToUser.new(user_id: assignee_from(combined_arguments)&.id)
|
||||
Command::GoToUser.new(user_id: context.find_user(combined_arguments)&.id)
|
||||
when "/assign", "/assignto"
|
||||
Command::Assign.new(assignee_ids: assignees_from(command_arguments).collect(&:id), card_ids: cards.ids)
|
||||
when "/clear"
|
||||
Command::ClearFilters.new(params: filter.as_params)
|
||||
when "/close"
|
||||
Command::Close.new(card_ids: cards.ids, reason: combined_arguments)
|
||||
when "/reopen"
|
||||
Command::Reopen.new(card_ids: cards.ids)
|
||||
when "/consider", "/reconsider"
|
||||
Command::Consider.new(card_ids: cards.ids)
|
||||
when "/do"
|
||||
Command::Do.new(card_ids: cards.ids)
|
||||
when "/insight"
|
||||
Command::GetInsight.new(query: combined_arguments, card_ids: cards.ids)
|
||||
when "/add_card"
|
||||
when "/add"
|
||||
Command::AddCard.new(card_title: combined_arguments, collection_id: guess_collection&.id)
|
||||
when "/search"
|
||||
Command::Search.new(terms: combined_arguments)
|
||||
when "/stage"
|
||||
Command::Stage.new(stage_id: stage_from(combined_arguments)&.id, card_ids: cards.ids)
|
||||
Command::Stage.new(stage_id: context.find_workflow_stage(combined_arguments)&.id, card_ids: cards.ids)
|
||||
when "/visit"
|
||||
Command::VisitUrl.new(url: command_arguments.first)
|
||||
when "/tag"
|
||||
@@ -55,23 +79,9 @@ class Command::Parser
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def assignees_from(strings)
|
||||
Array(strings).filter_map do |string|
|
||||
assignee_from(string)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: This is temporary as it can be ambiguous. We should inject the user ID in the command
|
||||
# under the hood instead, as determined by the user picker. E.g: @1234.
|
||||
def assignee_from(string)
|
||||
string_without_at = string.delete_prefix("@")
|
||||
User.all.find { |user| user.mentionable_handles.include?(string_without_at.downcase) }
|
||||
end
|
||||
|
||||
def stage_from(combined_arguments)
|
||||
candidate_stages.find do |stage|
|
||||
stage.name.downcase.include?(combined_arguments.downcase)
|
||||
context.find_user(string)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -79,12 +89,8 @@ class Command::Parser
|
||||
cards.first&.collection || Collection.first
|
||||
end
|
||||
|
||||
def candidate_stages
|
||||
Workflow::Stage.where(workflow_id: cards.joins(:collection).select("collections.workflow_id").distinct)
|
||||
end
|
||||
|
||||
def tag_title_from(string)
|
||||
string.gsub(/^#/, "")
|
||||
context.find_tag(string)&.title || string.gsub(/^#/, "")
|
||||
end
|
||||
|
||||
def parse_free_string(string)
|
||||
|
||||
@@ -29,6 +29,35 @@ class Command::Parser::Context
|
||||
viewing_cards_index? || viewing_search_results?
|
||||
end
|
||||
|
||||
def find_user(string)
|
||||
string = string.delete_prefix("@")
|
||||
|
||||
if string.starts_with?("gid://")
|
||||
User.find_by_id(GlobalID::Locator.locate(string).id)
|
||||
else
|
||||
User.all.find { |user| user.mentionable_handles.include?(string.downcase) }
|
||||
end
|
||||
end
|
||||
|
||||
def find_workflow_stage(string)
|
||||
candidate_stages.find do |stage|
|
||||
stage.name.downcase.include?(string.downcase)
|
||||
end
|
||||
end
|
||||
|
||||
def find_tag(string)
|
||||
string = string.delete_prefix("#")
|
||||
if string.starts_with?("gid://")
|
||||
Tag.find_by_id(GlobalID::Locator.locate(string).id)
|
||||
else
|
||||
Tag.find_by_title(string)
|
||||
end
|
||||
end
|
||||
|
||||
def find_collection(string)
|
||||
Collection.where("lower(name) like ?", "%#{string.downcase}%").first
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :controller, :action, :params
|
||||
|
||||
@@ -59,4 +88,8 @@ class Command::Parser::Context
|
||||
@action = route[:action]
|
||||
@params = ActionController::Parameters.new(Rack::Utils.parse_nested_query(uri.query).merge(route.except(:controller, :action)))
|
||||
end
|
||||
|
||||
def candidate_stages
|
||||
Workflow::Stage.where(workflow_id: cards.joins(:collection).select("collections.workflow_id").distinct)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
class Command::Reopen < Command
|
||||
include Command::Cards
|
||||
|
||||
store_accessor :data, :reopened_card_ids
|
||||
|
||||
def title
|
||||
"Reopen #{cards_description}"
|
||||
end
|
||||
|
||||
def execute
|
||||
reopened_card_ids = []
|
||||
|
||||
transaction do
|
||||
cards.find_each do |card|
|
||||
reopened_card_ids << card.id
|
||||
card.reopen(user: user)
|
||||
end
|
||||
|
||||
update! reopened_card_ids: reopened_card_ids
|
||||
end
|
||||
end
|
||||
|
||||
def undo
|
||||
transaction do
|
||||
reopened_cards.find_each do |card|
|
||||
card.close(user: user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def reopened_cards
|
||||
user.accessible_cards.where(id: reopened_card_ids)
|
||||
end
|
||||
end
|
||||
@@ -20,7 +20,7 @@ class Filter < ApplicationRecord
|
||||
@cards ||= begin
|
||||
result = creator.accessible_cards.indexed_by(indexed_by)
|
||||
result = result.where(id: card_ids) if card_ids.present?
|
||||
result = result.open unless indexed_by.closed? || closure_window || card_ids.present?
|
||||
result = result.open unless include_closed_cards?
|
||||
result = result.by_engagement_status(engagement_status) if engagement_status.present?
|
||||
result = result.unassigned if assignment_status.unassigned?
|
||||
result = result.assigned_to(assignees.ids) if assignees.present?
|
||||
@@ -30,6 +30,7 @@ class Filter < ApplicationRecord
|
||||
result = result.tagged_with(tags.ids) if tags.present?
|
||||
result = result.where("cards.created_at": creation_window) if creation_window
|
||||
result = result.closed_at_window(closure_window) if closure_window
|
||||
result = result.closed_by(closers) if closers.present?
|
||||
result = terms.reduce(result) do |result, term|
|
||||
result.mentioning(term)
|
||||
end
|
||||
@@ -57,4 +58,9 @@ class Filter < ApplicationRecord
|
||||
def cache_key
|
||||
ActiveSupport::Cache.expand_cache_key collections.cache_key_with_version, super
|
||||
end
|
||||
|
||||
private
|
||||
def include_closed_cards?
|
||||
indexed_by.closed? || closure_window || closers.present? || card_ids.present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,6 +10,7 @@ module Filter::Params
|
||||
card_ids: [],
|
||||
assignee_ids: [],
|
||||
creator_ids: [],
|
||||
closer_ids: [],
|
||||
collection_ids: [],
|
||||
stage_ids: [],
|
||||
tag_ids: [],
|
||||
@@ -50,6 +51,7 @@ module Filter::Params
|
||||
params[:card_ids] = card_ids
|
||||
params[:assignee_ids] = assignees.ids
|
||||
params[:creator_ids] = creators.ids
|
||||
params[:closer_ids] = closers.ids
|
||||
end.compact_blank.reject(&method(:default_value?))
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ module Filter::Resources
|
||||
has_and_belongs_to_many :stages, class_name: "Workflow::Stage", join_table: "filters_stages"
|
||||
has_and_belongs_to_many :assignees, class_name: "User", join_table: "assignees_filters", association_foreign_key: "assignee_id"
|
||||
has_and_belongs_to_many :creators, class_name: "User", join_table: "creators_filters", association_foreign_key: "creator_id"
|
||||
has_and_belongs_to_many :closers, class_name: "User", join_table: "closers_filters", association_foreign_key: "closer_id"
|
||||
end
|
||||
|
||||
def resource_removed(resource)
|
||||
|
||||
@@ -13,7 +13,7 @@ class Notification < ApplicationRecord
|
||||
delegate :card, to: :source
|
||||
|
||||
def self.read_all
|
||||
update!(read_at: Time.current)
|
||||
all.each { |notification| notification.read }
|
||||
end
|
||||
|
||||
def read
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class Step < ApplicationRecord
|
||||
belongs_to :card, touch: true
|
||||
|
||||
scope :completed, -> { where(completed: true) }
|
||||
|
||||
def completed?
|
||||
completed
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
class Tag < ApplicationRecord
|
||||
include Filterable
|
||||
include Attachable, Filterable
|
||||
|
||||
has_many :taggings, dependent: :destroy
|
||||
has_many :cards, through: :taggings
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
module Tag::Attachable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include ActionText::Attachable
|
||||
end
|
||||
|
||||
def attachable_plain_text_representation(...)
|
||||
"##{title}"
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
class User < ApplicationRecord
|
||||
include Accessor, ActionText::Attachable, Assignee, Mentionable, Named, Role, Searcher,
|
||||
include Accessor, Attachable, Assignee, Mentionable, Named, Role, Searcher,
|
||||
SignalUser, Transferable
|
||||
include Timelined # Depends on Accessor
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
module User::Attachable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include ActionText::Attachable
|
||||
end
|
||||
|
||||
def attachable_plain_text_representation(...)
|
||||
"@#{first_name.downcase}"
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
<li class="flex align-center gap-half pad-inline" data-filter-target="item">
|
||||
<%= avatar_tag user %>
|
||||
<%= avatar_tag user, hidden_for_screen_reader: true %>
|
||||
|
||||
<strong class="overflow-ellipsis">
|
||||
<%= link_to user.name, user, class: "txt-ink btn btn--plain" %>
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
<%= render "cards/stagings/stages", card: card if card.doing? %>
|
||||
</div>
|
||||
|
||||
<%= render "cards/display/perma/steps", card: card %>
|
||||
|
||||
<footer class="card__footer full-width flex align-start gap">
|
||||
<%= render "cards/display/perma/meta", card: card %>
|
||||
<%= render "cards/display/perma/background", card: card %>
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
|
||||
<div class="comment__content flex flex-column flex-item-grow full-width">
|
||||
<div class="comment__author flex align-center gap-half">
|
||||
<strong>
|
||||
<%= link_to comment.creator.name, comment.creator, class: "txt-ink btn btn--plain fill-transparent", data: { turbo_frame: "_top" } %>
|
||||
</strong>
|
||||
<h3 class="font-weight-normal txt-normal flex-item-justify-start">
|
||||
<strong>
|
||||
<%= link_to comment.creator.name, comment.creator, class: "txt-ink btn btn--plain fill-transparent", data: { turbo_frame: "_top" } %>
|
||||
</strong>
|
||||
|
||||
<%= link_to comment, class: "txt-undecorated txt-ink translucent txt-normal txt-capitalize flex-item-justify-start" do %>
|
||||
<%= local_datetime_tag comment.created_at, style: :agoorweekday %>,
|
||||
<%= local_datetime_tag comment.created_at, style: :time %>
|
||||
<% end %>
|
||||
<%= link_to comment, class: "txt-undecorated txt-ink translucent txt-normal txt-capitalize" do %>
|
||||
<%= local_datetime_tag comment.created_at, style: :agoorweekday %>,
|
||||
<%= local_datetime_tag comment.created_at, style: :time %>
|
||||
<% end %>
|
||||
</h3>
|
||||
|
||||
<%= link_to edit_card_comment_path(comment.card, comment),
|
||||
class: "comment__edit btn btn--circle borderless fill-transparent translucent" do %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="quick-reaction" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<div class="quick-reaction" data-controller="dialog" data-action="keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside">
|
||||
<%= yield %>
|
||||
|
||||
<dialog class="quick-reaction__popup popup panel fill-white shadow" data-dialog-target="dialog">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<details class="expander" data-controller="details" data-details-target="details" data-action="keydown.esc->details#close click@document->details#closeOnClickOutside">
|
||||
<details class="expander" data-controller="details" data-details-target="details" data-action="keydown.esc->details#close:stop click@document->details#closeOnClickOutside">
|
||||
<summary class="expander__button btn borderless">
|
||||
Close as
|
||||
<%= icon_tag "caret-down" %>
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
<% end %>
|
||||
<% elsif !card.closed? %>
|
||||
<%= form_with model: card, url: collection_card_path(card.collection, card), data: { controller: "form" } do |form| %>
|
||||
<label class="btn">
|
||||
<label class="btn input--file">
|
||||
<%= icon_tag "picture-add" %>
|
||||
<span class="for-screen-reader">Add background image</span>
|
||||
<%= form.file_field :image, class: "input",
|
||||
accept: "image/png, image/jpeg, image/jpg, image/webp",
|
||||
data: { action: "upload-preview#previewImage form#submit", upload_preview_target: "input" },
|
||||
hidden: true %>
|
||||
data: { action: "upload-preview#previewImage form#submit", upload_preview_target: "input" } %>
|
||||
</label>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<header class="card__header">
|
||||
<%= render "cards/display/preview/collection", card: card %>
|
||||
<%= render "cards/display/preview/tags", card: card %>
|
||||
<%= render "cards/display/preview/steps", card: card %>
|
||||
|
||||
<% if card.staged? && card.doing? %>
|
||||
<span class="btn justify-start workflow-stage workflow-stage--current txt-uppercase min-width max-width">
|
||||
@@ -16,9 +17,9 @@
|
||||
|
||||
<div class="card__body justify-space-between">
|
||||
<div class="card__content">
|
||||
<h1 class="card__title overflow-line-clamp">
|
||||
<h3 class="card__title overflow-line-clamp">
|
||||
<%= card.title %>
|
||||
</h1>
|
||||
</h3>
|
||||
|
||||
<%= link_to collection_card_path(card.collection, card), draggable: false, class: "card__link", title: card_title_tag(card), data: { action: "dialog#close", turbo_frame: "_top" } do %>
|
||||
<span class="for-screen-reader"><%= card.title %></span>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<ol class="steps txt-small margin-block">
|
||||
<%= render partial: "cards/steps/step", collection: card.steps, as: :step %>
|
||||
|
||||
<div class="step">
|
||||
<input type="checkbox" class="step__checkbox" disabled>
|
||||
<%= form_with model: [card, Step.new], url: card_steps_path(card) do |form| %>
|
||||
<%= form.text_field :content, class: "input step__content", placeholder: "Add a step…", required: true, autocomplete: "off" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</ol>
|
||||
@@ -0,0 +1,8 @@
|
||||
<% if card.steps.any? %>
|
||||
<div class="card__steps align-center gap-half flex-item-justify-end flex-item-no-shrink">
|
||||
<span class="steps__icon">
|
||||
<%= icon_tag "check" %>
|
||||
</span>
|
||||
<strong><%= "#{card.steps.completed.count}/#{card.steps.count}" %></strong>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,4 +1,4 @@
|
||||
<% @page_title = @filter.summary %>
|
||||
<% @page_title = filter_title @filter %>
|
||||
|
||||
<%= render "filters/broadcasts", filter: @filter %>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<% end %>
|
||||
|
||||
<header class="margin-block-end">
|
||||
<span class="txt-large overflow-ellipsis margin-none font-black" style="view-transistion-name: card-collection-title">
|
||||
<h1 class="txt-large overflow-ellipsis margin-none font-black" style="view-transistion-name: card-collection-title">
|
||||
<span><%= filter_title @filter %></span>
|
||||
</span>
|
||||
</h1>
|
||||
<%= render "filters/settings", filter: @filter %>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<%= turbo_frame_tag step do %>
|
||||
<li class="step">
|
||||
<%= form_with model: [step.card, step], data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :completed, { class: "step__checkbox", data: { action: "change->form#submit" } } %>
|
||||
<% end %>
|
||||
<%= link_to step.content, edit_card_step_path(step.card, step), class: "step__content" %>
|
||||
</li>
|
||||
<% end %>
|
||||
@@ -0,0 +1,18 @@
|
||||
<%= turbo_frame_tag @step do %>
|
||||
<div class="flex align-center gap-half">
|
||||
<%= form_with model: [@card, @step], class: "step", data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :completed, { class: "step__checkbox", checked: @step.completed?, disabled: true } %>
|
||||
<%= form.text_field :content, class: "input step__content step__content--edit", placeholder: "Name this step…", required: true, autofocus: true, autocomplete: "off",
|
||||
data: { action: "keydown.esc->form#cancel focus->form#select" } %>
|
||||
<%= form.button type: "submit", class: "btn btn--positive txt-xx-small" do %>
|
||||
<%= icon_tag "check" %>
|
||||
<span class="for-screen-reader">Save changes</span>
|
||||
<% end %>
|
||||
<%= link_to "Cancel changes", card_step_path(@card, @step), data: { form_target: "cancel" }, hidden: true %>
|
||||
<% end %>
|
||||
<%= button_to card_step_path(@card, @step), method: :delete, class: "btn btn--negative txt-xx-small" do %>
|
||||
<%= icon_tag "trash" %>
|
||||
<span class="for-screen-reader">Delete this step</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,3 @@
|
||||
<%= turbo_frame_tag @step do %>
|
||||
<%= render "cards/steps/step", step: @step %>
|
||||
<% end %>
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="margin-block-end">
|
||||
<strong class="divider txt-large">Do or Die</strong>
|
||||
<%= render "entropy/auto_close", model: collection %>
|
||||
</div>
|
||||
<%= turbo_frame_tag @collection, :entropy_configuration do %>
|
||||
<div class="margin-block-end">
|
||||
<strong class="divider txt-large">Do or Die</strong>
|
||||
<%= render "entropy/auto_close", model: collection, url: collection_entropy_configuration_path(collection) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,54 +1,55 @@
|
||||
<div class="margin-block">
|
||||
<strong class="divider txt-large">Public link</strong>
|
||||
<p class="margin-none-block-start">Turn on the Public link to share this collection with anyone in the world. They won’t need to log in and they won’t be able to see anything else in Fizzy.</p>
|
||||
<%= turbo_frame_tag @collection, :publication do %>
|
||||
<div class="margin-block">
|
||||
<strong class="divider txt-large">Public link</strong>
|
||||
<p class="margin-none-block-start">Turn on the Public link to share this collection with anyone in the world. They won’t need to log in and they won’t be able to see anything else in Fizzy.</p>
|
||||
|
||||
<% if collection.published? %>
|
||||
<div class="border-radius pad fill-selected">
|
||||
<div class="flex-inline center justify-between gap">
|
||||
<span class="txt-large"><%= icon_tag "lock" %></span>
|
||||
<label class="switch flex align-center justify-between">
|
||||
<%= form_with url: collection_publication_path(collection), method: :delete, data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :published, class: "switch__input", checked: true, data: { action: "change->form#submit" } %>
|
||||
<span class="switch__btn round"></span>
|
||||
<span class="for-screen-reader">Turn off the public link</span>
|
||||
<% end %>
|
||||
</label>
|
||||
<span class="txt-large"><%= icon_tag "world" %></span>
|
||||
</div>
|
||||
<% if collection.published? %>
|
||||
<div class="border-radius pad fill-selected">
|
||||
<div class="flex-inline center justify-between gap">
|
||||
<span class="txt-large"><%= icon_tag "lock" %></span>
|
||||
<label class="switch flex align-center justify-between">
|
||||
<%= form_with url: collection_publication_path(collection), method: :delete, data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :published, class: "switch__input", checked: true, data: { action: "change->form#submit" } %>
|
||||
<span class="switch__btn round"></span>
|
||||
<span class="for-screen-reader">Turn off the public link</span>
|
||||
<% end %>
|
||||
</label>
|
||||
<span class="txt-large"><%= icon_tag "world" %></span>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center gap-half margin-block">
|
||||
<%= text_field_tag :publication_url, published_collection_url(collection), readonly: true, class: "full-width input fill-white" %>
|
||||
<div class="flex align-center justify-center gap-half">
|
||||
<%= button_to_copy_to_clipboard(published_collection_url(collection)) do %>
|
||||
<%= icon_tag "copy-paste" %>
|
||||
<span class="for-screen-reader">Copy public link</span>
|
||||
<div class="flex align-center gap-half margin-block">
|
||||
<%= text_field_tag :publication_url, published_collection_url(collection), readonly: true, class: "full-width input fill-white" %>
|
||||
<div class="flex align-center justify-center gap-half">
|
||||
<%= button_to_copy_to_clipboard(published_collection_url(collection)) do %>
|
||||
<%= icon_tag "copy-paste" %>
|
||||
<span class="for-screen-reader">Copy public link</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<strong class="margin-block-end-half flex justify-center txt-small">Add an optional description to the public page</strong>
|
||||
<div class="border-radius input fill-white">
|
||||
<%= form_with model: collection, class: "txt-align-start", data: { controller: "form" } do |form| %>
|
||||
<%= form.rich_textarea :public_description, class: "rich-text-content txt-small",
|
||||
placeholder: "Add a public note about this collection…",
|
||||
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
|
||||
<%= form.button "Save changes", type: :submit, class: "btn txt-small" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<strong class="margin-block-end-half flex justify-center txt-small">Add an optional description to the public page</strong>
|
||||
<div class="border-radius input fill-white">
|
||||
<%= form_with model: collection, class: "txt-align-start", data: { controller: "form" } do |form| %>
|
||||
<%= form.rich_textarea :public_description, class: "rich-text-content txt-small",
|
||||
placeholder: "Add a public note about this collection…",
|
||||
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
|
||||
<%= form.button "Save changes", type: :submit, class: "btn txt-small" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="border-radius pad fill-shade">
|
||||
<div class="flex-inline center justify-between gap">
|
||||
<span class="txt-large"><%= icon_tag "lock" %></span>
|
||||
<label class="switch flex align-center justify-between">
|
||||
<%= form_with url: collection_publication_path(collection), method: :post, data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :published, class: "switch__input", checked: false, data: { action: "change->form#submit" } %>
|
||||
<span class="switch__btn round"></span>
|
||||
<span class="for-screen-reader">Turn on the public link</span>
|
||||
<% end %>
|
||||
</label>
|
||||
<span class="txt-large"><%= icon_tag "world" %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="border-radius pad fill-shade">
|
||||
<div class="flex-inline center justify-between gap">
|
||||
<span class="txt-large"><%= icon_tag "lock" %></span>
|
||||
<label class="switch flex align-center justify-between">
|
||||
<%= form_with url: collection_publication_path(collection), method: :post, data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :published, class: "switch__input", checked: false, data: { action: "change->form#submit" } %>
|
||||
<span class="switch__btn round"></span>
|
||||
<span class="for-screen-reader">Turn on the public link</span>
|
||||
<% end %>
|
||||
</label>
|
||||
<span class="txt-large"><%= icon_tag "world" %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
<strong class="txt-large">Workflows</strong>
|
||||
<div class="flex justify-center margin-block-end">Use a Workflow to track progress in this Collection.</div>
|
||||
<div class="flex flex-wrap gap justify-center">
|
||||
<% Workflow.all.each do |workflow| %>
|
||||
<div class="workflow-preview txt-align-start border-radius <%= 'workflow-preview--selected' if workflow == collection.workflow %>">
|
||||
<%= button_to collection_workflow_path(collection), method: :patch do %>
|
||||
<%= hidden_field_tag "collection[workflow_id]", workflow.id %>
|
||||
<strong class="txt-small overflow-ellipsis flex-item-no-shrink"><%= workflow.name %></strong>
|
||||
<ul class="list-style-none txt-xx-small margin-none flex flex-column gap-half">
|
||||
<% workflow.stages.each do |stage| %>
|
||||
<li class="overflow-ellipsis flex align-center gap-half min-width">
|
||||
<span class="workflow-preview__swatch btn btn--circle" style="--btn-background: <%= stage.color.present? ? stage.color : Card::Colored::DEFAULT_COLOR %>;"></span>
|
||||
<span class="overflow-ellipsis"><%= stage.name %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="margin-block-end-double"><%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link" %></p>
|
||||
<%= turbo_frame_tag @collection, :workflows do %>
|
||||
<strong class="txt-large">Workflows</strong>
|
||||
<div class="flex justify-center margin-block-end">Use a Workflow to track progress in this Collection.</div>
|
||||
<div class="flex flex-wrap gap justify-center">
|
||||
<% Workflow.all.each do |workflow| %>
|
||||
<div class="workflow-preview txt-align-start border-radius <%= 'workflow-preview--selected' if workflow == collection.workflow %>">
|
||||
<%= button_to collection_workflow_path(collection), method: :patch, aria: { label: workflow == collection.workflow ? "Stop using this workflow" : "Use this workflow" } do %>
|
||||
<%= hidden_field_tag "collection[workflow_id]", workflow == collection.workflow ? "" : workflow.id %>
|
||||
<strong class="txt-small overflow-ellipsis flex-item-no-shrink"><%= workflow.name %></strong>
|
||||
<ul class="list-style-none txt-xx-small margin-none flex flex-column gap-half">
|
||||
<% workflow.stages.each do |stage| %>
|
||||
<li class="overflow-ellipsis flex align-center gap-half min-width">
|
||||
<span class="workflow-preview__swatch btn btn--circle" style="--btn-background: <%= stage.color.present? ? stage.color : Card::Colored::DEFAULT_COLOR %>;"></span>
|
||||
<span class="overflow-ellipsis"><%= stage.name %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="margin-block-end-double"><%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link", data: { turbo_frame: "_top" } %></p>
|
||||
<% end %>
|
||||
|
||||
@@ -4,21 +4,36 @@
|
||||
data: {
|
||||
controller: "form",
|
||||
terminal_target: "form",
|
||||
action: "turbo:submit-start->terminal#commandSubmitted turbo:submit-end->terminal#focus keydown.meta+k@document->terminal#focus:prevent keydown.ctrl+k@document->terminal#focus:prevent keydown.enter->terminal#executeCommand keydown.esc->terminal#hideMenus turbo:submit-end->terminal#handleCommandResponse"
|
||||
action: "
|
||||
keydown.enter->terminal#submitCommand
|
||||
keydown.up->toggle-class#add:prevent
|
||||
keydown.up->navigable-list#selectCurrentOrReset:stop
|
||||
terminal#hideError
|
||||
turbo:submit-start->terminal#commandSubmitted
|
||||
turbo:submit-end->terminal#focus
|
||||
keydown.meta+k@document->terminal#focus:prevent
|
||||
keydown.ctrl+k@document->terminal#focus:prevent
|
||||
keydown.enter->terminal#executeCommand
|
||||
keydown.esc->terminal#hideMenus
|
||||
turbo:submit-end->terminal#handleCommandResponse
|
||||
"
|
||||
} do %>
|
||||
<label class="terminal__label txt-nowrap" for="fizzy_do">Fizzy do ></label>
|
||||
<label class="terminal__label txt-nowrap" for="command">Fizzy do ></label>
|
||||
|
||||
<%= text_field_tag "command", nil,
|
||||
autocomplete: "off",
|
||||
autocorrect: "off",
|
||||
autocapitalize: "off",
|
||||
<%= rich_textarea_tag "command", nil,
|
||||
toolbar: false,
|
||||
class: "terminal__input input hide-focus-ring fill-transparent unpad",
|
||||
"single-line": true,
|
||||
attachments: false,
|
||||
data: {
|
||||
terminal_target: "input",
|
||||
action: "keydown.up->toggle-class#add:prevent keydown.up->navigable-list#selectCurrentOrReset:stop terminal#hideError"
|
||||
terminal_target: "input"
|
||||
},
|
||||
placeholder: platform.desktop? ? "Press #{ hotkey_label(["ctrl", "K"]) } to search or type commands…" : "Search or type commands…",
|
||||
spellcheck: "false" %>
|
||||
placeholder: platform.desktop? ? "Press #{ hotkey_label(["ctrl", "K"]) } to search or type /commands…" : "Search or type /commands…",
|
||||
spellcheck: "false" do %>
|
||||
<%= global_mentions_prompt %>
|
||||
<%= tags_prompt %>
|
||||
<%= commands_prompt %>
|
||||
<% end %>
|
||||
|
||||
<%= hidden_field_tag "confirmed", nil, data: { terminal_target: "confirmation" } %>
|
||||
<%= hidden_field_tag "confirmed", nil, data: { terminal_target: "confirmation" } %>
|
||||
<% end %>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<dd>Clear all filters</dd>
|
||||
|
||||
<h3>Cards</h3>
|
||||
<dt><code>/add_card [title]</code></dt>
|
||||
<dt><code>/add [title]</code></dt>
|
||||
<dd>Create a new card</dd>
|
||||
<dt><code>/assign [person] [person]…</code></dt>
|
||||
<dd>Assign visible card(s)</dd>
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
related_element_group_value: card.id,
|
||||
action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>
|
||||
<div class="card__header flex align-start gap full-width">
|
||||
<div class="card__collection flex-inline align-start">
|
||||
<h4 class="card__collection flex-inline align-start">
|
||||
<span class="card__id"><%= card.id %></span>
|
||||
<span class="card__collection-name">
|
||||
<span class="overflow-ellipsis"><%= event.collection.name %></span>
|
||||
</span>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
<% if event_column(event) == 2 %>
|
||||
<span class="event__icon flex-item-justify-end txt-reversed">
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
</header>
|
||||
<div class="events" id="activity">
|
||||
<%= render "events/day", day_timeline: @day_timeline %>
|
||||
<%= event_next_page_link(@day_timeline.next_day) %>
|
||||
</div>
|
||||
<%= event_next_page_link(@day_timeline.next_day) %>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<% if filter.closers.any? || @expand_all %>
|
||||
<div class="quick-filter position-relative" data-controller="dialog"
|
||||
data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<button class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#open:stop">
|
||||
<span class="overflow-ellipsis">
|
||||
<% if filter.closers.any? %>
|
||||
<%= "Closed by #{filter.closers.map(&:familiar_name).to_sentence(two_words_connector: " or ", last_word_connector: ", or ")}" %>
|
||||
<% else %>
|
||||
Closed by…
|
||||
<% end %>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<%= tag.dialog class: "margin-block-start-half popup panel flex-column align-start gap-half fill-white shadow txt-small", data: {
|
||||
action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate filter:changed->navigable-list#reset toggle->filter#filter",
|
||||
aria: { label: "Closed by…", aria_description: "Closed by…" },
|
||||
controller: "filter navigable-list",
|
||||
dialog_target: "dialog",
|
||||
navigable_list_focus_on_selection_value: false,
|
||||
navigable_list_actionable_items_value: true } do %>
|
||||
<strong class="popup__title txt-nowrap">Closed by…</strong>
|
||||
|
||||
<% if User.active.many? %>
|
||||
<%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
|
||||
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
|
||||
<% end %>
|
||||
|
||||
<ul class="popup__list" data-filter-target="list">
|
||||
<% User.active.order(:name).each do |user| %>
|
||||
<li class="popup__group flex align-center" data-filter-target="item" data-navigable-list-target="item">
|
||||
<%= form_with url: cards_path, method: :get, class: "full-width", data: { controller: "form" } do |form| %>
|
||||
<% filter.as_params.except(:closer_ids).each do |key, value| %>
|
||||
<%= filter_hidden_field_tag key, value %>
|
||||
<% end %>
|
||||
|
||||
<% filter.closers.each do |existing_closer| %>
|
||||
<% unless existing_closer == user %>
|
||||
<%= hidden_field_tag "closer_ids[]", existing_closer.id %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% unless filter.closers.include?(user) %>
|
||||
<%= hidden_field_tag "closer_ids[]", user.id %>
|
||||
<% end %>
|
||||
|
||||
<% if @expand_all %>
|
||||
<%= hidden_field_tag :expand_all, @expand_all %>
|
||||
<% end %>
|
||||
|
||||
<%= form.button type: "submit", class: "btn popup__item min-width full-width unpad-inline" do %>
|
||||
<span class="overflow-ellipsis flex-item-grow"><%= user.name %></span>
|
||||
<% if filter.closers.include?(user) %>
|
||||
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -18,6 +18,7 @@
|
||||
<%= render "filters/tags", filter: filter %>
|
||||
<%= render "filters/assignees", filter: filter %>
|
||||
<%= render "filters/creators", filter: filter %>
|
||||
<%= render "filters/closers", filter: filter %>
|
||||
<%= render "filters/time_window", filter: filter, name: :creation, label: "Created" %>
|
||||
<%= render "filters/time_window", filter: filter, name: :closure, label: "Closed" %>
|
||||
<%# FIXME LATER: this needs to be able to allow selection of unique stages across all active workflows %>
|
||||
@@ -43,7 +44,7 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to cards_path(filter.as_params.except(:assignee_ids, :assignment_status, :card_ids, :creator_ids, :stage_ids, :tag_ids, :terms, :indexed_by, :creation, :closure)), class: "btn btn--remove txt-x-small" do %>
|
||||
<%= link_to cards_path(filter.as_params.except(:assignee_ids, :assignment_status, :card_ids, :creator_ids, :closer_ids, :stage_ids, :tag_ids, :terms, :indexed_by, :creation, :closure)), class: "btn btn--remove txt-x-small" do %>
|
||||
<%= icon_tag "close" %>
|
||||
<span class="for-screen-reader">Clear all</span>
|
||||
<% end %>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<body data-controller="lightbox local-time timezone-cookie" data-action="turbo:morph@window->local-time#refreshAll">
|
||||
<header id="header">
|
||||
<a href="#main-content" class="skip-navigation btn">Skip to main content</a>
|
||||
<a href="#main" class="skip-navigation btn" data-turbo="false">Skip to main content</a>
|
||||
<%= yield :header %>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<body class="public" data-controller="local-time timezone-cookie" data-action="turbo:morph@window->local-time#refreshAll">
|
||||
<header id="header">
|
||||
<a href="#main-content" class="skip-navigation btn">Skip to main content</a>
|
||||
<a href="#main" class="skip-navigation btn" data-turbo="false">Skip to main content</a>
|
||||
<%= yield :header %>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<%= tenanted_action_cable_meta_tag %>
|
||||
<%= yield :head %>
|
||||
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="manifest" href="<%= pwa_manifest_path(format: :json) %>">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="/favicon.png" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand pins stack">
|
||||
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand pins stack" aria-haspopup="true">
|
||||
<%= icon_tag "pinned" %>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<%= avatar_image_tag notification.creator %>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column min-width flex-item-grow">
|
||||
<h3 class="flex flex-column min-width flex-item-grow font-weight-normal">
|
||||
<%= render "notifications/notification/#{notification.source_type.underscore}/body", notification: notification %>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="tray__actions">
|
||||
<%= button_to read_all_notifications_path,
|
||||
class: "tray__notification-read-action btn btn--reversed fill-transparent borderless flex-item-justify-start txt-xx-small",
|
||||
title: "Mark all as read",
|
||||
title: "Mark all notifications as read",
|
||||
data: { action: "click->dialog#close", turbo_frame: "notifications" } do %>
|
||||
<%= icon_tag "check" %>
|
||||
<% end %>
|
||||
@@ -34,7 +34,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand notifications stack">
|
||||
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand notifications stack" aria-haspopup="true">
|
||||
<%= icon_tag "bell" %>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<lexical-prompt-item search="<%= "#{user.name} #{user.initials}" %>" sgid="<%= user.attachable_sgid %>">
|
||||
<lexical-prompt-item search="<%= "#{user.name} #{user.initials} #{"me" if user == Current.user}" %>" sgid="<%= user.attachable_sgid %>">
|
||||
<template type="menu">
|
||||
<%= avatar_image_tag user %>
|
||||
<%= user.name %>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<% command, description, editor_version = command %>
|
||||
|
||||
<lexical-prompt-item search="<%= description %> <%= command %>">
|
||||
<template type="menu">
|
||||
<code><%= command %></code> <%= description %>
|
||||
</template>
|
||||
<template type="editor">
|
||||
<%= editor_version.gsub(/ +$/, " ").html_safe %>
|
||||
</template>
|
||||
</lexical-prompt-item>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render partial: "prompts/commands/command", collection: @commands %>
|
||||
@@ -0,0 +1,8 @@
|
||||
<lexical-prompt-item search="<%= tag.title %>" sgid="<%= tag.attachable_sgid %>">
|
||||
<template type="menu">
|
||||
#<%= tag.title %>
|
||||
</template>
|
||||
<template type="editor">
|
||||
#<%= tag.title %>
|
||||
</template>
|
||||
</lexical-prompt-item>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render partial: "prompts/tags/tag", collection: @tags %>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render partial: "prompts/collections/users/user", collection: @users %>
|
||||
@@ -18,7 +18,7 @@
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"start_url": "<%= root_path %>",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"description": "Card-up the biggest issues in your projects",
|
||||
@@ -27,13 +27,13 @@
|
||||
{
|
||||
"name": "Notifications",
|
||||
"description": "Catch up on recent notifications",
|
||||
"url": "/notifications",
|
||||
"url": "<%= notifications_path %>",
|
||||
"icons": [{ "src": "<%= image_url("bell.svg") %>", "sizes": "any" }]
|
||||
},
|
||||
{
|
||||
"name": "Latest Activity",
|
||||
"description": "See what’s new",
|
||||
"url": "/",
|
||||
"url": "<%= root_path %>",
|
||||
"icons": [{ "src": "<%= image_url("activity.svg") %>", "sizes": "any" }]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -18,6 +18,7 @@ Rails.application.routes.draw do
|
||||
resource :workflow, only: :update
|
||||
resource :involvement
|
||||
resource :publication
|
||||
resource :entropy_configuration
|
||||
end
|
||||
|
||||
resources :cards
|
||||
@@ -44,6 +45,7 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :assignments
|
||||
resources :taggings
|
||||
resources :steps
|
||||
|
||||
resources :comments do
|
||||
resources :reactions, module: :comments
|
||||
@@ -116,6 +118,10 @@ Rails.application.routes.draw do
|
||||
|
||||
namespace :prompts do
|
||||
resources :cards
|
||||
resources :users
|
||||
resources :tags
|
||||
resources :commands
|
||||
|
||||
resources :collections do
|
||||
scope module: :collections do
|
||||
resources :users
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
class AddStepsToCards < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :steps do |t|
|
||||
t.references :card, null: false, foreign_key: true
|
||||
t.text :content, null: false
|
||||
t.boolean :completed, default: false, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :steps, :completed
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
class CreateClosersFiltersJoinTable < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :closers_filters, id: false do |t|
|
||||
t.integer :filter_id, null: false
|
||||
t.integer :closer_id, null: false
|
||||
end
|
||||
|
||||
add_index :closers_filters, :filter_id
|
||||
add_index :closers_filters, :closer_id
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class ChangeStepsIndex < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
remove_index :steps, :completed
|
||||
add_index :steps, %i[ card_id completed ]
|
||||
end
|
||||
end
|
||||
Generated
+19
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_07_02_160230) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_07_02_211937) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.datetime "accessed_at"
|
||||
t.integer "collection_id", null: false
|
||||
@@ -133,6 +133,13 @@ ActiveRecord::Schema[8.1].define(version: 2025_07_02_160230) do
|
||||
t.index ["stage_id"], name: "index_cards_on_stage_id"
|
||||
end
|
||||
|
||||
create_table "closers_filters", id: false, force: :cascade do |t|
|
||||
t.integer "closer_id", null: false
|
||||
t.integer "filter_id", null: false
|
||||
t.index ["closer_id"], name: "index_closers_filters_on_closer_id"
|
||||
t.index ["filter_id"], name: "index_closers_filters_on_filter_id"
|
||||
end
|
||||
|
||||
create_table "closure_reasons", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.string "label"
|
||||
@@ -325,6 +332,16 @@ ActiveRecord::Schema[8.1].define(version: 2025_07_02_160230) do
|
||||
t.index ["user_id"], name: "index_sessions_on_user_id"
|
||||
end
|
||||
|
||||
create_table "steps", force: :cascade do |t|
|
||||
t.integer "card_id", null: false
|
||||
t.boolean "completed", default: false, null: false
|
||||
t.text "content", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["card_id", "completed"], name: "index_steps_on_card_id_and_completed"
|
||||
t.index ["card_id"], name: "index_steps_on_card_id"
|
||||
end
|
||||
|
||||
create_table "taggings", force: :cascade do |t|
|
||||
t.integer "card_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -400,6 +417,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_07_02_160230) do
|
||||
add_foreign_key "pins", "users"
|
||||
add_foreign_key "search_queries", "users"
|
||||
add_foreign_key "sessions", "users"
|
||||
add_foreign_key "steps", "cards"
|
||||
add_foreign_key "taggings", "cards"
|
||||
add_foreign_key "taggings", "tags"
|
||||
add_foreign_key "watches", "cards"
|
||||
|
||||
+119
-11
@@ -451,7 +451,7 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &33 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: title
|
||||
cast_type: *7
|
||||
@@ -462,6 +462,18 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
closers_filters:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: closer_id
|
||||
cast_type: *3
|
||||
sql_type_metadata: *4
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *19
|
||||
closure_reasons:
|
||||
- *5
|
||||
- *6
|
||||
@@ -520,11 +532,11 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: all_access
|
||||
cast_type: &32 !ruby/object:ActiveModel::Type::Boolean
|
||||
cast_type: &30 !ruby/object:ActiveModel::Type::Boolean
|
||||
precision:
|
||||
scale:
|
||||
limit:
|
||||
sql_type_metadata: &33 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type_metadata: &31 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type: boolean
|
||||
type: :boolean
|
||||
limit:
|
||||
@@ -744,7 +756,7 @@ columns:
|
||||
comment:
|
||||
filters_tags:
|
||||
- *19
|
||||
- &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &32 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: tag_id
|
||||
cast_type: *3
|
||||
@@ -940,23 +952,48 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
- *25
|
||||
steps:
|
||||
- *22
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: completed
|
||||
cast_type: *30
|
||||
sql_type_metadata: *31
|
||||
'null': false
|
||||
default: false
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: content
|
||||
cast_type: *15
|
||||
sql_type_metadata: *16
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *5
|
||||
- *6
|
||||
- *9
|
||||
taggings:
|
||||
- *22
|
||||
- *5
|
||||
- *6
|
||||
- *30
|
||||
- *32
|
||||
- *9
|
||||
tags:
|
||||
- *5
|
||||
- *6
|
||||
- *31
|
||||
- *33
|
||||
- *9
|
||||
users:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: active
|
||||
cast_type: *32
|
||||
sql_type_metadata: *33
|
||||
cast_type: *30
|
||||
sql_type_metadata: *31
|
||||
'null': false
|
||||
default: true
|
||||
default_function:
|
||||
@@ -1015,8 +1052,8 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: watching
|
||||
cast_type: *32
|
||||
sql_type_metadata: *33
|
||||
cast_type: *30
|
||||
sql_type_metadata: *31
|
||||
'null': false
|
||||
default: true
|
||||
default_function:
|
||||
@@ -1067,6 +1104,7 @@ primary_keys:
|
||||
card_engagements: id
|
||||
card_goldnesses: id
|
||||
cards: id
|
||||
closers_filters:
|
||||
closure_reasons: id
|
||||
closures: id
|
||||
collection_publications: id
|
||||
@@ -1088,6 +1126,7 @@ primary_keys:
|
||||
search_queries: id
|
||||
search_results: id
|
||||
sessions: id
|
||||
steps: id
|
||||
taggings: id
|
||||
tags: id
|
||||
users: id
|
||||
@@ -1109,6 +1148,7 @@ data_sources:
|
||||
card_engagements: true
|
||||
card_goldnesses: true
|
||||
cards: true
|
||||
closers_filters: true
|
||||
closure_reasons: true
|
||||
closures: true
|
||||
collection_publications: true
|
||||
@@ -1130,6 +1170,7 @@ data_sources:
|
||||
search_queries: true
|
||||
search_results: true
|
||||
sessions: true
|
||||
steps: true
|
||||
taggings: true
|
||||
tags: true
|
||||
users: true
|
||||
@@ -1528,6 +1569,39 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
closers_filters:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: closers_filters
|
||||
name: index_closers_filters_on_closer_id
|
||||
unique: false
|
||||
columns:
|
||||
- closer_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: closers_filters
|
||||
name: index_closers_filters_on_filter_id
|
||||
unique: false
|
||||
columns:
|
||||
- filter_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
closure_reasons: []
|
||||
closures:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
@@ -2256,6 +2330,40 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
steps:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: steps
|
||||
name: index_steps_on_card_id
|
||||
unique: false
|
||||
columns:
|
||||
- card_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: steps
|
||||
name: index_steps_on_card_id_and_completed
|
||||
unique: false
|
||||
columns:
|
||||
- card_id
|
||||
- completed
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
taggings:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: taggings
|
||||
@@ -2391,4 +2499,4 @@ indexes:
|
||||
comment:
|
||||
valid: true
|
||||
workflows: []
|
||||
version: 20250702160230
|
||||
version: 20250702211937
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
raise "Seeding is just for development" unless Rails.env.development?
|
||||
|
||||
require "active_support/testing/time_helpers"
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
# Seed DSL
|
||||
def seed_account(name)
|
||||
print " #{name}…"
|
||||
|
||||
+15
-5
@@ -50,19 +50,29 @@ collections = [
|
||||
"Documentation"
|
||||
]
|
||||
|
||||
time_range = (60 .. 30.days.in_minutes)
|
||||
|
||||
collections.each_with_index do |collection_name, index|
|
||||
create_collection(collection_name, access_to: authors.sample(3)).tap do |collection|
|
||||
# Create 20 unique cards for each collection
|
||||
card_titles.each do |title|
|
||||
create_card(title,
|
||||
description: "#{title} for #{collection_name} phase #{index + 1}.",
|
||||
collection: collection
|
||||
).tap do |card|
|
||||
travel(-rand(time_range).minutes) do
|
||||
card = create_card title,
|
||||
description: "#{title} for #{collection_name} phase #{index + 1}.",
|
||||
collection: collection,
|
||||
creator: authors.sample
|
||||
|
||||
# Randomly assign to 1-2 authors
|
||||
travel rand(0..20).minutes
|
||||
card.toggle_assignment(authors.sample)
|
||||
card.toggle_assignment(authors.sample) if rand > 0.5
|
||||
|
||||
if rand > 0.5
|
||||
travel rand(0..20).minutes
|
||||
card.toggle_assignment(authors.sample)
|
||||
end
|
||||
|
||||
# Randomly set card state
|
||||
travel rand(0..20).minutes
|
||||
case rand(3)
|
||||
when 0
|
||||
card.engage
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
require "test_helper"
|
||||
|
||||
class Cards::StepsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "create" do
|
||||
card = cards(:logo)
|
||||
|
||||
assert_difference -> { card.steps.count }, +1 do
|
||||
post card_steps_path(card), params: { step: { content: "Research alternatives" } }, as: :turbo_stream
|
||||
assert_card_container_rerendered(card)
|
||||
end
|
||||
|
||||
assert_equal "Research alternatives", card.steps.last.content
|
||||
end
|
||||
|
||||
test "update" do
|
||||
card = cards(:logo)
|
||||
step = card.steps.create!(content: "Original content")
|
||||
|
||||
assert_changes -> { step.reload.content }, from: "Original content", to: "Updated content" do
|
||||
put card_step_path(card, step), params: { step: { content: "Updated content" } }, as: :turbo_stream
|
||||
assert_card_container_rerendered(card)
|
||||
end
|
||||
end
|
||||
|
||||
test "destroy" do
|
||||
card = cards(:logo)
|
||||
step = card.steps.create!(content: "Step to delete")
|
||||
|
||||
assert_difference -> { card.steps.count }, -1 do
|
||||
delete card_step_path(card, step), as: :turbo_stream
|
||||
assert_card_container_rerendered(card)
|
||||
end
|
||||
end
|
||||
|
||||
test "toggle completion" do
|
||||
card = cards(:logo)
|
||||
step = card.steps.create!(content: "Test step", completed: false)
|
||||
|
||||
# Toggle to completed
|
||||
assert_changes -> { step.reload.completed? }, from: false, to: true do
|
||||
put card_step_path(card, step), params: { step: { completed: "1" } }, as: :turbo_stream
|
||||
assert_card_container_rerendered(card)
|
||||
end
|
||||
|
||||
# Toggle back to incomplete
|
||||
assert_changes -> { step.reload.completed? }, from: true, to: false do
|
||||
put card_step_path(card, step), params: { step: { completed: "0" } }, as: :turbo_stream
|
||||
assert_card_container_rerendered(card)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
require "test_helper"
|
||||
|
||||
class Collections::EntropyConfigurationsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
@collection = collections(:writebook)
|
||||
end
|
||||
|
||||
test "update" do
|
||||
put collection_entropy_configuration_path(@collection), params: { collection: { auto_close_period: 1.day, auto_reconsider_period: 2.days } }
|
||||
|
||||
assert_equal 1.day, @collection.entropy_configuration.reload.auto_close_period
|
||||
assert_equal 2.days, @collection.entropy_configuration.reload.auto_reconsider_period
|
||||
|
||||
assert_turbo_stream action: :replace, target: dom_id(@collection, :entropy_configuration)
|
||||
end
|
||||
end
|
||||
@@ -13,7 +13,7 @@ class Collections::PublicationsControllerTest < ActionDispatch::IntegrationTest
|
||||
post collection_publication_path(@collection)
|
||||
end
|
||||
|
||||
assert_redirected_to edit_collection_path(@collection)
|
||||
assert_turbo_stream action: :replace, target: dom_id(@collection, :publication)
|
||||
end
|
||||
|
||||
test "unpublish a collection" do
|
||||
@@ -24,6 +24,6 @@ class Collections::PublicationsControllerTest < ActionDispatch::IntegrationTest
|
||||
delete collection_publication_path(@collection)
|
||||
end
|
||||
|
||||
assert_redirected_to edit_collection_path(@collection)
|
||||
assert_turbo_stream action: :replace, target: dom_id(@collection, :publication)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class Collections::WorkflowsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
patch collection_workflow_path(collection), params: { collection: { workflow_id: workflows(:on_call).id } }
|
||||
|
||||
assert_redirected_to cards_path(collection_ids: [ collection.id ])
|
||||
assert_turbo_stream action: :replace, target: dom_id(collection, :workflows)
|
||||
assert_equal workflows(:on_call), collection.reload.workflow
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
require "test_helper"
|
||||
|
||||
class ControllerAuthenticationTest < ActionDispatch::IntegrationTest
|
||||
test "access without an account slug redirects to launchpad" do
|
||||
integration_session.default_url_options[:script_name] = "" # no tenant
|
||||
|
||||
get cards_path
|
||||
|
||||
assert_redirected_to Launchpad.login_url(product: true)
|
||||
end
|
||||
|
||||
test "access with an account slug but no session redirects to launchpad" do
|
||||
get cards_path
|
||||
|
||||
assert_redirected_to Launchpad.login_url(product: true, account: Account.sole)
|
||||
end
|
||||
|
||||
test "access with an account slug and a session allows functional access" do
|
||||
sign_in_as :kevin
|
||||
|
||||
get cards_path
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user