Merge branch 'main' into jz-5-28-25
* main: (58 commits) Safari doesn't support CSS `field-sizing: content` so we can shim it for now Hide bubbles in trays update lexical update lexical Update script to deal with duplicated records Revert "Revert "Replace House with Lexical"" Revert "Replace House with Lexical" Update actiontext lexical Remove trace Update lexical Use existing method Update lexical Fix merge issues Format Cleanup Format Make tests pass with action text Don't choke when rendering commands where the card was deleted Update lexical Simple format excerpts in activity ...
This commit is contained in:
+1
-1
@@ -47,7 +47,7 @@ FROM base
|
||||
|
||||
# Install packages needed for deployment
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y curl libsqlite3-0 libvips && \
|
||||
apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# Copy built artifacts: gems, application
|
||||
|
||||
@@ -27,6 +27,8 @@ gem "rqrcode"
|
||||
gem "redcarpet"
|
||||
gem "rouge"
|
||||
gem "jbuilder"
|
||||
gem "actiontext-lexical", bc: "actiontext-lexical"
|
||||
gem "image_processing", "~> 1.14"
|
||||
gem "platform_agent"
|
||||
|
||||
# Telemetry and logging
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
GIT
|
||||
remote: https://github.com/basecamp/actiontext-lexical
|
||||
revision: 6e53a32d373bc73b8b49461338d22727499158b1
|
||||
specs:
|
||||
actiontext-lexical (0.1.0)
|
||||
rails (>= 8.0.2)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/active_record-tenanted
|
||||
revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b
|
||||
@@ -188,6 +195,10 @@ GEM
|
||||
net-http (>= 0.5.0)
|
||||
faraday-retry (2.3.1)
|
||||
faraday (~> 2.0)
|
||||
ffi (1.17.2)
|
||||
ffi (1.17.2-arm64-darwin)
|
||||
ffi (1.17.2-x86_64-darwin)
|
||||
ffi (1.17.2-x86_64-linux-gnu)
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
@@ -199,6 +210,9 @@ GEM
|
||||
hashdiff (1.1.2)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.14.0)
|
||||
mini_magick (>= 4.9.5, < 6)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
importmap-rails (2.1.0)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
@@ -236,6 +250,9 @@ GEM
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
mini_magick (5.2.0)
|
||||
benchmark
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.25.5)
|
||||
@@ -354,6 +371,9 @@ GEM
|
||||
rubocop-performance (>= 1.24)
|
||||
rubocop-rails (>= 2.30)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.3)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
rubyzip (2.4.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.32.0)
|
||||
@@ -448,6 +468,7 @@ PLATFORMS
|
||||
x86_64-mingw32
|
||||
|
||||
DEPENDENCIES
|
||||
actiontext-lexical!
|
||||
active_record-tenanted!
|
||||
bcrypt (~> 3.1.7)
|
||||
bootsnap
|
||||
@@ -456,6 +477,7 @@ DEPENDENCIES
|
||||
capybara
|
||||
debug
|
||||
geared_pagination (~> 1.2)
|
||||
image_processing (~> 1.14)
|
||||
importmap-rails
|
||||
jbuilder
|
||||
kamal
|
||||
|
||||
@@ -177,6 +177,17 @@
|
||||
--color-card-7: oklch(var(--lch-purple-medium));
|
||||
--color-card-8: oklch(var(--lch-pink-medium));
|
||||
|
||||
/* Colors: Syntax highlighting */
|
||||
--color-code-token__att: oklch(var(--lch-blue-dark));
|
||||
--color-code-token__comment: oklch(var(--lch-ink-medium));
|
||||
--color-code-token__function: oklch(var(--lch-purple-dark));
|
||||
--color-code-token__operator: oklch(var(--lch-red-dark));
|
||||
--color-code-token__property: oklch(var(--lch-purple-dark));
|
||||
--color-code-token__punctuation: oklch(var(--lch-ink-dark));
|
||||
--color-code-token__selector: oklch(var(--lch-green-dark));
|
||||
--color-code-token__variable: oklch(var(--lch-red-dark));
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--lch-canvas: 20% 0.0195 232.58;
|
||||
--lch-ink-inverted: var(--lch-black);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
lexical-editor {
|
||||
.node--selected {
|
||||
&:not(:has(img)) {
|
||||
box-shadow: 0 0 0 var(--hover-size) var(--hover-color);
|
||||
}
|
||||
|
||||
&:has(img) {
|
||||
img {
|
||||
box-shadow: 0 0 0 var(--hover-size) var(--hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lexical-dialog-actions {
|
||||
display: flex;
|
||||
font-size: var(--text-x-small);
|
||||
flex: 1 1 0%;
|
||||
gap: var(--inline-space-half);
|
||||
margin-block-start: var(--block-space-half);
|
||||
|
||||
.btn {
|
||||
--radius: 0.3em;
|
||||
|
||||
inline-size: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
span {
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.lexical-editor__content {
|
||||
--outline-size: max(2px, 0.08em);
|
||||
|
||||
margin: var(--block-space-half) 0;
|
||||
min-block-size: calc(7lh + var(--block-space));
|
||||
outline: 0;
|
||||
padding: var(--block-space-half) var(--inline-space);
|
||||
|
||||
* {
|
||||
&:first-child {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lexical-editor--drag-over {
|
||||
background-color: var(--color-selected);
|
||||
border-radius: 4px;
|
||||
outline: 2px dashed var(--color-selected-dark);
|
||||
}
|
||||
|
||||
lexical-toolbar {
|
||||
background-color: inherit;
|
||||
border-block-end: 1px solid var(--color-ink-light);
|
||||
color: currentColor;
|
||||
display: flex;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
max-inline-size: 100%;
|
||||
padding: 0.2em 0;
|
||||
position: relative;
|
||||
|
||||
dialog {
|
||||
background-color: var(--color-canvas);
|
||||
border: 1px solid var(--color-ink-lighter);
|
||||
border-radius: 0.5em;
|
||||
box-shadow: var(--shadow);
|
||||
color: var(--color-ink);
|
||||
padding: var(--block-space) calc(var(--inline-space) * 1.5);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
> button {
|
||||
aspect-ratio: 4/3.5;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
block-size: 2em;
|
||||
border: none;
|
||||
border-radius: 0.2em;
|
||||
color: currentColor;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
font-size: inherit;
|
||||
inline-size: auto;
|
||||
place-items: center;
|
||||
transition: background-color 300ms ease;
|
||||
|
||||
svg {
|
||||
-webkit-touch-callout: none;
|
||||
block-size: 0.85em;
|
||||
fill: currentColor;
|
||||
grid-area: 1/1;
|
||||
inline-size: auto;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&:is(:focus, :hover) {
|
||||
background-color: var(--color-ink-lighter);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:is(:active) {
|
||||
background-color: var(--color-selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
@layer components {
|
||||
@supports not (field-sizing: content) {
|
||||
.autoresize__wrapper {
|
||||
display: grid !important;
|
||||
position: relative;
|
||||
|
||||
> *, &::after {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: attr(data-autoresize-clone-value) " ";
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
visibility: hidden;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.autoresize__textarea {
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,6 +150,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card-field__description {
|
||||
lexical-toolbar {
|
||||
border-block-start: 1px solid var(--color-ink-light);
|
||||
margin-block-start: var(--block-margin) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.card__content {
|
||||
flex: 2 1 auto;
|
||||
max-inline-size: 100%;
|
||||
@@ -167,6 +174,12 @@
|
||||
line-height: 1.2;
|
||||
text-wrap: balance;
|
||||
|
||||
@supports not (field-sizing: content) {
|
||||
&:has(textarea) {
|
||||
text-wrap: unset; /* Safari is annoying if you have text-wrap: balance in textareas */
|
||||
}
|
||||
}
|
||||
|
||||
.card-field__title {
|
||||
&:is(textarea)::placeholder {
|
||||
color: inherit;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@layer components {
|
||||
.comments {
|
||||
--avatar-size: 2.33em;
|
||||
--comment-padding-block: var(--block-space);
|
||||
--comment-padding-block: var(--block-space-half);
|
||||
--comment-padding-inline: var(--inline-space-double);
|
||||
--comment-max: 70ch;
|
||||
--reaction-size: 2rem;
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
|
||||
.comment__avatar {
|
||||
margin: calc(var(--comment-padding-block) * 0.5) calc(var(--comment-padding-inline) * -0.75);
|
||||
margin: calc(var(--comment-padding-block) * 0.75) calc(var(--comment-padding-inline) * -0.75);
|
||||
z-index: 0;
|
||||
|
||||
.comment--mine_ & {
|
||||
@@ -64,7 +64,7 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
* {
|
||||
&:first-child {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
@@ -83,6 +83,8 @@
|
||||
border-radius: 0.2em;
|
||||
padding:
|
||||
var(--comment-padding-block)
|
||||
var(--comment-padding-inline)
|
||||
calc(var(--comment-padding-block) * 1.5)
|
||||
var(--comment-padding-inline);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@@ -112,7 +114,7 @@
|
||||
|
||||
max-inline-size: var(--comment-max);
|
||||
text-align: center;
|
||||
|
||||
|
||||
|
||||
&::before {
|
||||
/* Make up space for lack of avatar */
|
||||
@@ -135,17 +137,4 @@
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__input {
|
||||
--input-border-size: 0;
|
||||
--input-padding: 0;
|
||||
|
||||
min-block-size: calc(9lh + (2 * var(--comment-padding-block)));
|
||||
min-inline-size: 100%;
|
||||
|
||||
@supports (field-sizing: content) {
|
||||
field-sizing: content;
|
||||
min-block-size: calc(9lh + (2 * var(--comment-padding-block)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
@layer components {
|
||||
.markdown-content {
|
||||
:where(h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6) {
|
||||
font-weight: 800;
|
||||
hyphens: auto;
|
||||
letter-spacing: -0.02ch;
|
||||
line-height: 1.1;
|
||||
margin-block: 0;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
:where(h1, h2, h3, h4, h5, h6) { display: block; }
|
||||
:where(h1, h2, h3, h4, h5, h6) { margin-block: 0.65em; }
|
||||
|
||||
:is(h1, .h1) { font-size: 2em; }
|
||||
:is(h2, .h2) { font-size: 1.5em; }
|
||||
:is(h3, .h3) { font-size: 1.17em; }
|
||||
:is(h4, .h4) { font-size: 1em; }
|
||||
:is(h5, .h5) { font-size: 0.83em; }
|
||||
:is(h6, .h6) { font-size: 0.67em; }
|
||||
|
||||
:where(p, ul, ol, dl, blockquote, pre, figure, table, hr) {
|
||||
margin-block: 0.65lh;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
:where(hr) {
|
||||
border-color: var(--color-ink-medium);
|
||||
border-style: var(--border-style, solid) none none;
|
||||
margin: 2lh auto;
|
||||
}
|
||||
|
||||
:where(strong) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:is(pre, code, .pre, .code) {
|
||||
background-color: var(--color-ink-lightest);
|
||||
border: 1px solid var(--color-ink-lighter);
|
||||
border-radius: 0.3em;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
:is(pre) {
|
||||
overflow-x: auto;
|
||||
/* text-wrap: nowrap; */
|
||||
}
|
||||
|
||||
:where(code) {
|
||||
padding: 0.1em 0.3em;
|
||||
}
|
||||
|
||||
:is(pre, .pre) {
|
||||
border-radius: 0.5em;
|
||||
padding: 0.5lh 2ch;
|
||||
|
||||
:is(code, .code) {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
font-size: 1em;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:where(p) {
|
||||
hyphens: auto;
|
||||
letter-spacing: -0.005ch;
|
||||
}
|
||||
|
||||
:is(blockquote, .quote) {
|
||||
font-style: italic;
|
||||
margin: 0 3ch;
|
||||
|
||||
:is(p) {
|
||||
hyphens: none;
|
||||
}
|
||||
}
|
||||
|
||||
:where(table, th, td) {
|
||||
border: 1px solid var(--color-ink-medium);
|
||||
}
|
||||
|
||||
:where(table) {
|
||||
border-collapse: collapse;
|
||||
margin: 1lh 0;
|
||||
}
|
||||
|
||||
:where(th, td) {
|
||||
padding: 0.2lh 1ch;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
:where(th) {
|
||||
border-block-end-width: 3px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:where(img, video, embed, object) {
|
||||
max-block-size: 32rem;
|
||||
|
||||
/* Links should hug media contained within */
|
||||
a:has(&) {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
@layer components {
|
||||
.rich-text-content {
|
||||
--block-margin: 0.65lh;
|
||||
|
||||
:where(h1, h2, h3, h4, h5, h6) {
|
||||
display: block;
|
||||
font-weight: 800;
|
||||
hyphens: auto;
|
||||
letter-spacing: -0.02ch;
|
||||
line-height: 1.1;
|
||||
margin-block: var(--block-margin);
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
:where(h1) { font-size: 2em; }
|
||||
:where(h2) { font-size: 1.5em; }
|
||||
:where(h3) { font-size: 1.17em; }
|
||||
:where(h4) { font-size: 1em; }
|
||||
:where(h5) { font-size: 0.83em; }
|
||||
:where(h6) { font-size: 0.67em; }
|
||||
|
||||
:where(p, ul, ol, dl, blockquote, figure, .attachment) {
|
||||
margin-block: var(--block-margin);
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
:where(b, strong) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:where(p, blockquote) {
|
||||
hyphens: auto;
|
||||
letter-spacing: -0.005ch;
|
||||
}
|
||||
|
||||
:where(blockquote) {
|
||||
border-inline-start: 0.25em solid var(--color-ink-lighter);
|
||||
font-style: italic;
|
||||
margin: var(--block-margin) 0;
|
||||
padding: 0.5lh 2ch;
|
||||
}
|
||||
|
||||
:where(img, video, embed, object) {
|
||||
margin-inline: auto;
|
||||
max-block-size: 32rem;
|
||||
|
||||
/* Links should hug media contained within */
|
||||
a:has(&) {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Attachments */
|
||||
.attachment {
|
||||
block-size: auto;
|
||||
display: inline-block;
|
||||
inline-size: 100%;
|
||||
position: relative;
|
||||
margin-inline: 0;
|
||||
max-inline-size: 100%;
|
||||
|
||||
&:is(figure) img {
|
||||
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__figure {
|
||||
--hover-size: 0;
|
||||
}
|
||||
|
||||
.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 {
|
||||
--hover-size: 0;
|
||||
|
||||
color: var(--color-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Attachment junk gets wrapped in a paragraph causing unwanted space */
|
||||
p:has(action-text-attachment) {
|
||||
display: none;
|
||||
|
||||
+ * {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Avoid extra space due to empty paragraphs */
|
||||
p:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Code */
|
||||
:where(code, pre) {
|
||||
background-color: var(--color-canvas);
|
||||
border: 1px solid var(--color-ink-lighter);
|
||||
border-radius: 0.3em;
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
margin-block: var(--block-margin);
|
||||
padding: 0.1em 0.3em;
|
||||
|
||||
&[data-language] {
|
||||
border-radius: 0.5em;
|
||||
display: block;
|
||||
hyphens: none;
|
||||
overflow-x: auto;
|
||||
padding: 0.5lh 2ch;
|
||||
tab-size: 2;
|
||||
text-wrap: nowrap;
|
||||
white-space: pre;
|
||||
word-break: break-word;
|
||||
|
||||
/* Keywords and attributes */
|
||||
.code-token__attr,
|
||||
.token.atrule,
|
||||
.token.attr,
|
||||
.token.keyword {
|
||||
color: var(--color-code-token__att);
|
||||
}
|
||||
|
||||
/* Constants, booleans, numbers, properties, tags */
|
||||
.code-token__property,
|
||||
.token.boolean,
|
||||
.token.constant,
|
||||
.token.deleted,
|
||||
.token.number,
|
||||
.token.property,
|
||||
.token.symbol,
|
||||
.token.tag {
|
||||
color: var(--color-code-token__property);
|
||||
}
|
||||
|
||||
/* Strings, selectors, and built-in constructs */
|
||||
.code-token__selector,
|
||||
.token.builtin,
|
||||
.token.char,
|
||||
.token.inserted,
|
||||
.token.selector,
|
||||
.token.string {
|
||||
color: var(--color-code-token__selector);
|
||||
}
|
||||
|
||||
/* Comments and meta information */
|
||||
.code-token__comment,
|
||||
.token.cdata,
|
||||
.token.comment,
|
||||
.token.doctype,
|
||||
.token.prolog {
|
||||
color: var(--color-code-token__comment);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Operators and symbolic entities */
|
||||
.code-token__operator,
|
||||
.token.entity,
|
||||
.token.operator,
|
||||
.token.url {
|
||||
color: var(--color-code-token__operator);
|
||||
}
|
||||
|
||||
/* Functions and class names */
|
||||
.code-token__function,
|
||||
.token.class,
|
||||
.token.class-name,
|
||||
.token.function {
|
||||
color: var(--color-code-token__function);
|
||||
}
|
||||
|
||||
/* Variables, regex, namespaces, important */
|
||||
.code-token__variable,
|
||||
.token.important,
|
||||
.token.namespace,
|
||||
.token.regex,
|
||||
.token.variable {
|
||||
color: var(--color-code-token__variable);
|
||||
}
|
||||
|
||||
/* Punctuation */
|
||||
.code-token__punctuation,
|
||||
.token.punctuation {
|
||||
color: var(--color-code-token__punctuation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,10 @@
|
||||
--tray-item-margin: 0;
|
||||
--tray-item-scale: 1;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tray__item--overflow {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
module ExcerptHelper
|
||||
def format_excerpt(content, length: 200)
|
||||
return "" if content.blank?
|
||||
|
||||
text = content.to_plain_text
|
||||
text = text.gsub(/^>\s*(.*)$/m, '> \1')
|
||||
text = text.gsub(/^[-*]\s*(.*)$/m, '• \1')
|
||||
text = text.gsub(/^\d+\.\s*(.*)$/m) { |m| m }
|
||||
text = text.gsub(/\s+/, " ").strip
|
||||
text.truncate(length)
|
||||
end
|
||||
end
|
||||
@@ -58,7 +58,7 @@ module NotificationsHelper
|
||||
|
||||
def comment_notification_body(event)
|
||||
comment = event.eventable
|
||||
"#{strip_tags(comment.body_html).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body_html).truncate(200)}"
|
||||
"#{strip_tags(comment.body.to_s).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body.to_s).truncate(200)}"
|
||||
end
|
||||
|
||||
def card_notification_title(card)
|
||||
|
||||
@@ -2,4 +2,6 @@
|
||||
import "@hotwired/turbo-rails"
|
||||
import "initializers"
|
||||
import "controllers"
|
||||
import "house"
|
||||
|
||||
import "actiontext-lexical"
|
||||
import "@rails/actiontext"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["textarea", "wrapper"]
|
||||
|
||||
connect() {
|
||||
this.resize()
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.wrapperTarget.setAttribute("data-autoresize-clone-value", this.textareaTarget.value)
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,10 @@ export default class extends Controller {
|
||||
|
||||
async restoreContent() {
|
||||
await nextFrame()
|
||||
const savedContent = localStorage.getItem(this.keyValue)
|
||||
let savedContent = localStorage.getItem(this.keyValue)
|
||||
|
||||
if (savedContent) {
|
||||
savedContent = `<div>${savedContent}</div>` // temporary for old markdown saves
|
||||
this.inputTarget.value = savedContent
|
||||
this.#triggerChangeEvent(savedContent)
|
||||
}
|
||||
@@ -45,8 +46,8 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
#triggerChangeEvent(newValue) {
|
||||
if (this.inputTarget.tagName === "HOUSE-MD") {
|
||||
this.inputTarget.dispatchEvent(new CustomEvent('house-md:change', {
|
||||
if (this.inputTarget.tagName === "LEXICAL-EDITOR") {
|
||||
this.inputTarget.dispatchEvent(new CustomEvent('actiontext:change', {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
previousContent: '',
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
pasteFiles(event) {
|
||||
const editor = event.target.closest("house-md")
|
||||
if (!editor) return
|
||||
|
||||
const files = event.clipboardData?.files
|
||||
if (!files?.length) return
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
for (const file of files) {
|
||||
const uploadEvent = new CustomEvent("house-md:before-upload", {
|
||||
bubbles: true,
|
||||
detail: { file },
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
if (editor.dispatchEvent(uploadEvent)) {
|
||||
const upload = document.createElement("house-md-upload")
|
||||
upload.file = file
|
||||
upload.uploadsURL = editor.dataset.uploadsUrl
|
||||
editor.appendChild(upload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { highlightAll } from "actiontext-lexical"
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
highlightAll()
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ class Card < ApplicationRecord
|
||||
has_many :comments, dependent: :destroy
|
||||
has_one_attached :image, dependent: :purge_later
|
||||
|
||||
has_markdown :description
|
||||
has_rich_text :description
|
||||
|
||||
before_save :set_default_title, if: :published?
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ class Command::GoToCard < Command
|
||||
validates_presence_of :card
|
||||
|
||||
def title
|
||||
"Visit card '#{card.title}'"
|
||||
"Visit card '#{card&.title}'"
|
||||
end
|
||||
|
||||
def execute
|
||||
@@ -13,6 +13,6 @@ class Command::GoToCard < Command
|
||||
|
||||
private
|
||||
def card
|
||||
user.accessible_cards.find(card_id)
|
||||
user.accessible_cards.find_by_id(card_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ class Comment < ApplicationRecord
|
||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||
has_many :reactions, dependent: :delete_all
|
||||
|
||||
has_markdown :body
|
||||
has_rich_text :body
|
||||
searchable_by :body_plain_text, using: :comments_search_index, as: :body
|
||||
|
||||
scope :chronologically, -> { order created_at: :asc, id: :desc }
|
||||
@@ -19,6 +19,10 @@ class Comment < ApplicationRecord
|
||||
end
|
||||
|
||||
private
|
||||
def body_plain_text
|
||||
body.to_plain_text
|
||||
end
|
||||
|
||||
def watch_card_by_creator
|
||||
card.watch_by creator
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ module Mentions
|
||||
end
|
||||
|
||||
def mentionable_content
|
||||
markdown_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ")
|
||||
rich_text_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ")
|
||||
end
|
||||
|
||||
private
|
||||
@@ -32,12 +32,12 @@ module Mentions
|
||||
collection.users
|
||||
end
|
||||
|
||||
def markdown_associations
|
||||
self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::Markdown }
|
||||
def rich_text_associations
|
||||
self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::RichText }
|
||||
end
|
||||
|
||||
def mentionable_content_changed?
|
||||
markdown_associations.any? { send(it.name).content_previously_changed? }
|
||||
rich_text_associations.any? { send(it.name).body_previously_changed? }
|
||||
end
|
||||
|
||||
def create_mentions_later
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<% if blob.representable? %>
|
||||
<figure class="attachment attachment--preview attachment--<%= blob.filename.extension %>">
|
||||
<%= render "active_storage/blobs/web/representation", blob: blob %>
|
||||
|
||||
<figcaption class="attachment__caption">
|
||||
<% if caption = blob.try(:caption) %>
|
||||
<span><%= caption %></span>
|
||||
<% else %>
|
||||
<span class="attachment__name"><%= blob.filename %></span>
|
||||
<% end %>
|
||||
<span> · </span>
|
||||
<span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
|
||||
<span> · </span>
|
||||
<%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__link", download: blob.filename, title: "Download #{blob.filename}" do %>
|
||||
<span>Download</span>
|
||||
<% end %>
|
||||
</figcaption>
|
||||
</figure>
|
||||
<% else %>
|
||||
<div class="attachment attachment--file attachment--<%= blob.filename.extension -%>">
|
||||
<%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__figure", download: blob.filename, title: "Download #{blob.filename}" do %>
|
||||
<%= render "active_storage/blobs/web/representation", blob: blob %>
|
||||
<% end %>
|
||||
|
||||
<div class="attachment__caption">
|
||||
<div>
|
||||
<% if caption = blob.try(:caption) %>
|
||||
<strong><%= caption %></strong>
|
||||
<% else %>
|
||||
<strong class="attachment__name"><%= blob.filename %></strong>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
|
||||
<span> · </span>
|
||||
<%= link_to rails_blob_path(blob, disposition: :attachment), class: "attachment__link", download: blob.filename, title: "Download #{blob.filename}" do %>
|
||||
<span>Download</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,19 @@
|
||||
<% size = local_assigns[:in_gallery] ? [800, 600] : [1024, 768] %>
|
||||
|
||||
<% if blob.video? %>
|
||||
<%= tag.video \
|
||||
src: rails_blob_path(blob),
|
||||
controls: true,
|
||||
preload: :none,
|
||||
width: "100%", height: "100%" %>
|
||||
<% elsif blob.audio? %>
|
||||
<audio controls="true" width="100%" preload="metadata">
|
||||
<source src="<%= rails_blob_url(blob) %>" type="<%= blob.content_type %>">
|
||||
</audio>
|
||||
<% elsif blob.variable? %>
|
||||
<%= image_tag url_for(blob.variant(loader: { n: -1 }, resize_to_limit: size)) %>
|
||||
<% elsif blob.previewable? %>
|
||||
<%= image_tag url_for(blob.preview(resize_to_limit: size)) %>
|
||||
<% else %>
|
||||
<span class="attachment__icon"><%= blob.filename.extension&.downcase.presence || "unknown" %></span>
|
||||
<% end %>
|
||||
@@ -28,8 +28,8 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="comment__body markdown-content txt-align-start">
|
||||
<%= sanitize comment.body_html %>
|
||||
<div class="comment__body rich-text-content txt-align-start" data-controller="syntax-highlight">
|
||||
<%= comment.body %>
|
||||
</div>
|
||||
|
||||
<%= render "cards/comments/reactions/reactions", comment: comment %>
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
</figure>
|
||||
|
||||
<div class="comment__content flex-inline flex-column full-width">
|
||||
<div class="comment__body markdown-content txt-align-start">
|
||||
<div class="comment__body rich-text-content txt-align-start">
|
||||
<%= form_with model: Comment.new, url: card_comments_path(card), class: "flex flex-column gap full-width",
|
||||
data: { controller: "form paste local-save",
|
||||
data: { controller: "form local-save",
|
||||
local_save_key_value: "comment-#{card.id}",
|
||||
action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %>
|
||||
<%= form.markdown_area :body, class: "input comment__input", required: true, placeholder: new_comment_placeholder(card),
|
||||
data: { local_save_target: "input", action: "house-md:change->local-save#save turbo:morph-element->local-save#restoreContent" } %>
|
||||
action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %>
|
||||
<%= form.rich_textarea :body, required: true, placeholder: new_comment_placeholder(card),
|
||||
data: { local_save_target: "input", action: "actiontext:change->local-save#save turbo:morph-element->local-save#restoreContent" } %>
|
||||
<%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %>
|
||||
<span>Post this comment</span>
|
||||
<% end %>
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
</figure>
|
||||
|
||||
<div class="comment__content flex flex-column flex-item-grow full-width">
|
||||
<div class="comment__body markdown-content txt-align-start">
|
||||
<div class="comment__body rich-text-content txt-align-start">
|
||||
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width",
|
||||
data: { controller: "form paste", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %>
|
||||
<%= form.markdown_area :body, class: "input comment__input", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %>
|
||||
data: { controller: "form", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %>
|
||||
<%= form.rich_textarea :body, required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %>
|
||||
<div class="flex gap-half justify-start">
|
||||
<%= form.button class: "btn btn--reversed", type: :submit do %>
|
||||
<span>Save changes</span>
|
||||
|
||||
@@ -4,23 +4,23 @@
|
||||
<h1 class="card__title flex align-start gap-half">
|
||||
<%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>
|
||||
</h1>
|
||||
<div class="card__description markdown-content margin-block-half">
|
||||
<%= sanitize card.description_html %>
|
||||
<div class="card__description rich-text-content margin-block-half rich-text-content" data-controller="syntax-highlight">
|
||||
<%= card.description %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "paste auto-save" } do |form| %>
|
||||
<h1 class="card__title">
|
||||
<%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "autoresize auto-save" } do |form| %>
|
||||
<h1 class="card__title autoresize__wrapper" data-autoresize-target="wrapper" data-autoresize-clone-value="">
|
||||
<%= form.text_area :title, placeholder: "Name it…",
|
||||
class: "input input--textarea full-width borderless txt-align-start",
|
||||
autofocus: card.title.blank?,
|
||||
data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
|
||||
class: "input input--textarea full-width borderless txt-align-start autoresize__textarea",
|
||||
autofocus: card.title.blank?, rows: 1,
|
||||
data: { autoresize_target: "textarea", action: "input->autoresize#resize auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
|
||||
</h1>
|
||||
<div class="card__description markdown-content margin-block-start-half margin-block-end">
|
||||
<%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description",
|
||||
<div class="card__description rich-text-content margin-block-start-half margin-block-end">
|
||||
<%= form.rich_textarea :description, class: "card-field__description",
|
||||
placeholder: "Add some notes, context, pictures, or video about this…",
|
||||
data: { action: "house-md:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
|
||||
data: { action: "actiontext:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
<%= turbo_frame_tag @card, :edit do %>
|
||||
<%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "form paste" } do |form| %>
|
||||
<%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "autoresize form" } do |form| %>
|
||||
<h1 class="card__title">
|
||||
<%= form.label :title, class: "flex flex-column align-center" do %>
|
||||
<%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title",
|
||||
required: true, autofocus: true, placeholder: "Name it…",
|
||||
rows: 1,
|
||||
data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %>
|
||||
<%= form.label :title, class: "flex flex-column align-center autoresize__wrapper", data: { autoresize_target: "wrapper", autoresize_clone_value: "" } do %>
|
||||
<%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title autoresize__textarea",
|
||||
required: true, autofocus: true, placeholder: "Name it…", rows: 1,
|
||||
data: { autoresize_target: "textarea", action: "input->autoresize#resize keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %>
|
||||
<% end %>
|
||||
</h1>
|
||||
<div class="card__description markdown-content margin-block-start-half margin-block-end">
|
||||
<%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description",
|
||||
placeholder: "Add some notes, context, pictures, or video about this…",
|
||||
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
|
||||
|
||||
<%= form.rich_textarea :description, class: "card-field__description rich-text-content",
|
||||
placeholder: "Add some notes, context, pictures, or video about this…",
|
||||
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
|
||||
</div>
|
||||
<%= form.button "Save changes", type: :submit, class: "btn" %>
|
||||
<%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%= render "events/event/layout", card: event.eventable.card, event: event do %>
|
||||
<span class="txt-break overflow-line-clamp txt-tight-lines">
|
||||
<%= strip_tags(event&.eventable.body_html).truncate(200) -%>
|
||||
<%= format_excerpt(event&.eventable.body, length: 200) -%>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="action-text-content">
|
||||
<%= yield -%>
|
||||
</div>
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<%= page_title_tag %>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, interactive-widget=resizes-content">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<meta name="view-transition" content="same-origin">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||
|
||||
+3
-1
@@ -5,9 +5,11 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
||||
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
||||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
||||
pin "@rails/request.js", to: "@rails--request.js" # @0.0.11
|
||||
pin "house", to: "house.min.js"
|
||||
|
||||
pin_all_from "app/javascript/controllers", under: "controllers"
|
||||
pin_all_from "app/javascript/helpers", under: "helpers"
|
||||
pin_all_from "app/javascript/initializers", under: "initializers"
|
||||
pin "marked" # @15.0.11
|
||||
pin "actiontext-lexical"
|
||||
pin "@rails/activestorage", to: "activestorage.esm.js"
|
||||
pin "@rails/actiontext", to: "actiontext.esm.js"
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
ActiveSupport.on_load :action_text_markdown do
|
||||
require "markdown_renderer"
|
||||
require "redcarpet/render_strip"
|
||||
|
||||
ActionText::Markdown.html_renderer = ->(content) { MarkdownRenderer.build.render(content) }
|
||||
ActionText::Markdown.plain_text_renderer = Redcarpet::Markdown.new(Redcarpet::Render::StripDown)
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# This migration comes from action_text (originally 20180528164100)
|
||||
class CreateActionTextTables < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
# Use Active Record's configured type for primary and foreign keys
|
||||
primary_key_type, foreign_key_type = primary_and_foreign_key_types
|
||||
|
||||
create_table :action_text_rich_texts, id: primary_key_type do |t|
|
||||
t.string :name, null: false
|
||||
t.text :body, size: :long
|
||||
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
|
||||
|
||||
t.timestamps
|
||||
|
||||
t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def primary_and_foreign_key_types
|
||||
config = Rails.configuration.generators
|
||||
setting = config.options[config.orm][:primary_key_type]
|
||||
primary_key_type = setting || :primary_key
|
||||
foreign_key_type = setting || :bigint
|
||||
[ primary_key_type, foreign_key_type ]
|
||||
end
|
||||
end
|
||||
Generated
+11
-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_05_15_125505) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_05_23_084734) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.integer "collection_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -39,6 +39,16 @@ ActiveRecord::Schema[8.1].define(version: 2025_05_15_125505) do
|
||||
t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record"
|
||||
end
|
||||
|
||||
create_table "action_text_rich_texts", force: :cascade do |t|
|
||||
t.text "body"
|
||||
t.datetime "created_at", null: false
|
||||
t.string "name", null: false
|
||||
t.bigint "record_id", null: false
|
||||
t.string "record_type", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.bigint "blob_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
|
||||
+116
-78
@@ -1,7 +1,7 @@
|
||||
--- !ruby/object:ActiveRecord::ConnectionAdapters::SchemaCache
|
||||
columns:
|
||||
accesses:
|
||||
- &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: collection_id
|
||||
cast_type: &1 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
|
||||
@@ -80,7 +80,7 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &25 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: user_id
|
||||
cast_type: *1
|
||||
@@ -118,13 +118,13 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: content
|
||||
cast_type: &14 !ruby/object:ActiveRecord::Type::Text
|
||||
cast_type: &11 !ruby/object:ActiveRecord::Type::Text
|
||||
true: t
|
||||
false: f
|
||||
precision:
|
||||
scale:
|
||||
limit:
|
||||
sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type: TEXT
|
||||
type: :text
|
||||
limit:
|
||||
@@ -159,17 +159,30 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
active_storage_attachments:
|
||||
action_text_rich_texts:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: body
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *5
|
||||
- *6
|
||||
- *10
|
||||
- &16 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: blob_id
|
||||
cast_type: &11 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
|
||||
name: record_id
|
||||
cast_type: &14 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
|
||||
precision:
|
||||
scale:
|
||||
limit:
|
||||
max: 9223372036854775808
|
||||
min: -9223372036854775808
|
||||
sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type: bigint
|
||||
type: :integer
|
||||
limit:
|
||||
@@ -180,19 +193,23 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *5
|
||||
- *6
|
||||
- *10
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- *13
|
||||
- *9
|
||||
active_storage_attachments:
|
||||
- &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: record_id
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
name: blob_id
|
||||
cast_type: *14
|
||||
sql_type_metadata: *15
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *5
|
||||
- *6
|
||||
- *10
|
||||
- *16
|
||||
- *13
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
@@ -208,8 +225,8 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: byte_size
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
cast_type: *14
|
||||
sql_type_metadata: *15
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
@@ -247,7 +264,7 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
- *6
|
||||
- &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: key
|
||||
cast_type: *7
|
||||
@@ -260,8 +277,8 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: metadata
|
||||
cast_type: *14
|
||||
sql_type_metadata: *15
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
@@ -278,7 +295,7 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
active_storage_variant_records:
|
||||
- *16
|
||||
- *17
|
||||
- *6
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
@@ -292,7 +309,7 @@ columns:
|
||||
comment:
|
||||
ar_internal_metadata:
|
||||
- *5
|
||||
- *17
|
||||
- *18
|
||||
- *9
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
@@ -305,7 +322,7 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
assignees_filters:
|
||||
- &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: assignee_id
|
||||
cast_type: *1
|
||||
@@ -315,7 +332,7 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: filter_id
|
||||
cast_type: *1
|
||||
@@ -326,7 +343,7 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
assigners_filters:
|
||||
- &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: assigner_id
|
||||
cast_type: *1
|
||||
@@ -336,11 +353,11 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *18
|
||||
assignments:
|
||||
- *19
|
||||
assignments:
|
||||
- *20
|
||||
- &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- *21
|
||||
- &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: card_id
|
||||
cast_type: *1
|
||||
@@ -368,14 +385,14 @@ columns:
|
||||
- *6
|
||||
- *9
|
||||
card_goldnesses:
|
||||
- *21
|
||||
- *22
|
||||
- *5
|
||||
- *6
|
||||
- *9
|
||||
cards:
|
||||
- *22
|
||||
- *23
|
||||
- *5
|
||||
- &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: creator_id
|
||||
cast_type: *1
|
||||
@@ -428,14 +445,14 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: status
|
||||
cast_type: *14
|
||||
sql_type_metadata: *15
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': false
|
||||
default: creating
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: title
|
||||
cast_type: *7
|
||||
@@ -461,7 +478,7 @@ columns:
|
||||
comment:
|
||||
- *9
|
||||
closures:
|
||||
- *21
|
||||
- *22
|
||||
- *5
|
||||
- *6
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
@@ -489,11 +506,11 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: all_access
|
||||
cast_type: &31 !ruby/object:ActiveModel::Type::Boolean
|
||||
cast_type: &32 !ruby/object:ActiveModel::Type::Boolean
|
||||
precision:
|
||||
scale:
|
||||
limit:
|
||||
sql_type_metadata: &32 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type_metadata: &33 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type: boolean
|
||||
type: :boolean
|
||||
limit:
|
||||
@@ -507,15 +524,15 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: auto_close_period
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
cast_type: *14
|
||||
sql_type_metadata: *15
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *5
|
||||
- *23
|
||||
- *24
|
||||
- *6
|
||||
- *10
|
||||
- *9
|
||||
@@ -530,18 +547,18 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
collections_filters:
|
||||
- *22
|
||||
- *18
|
||||
- *23
|
||||
- *19
|
||||
commands:
|
||||
- *5
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: data
|
||||
cast_type: &25 !ruby/object:ActiveRecord::Type::Json
|
||||
cast_type: &26 !ruby/object:ActiveRecord::Type::Json
|
||||
precision:
|
||||
scale:
|
||||
limit:
|
||||
sql_type_metadata: &26 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type: json
|
||||
type: :json
|
||||
limit:
|
||||
@@ -556,8 +573,8 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: line
|
||||
cast_type: *14
|
||||
sql_type_metadata: *15
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
@@ -584,16 +601,16 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
- *24
|
||||
- *25
|
||||
comments:
|
||||
- *21
|
||||
- *22
|
||||
- *5
|
||||
- *23
|
||||
- *24
|
||||
- *6
|
||||
- *9
|
||||
creators_filters:
|
||||
- *23
|
||||
- *18
|
||||
- *24
|
||||
- *19
|
||||
events:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
@@ -605,9 +622,9 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *22
|
||||
- *5
|
||||
- *23
|
||||
- *5
|
||||
- *24
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: eventable_id
|
||||
@@ -632,8 +649,8 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: particulars
|
||||
cast_type: *25
|
||||
sql_type_metadata: *26
|
||||
cast_type: *26
|
||||
sql_type_metadata: *27
|
||||
'null': true
|
||||
default: "{}"
|
||||
default_function:
|
||||
@@ -642,12 +659,12 @@ columns:
|
||||
- *9
|
||||
filters:
|
||||
- *5
|
||||
- *23
|
||||
- *24
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: fields
|
||||
cast_type: *25
|
||||
sql_type_metadata: *26
|
||||
cast_type: *26
|
||||
sql_type_metadata: *27
|
||||
'null': false
|
||||
default: "{}"
|
||||
default_function:
|
||||
@@ -666,7 +683,7 @@ columns:
|
||||
comment:
|
||||
- *9
|
||||
filters_stages:
|
||||
- *18
|
||||
- *19
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: stage_id
|
||||
@@ -678,8 +695,8 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
filters_tags:
|
||||
- *18
|
||||
- &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- *19
|
||||
- &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: tag_id
|
||||
cast_type: *1
|
||||
@@ -712,7 +729,7 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- &27 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: source_id
|
||||
cast_type: *1
|
||||
@@ -722,7 +739,7 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: source_type
|
||||
cast_type: *7
|
||||
@@ -756,16 +773,16 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *27
|
||||
- *28
|
||||
- *29
|
||||
- *9
|
||||
- *24
|
||||
- *25
|
||||
pins:
|
||||
- *21
|
||||
- *22
|
||||
- *5
|
||||
- *6
|
||||
- *9
|
||||
- *24
|
||||
- *25
|
||||
reactions:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
@@ -882,24 +899,24 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *24
|
||||
- *25
|
||||
taggings:
|
||||
- *21
|
||||
- *22
|
||||
- *5
|
||||
- *6
|
||||
- *29
|
||||
- *30
|
||||
- *9
|
||||
tags:
|
||||
- *5
|
||||
- *6
|
||||
- *30
|
||||
- *31
|
||||
- *9
|
||||
users:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: active
|
||||
cast_type: *31
|
||||
sql_type_metadata: *32
|
||||
cast_type: *32
|
||||
sql_type_metadata: *33
|
||||
'null': false
|
||||
default: true
|
||||
default_function:
|
||||
@@ -940,16 +957,16 @@ columns:
|
||||
comment:
|
||||
- *9
|
||||
watches:
|
||||
- *21
|
||||
- *22
|
||||
- *5
|
||||
- *6
|
||||
- *9
|
||||
- *24
|
||||
- *25
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: watching
|
||||
cast_type: *31
|
||||
sql_type_metadata: *32
|
||||
cast_type: *32
|
||||
sql_type_metadata: *33
|
||||
'null': false
|
||||
default: true
|
||||
default_function:
|
||||
@@ -989,6 +1006,7 @@ primary_keys:
|
||||
accesses: id
|
||||
accounts: id
|
||||
action_text_markdowns: id
|
||||
action_text_rich_texts: id
|
||||
active_storage_attachments: id
|
||||
active_storage_blobs: id
|
||||
active_storage_variant_records: id
|
||||
@@ -1027,6 +1045,7 @@ data_sources:
|
||||
accesses: true
|
||||
accounts: true
|
||||
action_text_markdowns: true
|
||||
action_text_rich_texts: true
|
||||
active_storage_attachments: true
|
||||
active_storage_blobs: true
|
||||
active_storage_variant_records: true
|
||||
@@ -1131,6 +1150,25 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
action_text_rich_texts:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: action_text_rich_texts
|
||||
name: index_action_text_rich_texts_uniqueness
|
||||
unique: true
|
||||
columns:
|
||||
- record_type
|
||||
- record_id
|
||||
- name
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
active_storage_attachments:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: active_storage_attachments
|
||||
@@ -2161,4 +2199,4 @@ indexes:
|
||||
comment:
|
||||
valid: true
|
||||
workflows: []
|
||||
version: 20250515125505
|
||||
version: 20250523084734
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
require "rouge/plugins/redcarpet"
|
||||
|
||||
class MarkdownRenderer < Redcarpet::Render::HTML
|
||||
include Rouge::Plugins::Redcarpet
|
||||
|
||||
def self.build
|
||||
Redcarpet::Markdown.new MarkdownRenderer.new(filter_html: false),
|
||||
autolink: true, highlight: true, no_intra_emphasis: true,
|
||||
fenced_code_blocks: true, lax_spacing: true, strikethrough: true, tables: true
|
||||
end
|
||||
|
||||
def initialize(...)
|
||||
super
|
||||
@id_counts = Hash.new 0
|
||||
end
|
||||
|
||||
def header(text, header_level)
|
||||
unique_id(text).then do |id|
|
||||
<<~HTML.chomp
|
||||
<h#{header_level} id="#{id}">
|
||||
#{text} <a href="##{id}" class="heading__link" aria-hidden="true">#</a>
|
||||
</h#{header_level}>
|
||||
HTML
|
||||
end
|
||||
end
|
||||
|
||||
def image(url, title, alt_text)
|
||||
<<~HTML.chomp
|
||||
<a title="#{title}" href="#{url}" data-action="lightbox#open:prevent" data-lightbox-target="image" data-lightbox-url-value="#{url}?disposition=attachment">
|
||||
<img src="#{url}" alt="#{alt_text}">
|
||||
</a>
|
||||
HTML
|
||||
end
|
||||
|
||||
def code_block(code, language)
|
||||
block_code(code, language) # call Rouge Redcarpet plugin
|
||||
end
|
||||
|
||||
def link(url, title, content)
|
||||
return video_tag(url, title) if video_url?(url)
|
||||
|
||||
attributes = { href: url }
|
||||
attributes[:title] = title if title
|
||||
attributes["data-turbo-frame"] = "_top"
|
||||
|
||||
"<a #{html_attributes(attributes)}>#{content}</a>"
|
||||
end
|
||||
|
||||
def autolink(url, link_type)
|
||||
attributes = { href: url }
|
||||
attributes["data-turbo-frame"] = "_top"
|
||||
|
||||
"<a #{html_attributes(attributes)}>#{url}</a>"
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :id_counts
|
||||
|
||||
def html_attributes(attributes)
|
||||
attributes.map { |key, value| %Q(#{key}="#{value}") }.join(" ")
|
||||
end
|
||||
|
||||
def unique_id(text)
|
||||
text.parameterize.then do |base_id|
|
||||
id_counts[base_id] += 1
|
||||
id_counts[base_id] > 1 ? "#{base_id}-#{id_counts[base_id]}" : base_id
|
||||
end
|
||||
end
|
||||
|
||||
def video_url?(url)
|
||||
File.extname(url).downcase.match?(/\.(mp4|webm|ogg|mov|avi|mkv)/)
|
||||
end
|
||||
|
||||
def video_tag(url, title)
|
||||
<<~HTML.chomp
|
||||
<video controls title="#{title}" width="1024">
|
||||
<source src="#{url}" type="video/#{File.extname(url).delete('.')}">
|
||||
</video>
|
||||
HTML
|
||||
end
|
||||
end
|
||||
@@ -1,47 +0,0 @@
|
||||
module ActionText
|
||||
module HasMarkdown
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def has_markdown(name, strict_loading: strict_loading_by_default)
|
||||
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
||||
def #{name}
|
||||
markdown_#{name} || build_markdown_#{name}
|
||||
end
|
||||
|
||||
def #{name}_html
|
||||
#{name}.to_html
|
||||
end
|
||||
|
||||
def #{name}_plain_text
|
||||
#{name}.to_plain_text
|
||||
end
|
||||
|
||||
def #{name}?
|
||||
markdown_#{name}.present?
|
||||
end
|
||||
|
||||
def #{name}=(content)
|
||||
self.#{name}.content = content
|
||||
end
|
||||
CODE
|
||||
|
||||
has_one :"markdown_#{name}", -> { where(name: name) },
|
||||
class_name: "ActionText::Markdown", as: :record, inverse_of: :record, autosave: true, dependent: :destroy,
|
||||
strict_loading: strict_loading
|
||||
|
||||
scope :"with_markdown_#{name}", -> { includes("markdown_#{name}") }
|
||||
end
|
||||
end
|
||||
|
||||
def safe_markdown_attribute(name)
|
||||
if self.class.reflect_on_association("markdown_#{name}")&.klass == ActionText::Markdown
|
||||
public_send(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ActiveSupport.on_load :active_record do
|
||||
include ActionText::HasMarkdown
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
module ActionText
|
||||
class Markdown < Record
|
||||
mattr_accessor :html_renderer
|
||||
mattr_accessor :plain_text_renderer
|
||||
|
||||
belongs_to :record, polymorphic: true, touch: true
|
||||
|
||||
def to_html
|
||||
to_unsafe_html.html_safe
|
||||
end
|
||||
|
||||
def to_unsafe_html
|
||||
if html_renderer.respond_to? :call
|
||||
html_renderer.call content
|
||||
else
|
||||
html_renderer.render content
|
||||
end
|
||||
end
|
||||
|
||||
def to_plain_text
|
||||
if plain_text_renderer.respond_to? :call
|
||||
plain_text_renderer.call content
|
||||
else
|
||||
plain_text_renderer.render content
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ActionText::Markdown::Uploads
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many_attached :uploads, dependent: :destroy
|
||||
end
|
||||
end
|
||||
|
||||
ActiveSupport.on_load :active_storage_attachment do
|
||||
class ActionText::Markdown
|
||||
include ActionText::Markdown::Uploads
|
||||
end
|
||||
end
|
||||
|
||||
ActiveSupport.run_load_hooks :action_text_markdown, ActionText::Markdown
|
||||
@@ -1,36 +0,0 @@
|
||||
module ActionText
|
||||
module TagHelper
|
||||
def markdown_area(record, name, value: nil, data: {}, **options)
|
||||
field_name = "#{record.class.model_name.param_key}[#{name}]"
|
||||
value = record.safe_markdown_attribute(name).content.to_s if value.nil?
|
||||
data = data.reverse_merge! uploads_url: uploads_url(format: "json")
|
||||
|
||||
tag.house_md value, name: field_name, data: data, **options
|
||||
end
|
||||
|
||||
def house_toolbar(**options, &block)
|
||||
tag.house_md_toolbar(**options, &block)
|
||||
end
|
||||
|
||||
def house_toolbar_button(action, **options, &block)
|
||||
tag.button title: action.to_s.humanize, data: { "house-md-action": action }, **options, &block
|
||||
end
|
||||
|
||||
def house_toolbar_file_upload_button(name: "upload", title: "Upload File", **options, &block)
|
||||
tag.label title: title, **options do
|
||||
safe_join [
|
||||
file_field_tag(name, data: { "house-md-toolbar-file-picker": true }, style: "display: none;"),
|
||||
capture(&block)
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ActionView::Helpers
|
||||
class FormBuilder
|
||||
def markdown_area(method, **options)
|
||||
@template.markdown_area(@object, method, **options)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
module ActiveStorage::Sluggable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_create :set_slug
|
||||
end
|
||||
|
||||
def slug_url(host: ActiveStorage::Current.url_options[:host], port: ActiveStorage::Current.url_options[:port])
|
||||
Rails.application.routes.url_helpers.upload_url(slug, host: host, port: port)
|
||||
end
|
||||
|
||||
private
|
||||
def set_slug
|
||||
self.slug = "#{slug_basename}-#{SecureRandom.alphanumeric(6)}.#{slug_extension}"
|
||||
end
|
||||
|
||||
def slug_basename
|
||||
File.basename(slug_filename, ".*").parameterize
|
||||
end
|
||||
|
||||
def slug_extension
|
||||
File.extname(slug_filename).delete(".").parameterize
|
||||
end
|
||||
|
||||
def slug_filename
|
||||
slug.presence || filename.to_s
|
||||
end
|
||||
end
|
||||
|
||||
ActiveSupport.on_load :active_storage_attachment do
|
||||
ActiveStorage::Attachment.include ActiveStorage::Sluggable
|
||||
end
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative "../config/environment"
|
||||
require "redcarpet"
|
||||
require "nokogiri"
|
||||
|
||||
class ActionText::Markdown < ApplicationRecord
|
||||
belongs_to :record, polymorphic: true
|
||||
end
|
||||
|
||||
class MarkdownToActionTextConverter
|
||||
ATTACHMENT_URL_REGEX = %r{/u/(?<slug>[^\/\s\)]+)}
|
||||
|
||||
def initialize(html)
|
||||
@doc = Nokogiri::HTML::DocumentFragment.parse(html)
|
||||
@attachments = []
|
||||
end
|
||||
|
||||
def convert
|
||||
process_images
|
||||
process_links
|
||||
[ @doc.to_html, @attachments ]
|
||||
end
|
||||
|
||||
private
|
||||
def process_images
|
||||
@doc.css("img").each do |img|
|
||||
src = img["src"].presence
|
||||
if src && match = src.match(ATTACHMENT_URL_REGEX)
|
||||
if (attachment = find_attachment(match[:slug]))
|
||||
img.replace(build_attachment_node(attachment))
|
||||
@attachments << attachment
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def process_links
|
||||
@doc.css("a").each do |link|
|
||||
href = link["href"].presence
|
||||
|
||||
if href && match = href.match(ATTACHMENT_URL_REGEX)
|
||||
if (attachment = find_attachment(match[:slug]))
|
||||
link.replace(build_attachment_node(attachment))
|
||||
@attachments << attachment
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_attachment_node(attachment)
|
||||
html = ActionText::Attachment.from_attachable(attachment).to_html
|
||||
fragment = Nokogiri::HTML::DocumentFragment.parse(html)
|
||||
|
||||
node = fragment.at_css("action-text-attachment")
|
||||
node["url"] = Rails.application.routes.url_helpers.rails_blob_path(attachment.blob, only_path: true)
|
||||
|
||||
fragment
|
||||
end
|
||||
|
||||
def find_attachment(slug)
|
||||
ActiveStorage::Attachment.find_by(slug: slug)
|
||||
end
|
||||
end
|
||||
|
||||
class RedcarpetRenderer
|
||||
def self.render(markdown)
|
||||
renderer = Redcarpet::Render::HTML.new
|
||||
markdowner = Redcarpet::Markdown.new(renderer,
|
||||
autolink: true,
|
||||
tables: true,
|
||||
fenced_code_blocks: true,
|
||||
strikethrough: true,
|
||||
superscript: true,
|
||||
)
|
||||
markdowner.render(markdown.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
def process_all(klass, field)
|
||||
klass.find_each do |record|
|
||||
markdown = ActionText::Markdown.find_by(record: record, name: field)
|
||||
next unless markdown
|
||||
|
||||
puts "markdown.id=#{markdown.id}"
|
||||
next unless markdown.record
|
||||
|
||||
html = RedcarpetRenderer.render(markdown.content.to_s)
|
||||
converter = MarkdownToActionTextConverter.new(html)
|
||||
rich_text_html, attachments = converter.convert
|
||||
|
||||
rich_text = ActionText::RichText.create!(
|
||||
name: markdown.name,
|
||||
record: markdown.record,
|
||||
body: rich_text_html
|
||||
)
|
||||
|
||||
attachments.each do |attachment|
|
||||
attachment.update!(record: rich_text)
|
||||
end
|
||||
|
||||
puts "✓ Created rich text for #{markdown.record_type}##{markdown.record_id} (#{markdown.name})"
|
||||
rescue => e
|
||||
warn "✗ Failed to process markdown ##{markdown.id}: #{e.class} - #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
puts "Processing tenant: #{tenant}"
|
||||
|
||||
ActionText::RichText.delete_all
|
||||
|
||||
process_all(Card, :description)
|
||||
process_all(Comment, :body)
|
||||
end
|
||||
@@ -214,9 +214,6 @@ class EnactTenanting
|
||||
assert_count Tagging, Tagging.count
|
||||
assert_count Watch, Watch.count
|
||||
|
||||
# we're only copying the markdown records that are still accessible.
|
||||
assert_count ActionText::Markdown, Comment.count
|
||||
|
||||
assert_count Event, Event.count
|
||||
assert_count Notification, Notification.count
|
||||
|
||||
@@ -228,22 +225,7 @@ class EnactTenanting
|
||||
end
|
||||
|
||||
def update_action_text_urls
|
||||
update_counter["ActionText::Markdown"] = 0
|
||||
TENANTS.each do |tenant|
|
||||
tenanted_domain = sprintf(TENANTED_DOMAIN, tenant: tenant)
|
||||
|
||||
ApplicationRecord.with_tenant(tenant) do
|
||||
ActionText::Markdown.all.each do |markdown|
|
||||
content = markdown.content
|
||||
next unless content =~ %r{#{CURRENT_DOMAIN}/u/}
|
||||
|
||||
content.gsub!(%r{#{CURRENT_DOMAIN}/u/}, "#{tenanted_domain}/u/")
|
||||
|
||||
markdown.update_column :content, content
|
||||
update_counter["ActionText::Markdown"] += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
raise "No more markdown"
|
||||
end
|
||||
|
||||
def hardlink_active_storage
|
||||
|
||||
@@ -100,9 +100,9 @@ ApplicationRecord.with_each_tenant do |tenant|
|
||||
end
|
||||
|
||||
# Update action_text_markdowns table (polymorphic relationship)
|
||||
ActionText::Markdown.where(record_type: "Card").find_each do |markdown|
|
||||
if id_mapping[markdown.record_id]
|
||||
markdown.update_column(:record_id, id_mapping[markdown.record_id])
|
||||
ActionText::RichText.where(record_type: "Card").find_each do |rich_text|
|
||||
if id_mapping[rich_text.record_id]
|
||||
rich_text.update_column(:record_id, id_mapping[rich_text.record_id])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -73,9 +73,9 @@ ApplicationRecord.with_each_tenant do |tenant|
|
||||
end
|
||||
|
||||
# Update action_text_markdowns table (polymorphic relationship)
|
||||
ActionText::Markdown.where(record_type: "Collection").find_each do |markdown|
|
||||
if id_mapping[markdown.record_id]
|
||||
markdown.update_column(:record_id, id_mapping[markdown.record_id])
|
||||
ActionText::RichText.where(record_type: "Collection").find_each do |rich_text|
|
||||
if id_mapping[rich_text.record_id]
|
||||
rich_text.update_column(:record_id, id_mapping[rich_text.record_id])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ class Cards::CommentsControllerTest < ActionDispatch::IntegrationTest
|
||||
put card_comment_path(cards(:logo), comments(:logo_agreement_kevin)), params: { comment: { body: "I've changed my mind" } }
|
||||
|
||||
assert_response :success
|
||||
assert_equal "I've changed my mind", comments(:logo_agreement_kevin).reload.body.content
|
||||
assert_action_text "I've changed my mind", comments(:logo_agreement_kevin).reload.body
|
||||
end
|
||||
|
||||
test "update another user's comment" do
|
||||
assert_no_changes "comments(:logo_agreement_jz).body.content" do
|
||||
assert_no_changes -> { comments(:logo_agreement_jz).reload.body.to_s } do
|
||||
put card_comment_path(cards(:logo), comments(:logo_agreement_jz)), params: { comment: { body: "I've changed my mind" } }
|
||||
end
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_equal "moon.jpg", card.image.filename.to_s
|
||||
assert_equal [ tags(:mobile) ], card.tags
|
||||
|
||||
assert_equal "Something more in-depth", card.description_plain_text.strip
|
||||
assert_equal "Something more in-depth", card.description.to_plain_text.strip
|
||||
end
|
||||
|
||||
test "users can only see cards in collections they have access to" do
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class UploadsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "create" do
|
||||
assert_changes -> { ActiveStorage::Attachment.count }, 1 do
|
||||
post uploads_path(format: "json"), params: { file: fixture_file_upload("moon.jpg", "image/jpeg") }, as: :xhr
|
||||
end
|
||||
|
||||
assert_response :success
|
||||
assert_equal ActiveStorage::Attachment.last.slug_url(host: "#{ApplicationRecord.current_tenant}.example.com", port: nil), response.parsed_body["fileUrl"]
|
||||
assert_equal "image/jpeg", response.parsed_body["mimetype"]
|
||||
assert_equal "moon.jpg", response.parsed_body["fileName"]
|
||||
end
|
||||
|
||||
test "show" do
|
||||
accounts("37s").uploads.attach fixture_file_upload("moon.jpg", "image/jpeg")
|
||||
get upload_path(slug: accounts("37s").uploads.last.slug)
|
||||
assert_response :redirect
|
||||
assert_match /\/rails\/active_storage\/.*\/moon\.jpg/, @response.redirect_url
|
||||
end
|
||||
end
|
||||
+5
-3
@@ -1,14 +1,16 @@
|
||||
logo_agreement_jz:
|
||||
record: logo_agreement_jz (Comment)
|
||||
name: body
|
||||
content: I agree.
|
||||
body: I agree.
|
||||
|
||||
logo_agreement_kevin:
|
||||
record: logo_agreement_kevin (Comment)
|
||||
name: body
|
||||
content: Same, let’s do it.
|
||||
body: Same, let’s do it.
|
||||
|
||||
layout_overflowing_david:
|
||||
record: layout_overflowing_david (Comment)
|
||||
name: body
|
||||
content: The text is overflowing the container.
|
||||
body: The text is overflowing the container.
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ module ActiveSupport
|
||||
fixtures :all
|
||||
|
||||
include ActiveJob::TestHelper
|
||||
include CardTestHelper, ChangeTestHelper, SessionTestHelper
|
||||
include ActionTextTestHelper, CardTestHelper, ChangeTestHelper, SessionTestHelper
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
module ActionTextTestHelper
|
||||
def assert_action_text(expected_html, content)
|
||||
assert_equal_html <<~HTML, content.to_s
|
||||
<div class="action-text-content">#{expected_html}</action-text-content>
|
||||
HTML
|
||||
end
|
||||
|
||||
def assert_equal_html(expected, actual)
|
||||
assert_equal normalize_html(expected), normalize_html(actual)
|
||||
end
|
||||
|
||||
def normalize_html(html)
|
||||
Nokogiri::HTML.fragment(html).tap do |fragment|
|
||||
fragment.traverse do |node|
|
||||
if node.text?
|
||||
node.content = node.text.squish
|
||||
end
|
||||
end
|
||||
end.to_html.strip
|
||||
end
|
||||
end
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user