Revert "Revert "Replace House with Lexical""
This commit is contained in:
+1
-1
@@ -47,7 +47,7 @@ FROM base
|
|||||||
|
|
||||||
# Install packages needed for deployment
|
# Install packages needed for deployment
|
||||||
RUN apt-get update -qq && \
|
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
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
# Copy built artifacts: gems, application
|
# Copy built artifacts: gems, application
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ gem "rqrcode"
|
|||||||
gem "redcarpet"
|
gem "redcarpet"
|
||||||
gem "rouge"
|
gem "rouge"
|
||||||
gem "jbuilder"
|
gem "jbuilder"
|
||||||
|
gem "actiontext-lexical", bc: "actiontext-lexical"
|
||||||
|
gem "image_processing", "~> 1.14"
|
||||||
gem "platform_agent"
|
gem "platform_agent"
|
||||||
|
|
||||||
# Telemetry and logging
|
# Telemetry and logging
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
GIT
|
||||||
|
remote: https://github.com/basecamp/actiontext-lexical
|
||||||
|
revision: 777b2a4a933db9cb4cd515c4569dfcaf8cae45b5
|
||||||
|
specs:
|
||||||
|
actiontext-lexical (0.1.0)
|
||||||
|
rails (>= 8.0.2)
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/basecamp/active_record-tenanted
|
remote: https://github.com/basecamp/active_record-tenanted
|
||||||
revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b
|
revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b
|
||||||
@@ -188,6 +195,10 @@ GEM
|
|||||||
net-http (>= 0.5.0)
|
net-http (>= 0.5.0)
|
||||||
faraday-retry (2.3.1)
|
faraday-retry (2.3.1)
|
||||||
faraday (~> 2.0)
|
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)
|
fugit (1.11.1)
|
||||||
et-orbi (~> 1, >= 1.2.11)
|
et-orbi (~> 1, >= 1.2.11)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
@@ -199,6 +210,9 @@ GEM
|
|||||||
hashdiff (1.1.2)
|
hashdiff (1.1.2)
|
||||||
i18n (1.14.7)
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
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)
|
importmap-rails (2.1.0)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
activesupport (>= 6.0.0)
|
activesupport (>= 6.0.0)
|
||||||
@@ -236,6 +250,9 @@ GEM
|
|||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.4)
|
marcel (1.0.4)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
|
mini_magick (5.2.0)
|
||||||
|
benchmark
|
||||||
|
logger
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.9)
|
mini_portile2 (2.8.9)
|
||||||
minitest (5.25.5)
|
minitest (5.25.5)
|
||||||
@@ -354,6 +371,9 @@ GEM
|
|||||||
rubocop-performance (>= 1.24)
|
rubocop-performance (>= 1.24)
|
||||||
rubocop-rails (>= 2.30)
|
rubocop-rails (>= 2.30)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
|
ruby-vips (2.2.3)
|
||||||
|
ffi (~> 1.12)
|
||||||
|
logger
|
||||||
rubyzip (2.4.1)
|
rubyzip (2.4.1)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.32.0)
|
selenium-webdriver (4.32.0)
|
||||||
@@ -448,6 +468,7 @@ PLATFORMS
|
|||||||
x86_64-mingw32
|
x86_64-mingw32
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
actiontext-lexical!
|
||||||
active_record-tenanted!
|
active_record-tenanted!
|
||||||
bcrypt (~> 3.1.7)
|
bcrypt (~> 3.1.7)
|
||||||
bootsnap
|
bootsnap
|
||||||
@@ -456,6 +477,7 @@ DEPENDENCIES
|
|||||||
capybara
|
capybara
|
||||||
debug
|
debug
|
||||||
geared_pagination (~> 1.2)
|
geared_pagination (~> 1.2)
|
||||||
|
image_processing (~> 1.14)
|
||||||
importmap-rails
|
importmap-rails
|
||||||
jbuilder
|
jbuilder
|
||||||
kamal
|
kamal
|
||||||
|
|||||||
@@ -177,6 +177,17 @@
|
|||||||
--color-card-7: oklch(var(--lch-purple-medium));
|
--color-card-7: oklch(var(--lch-purple-medium));
|
||||||
--color-card-8: oklch(var(--lch-pink-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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
--lch-canvas: 20% 0.0195 232.58;
|
--lch-canvas: 20% 0.0195 232.58;
|
||||||
--lch-ink-inverted: var(--lch-black);
|
--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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
.card__content {
|
||||||
flex: 2 1 auto;
|
flex: 2 1 auto;
|
||||||
max-inline-size: 100%;
|
max-inline-size: 100%;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@layer components {
|
@layer components {
|
||||||
.comments {
|
.comments {
|
||||||
--avatar-size: 2.33em;
|
--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-padding-inline: var(--inline-space-double);
|
||||||
--comment-max: 70ch;
|
--comment-max: 70ch;
|
||||||
--reaction-size: 2rem;
|
--reaction-size: 2rem;
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.comment__avatar {
|
.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;
|
z-index: 0;
|
||||||
|
|
||||||
.comment--mine_ & {
|
.comment--mine_ & {
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
* {
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
}
|
}
|
||||||
@@ -83,6 +83,8 @@
|
|||||||
border-radius: 0.2em;
|
border-radius: 0.2em;
|
||||||
padding:
|
padding:
|
||||||
var(--comment-padding-block)
|
var(--comment-padding-block)
|
||||||
|
var(--comment-padding-inline)
|
||||||
|
calc(var(--comment-padding-block) * 1.5)
|
||||||
var(--comment-padding-inline);
|
var(--comment-padding-inline);
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
@@ -113,7 +115,7 @@
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
max-inline-size: var(--comment-max);
|
max-inline-size: var(--comment-max);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
/* Make up space for lack of avatar */
|
/* Make up space for lack of avatar */
|
||||||
@@ -133,17 +135,4 @@
|
|||||||
text-align: center !important;
|
text-align: center !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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
def comment_notification_body(event)
|
||||||
comment = event.eventable
|
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
|
end
|
||||||
|
|
||||||
def card_notification_title(card)
|
def card_notification_title(card)
|
||||||
|
|||||||
@@ -2,4 +2,6 @@
|
|||||||
import "@hotwired/turbo-rails"
|
import "@hotwired/turbo-rails"
|
||||||
import "initializers"
|
import "initializers"
|
||||||
import "controllers"
|
import "controllers"
|
||||||
import "house"
|
|
||||||
|
import "actiontext-lexical"
|
||||||
|
import "@rails/actiontext"
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ export default class extends Controller {
|
|||||||
|
|
||||||
async restoreContent() {
|
async restoreContent() {
|
||||||
await nextFrame()
|
await nextFrame()
|
||||||
const savedContent = localStorage.getItem(this.keyValue)
|
let savedContent = localStorage.getItem(this.keyValue)
|
||||||
|
|
||||||
if (savedContent) {
|
if (savedContent) {
|
||||||
|
savedContent = `<div>${savedContent}</div>` // temporary for old markdown saves
|
||||||
this.inputTarget.value = savedContent
|
this.inputTarget.value = savedContent
|
||||||
this.#triggerChangeEvent(savedContent)
|
this.#triggerChangeEvent(savedContent)
|
||||||
}
|
}
|
||||||
@@ -45,8 +46,8 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#triggerChangeEvent(newValue) {
|
#triggerChangeEvent(newValue) {
|
||||||
if (this.inputTarget.tagName === "HOUSE-MD") {
|
if (this.inputTarget.tagName === "LEXICAL-EDITOR") {
|
||||||
this.inputTarget.dispatchEvent(new CustomEvent('house-md:change', {
|
this.inputTarget.dispatchEvent(new CustomEvent('actiontext:change', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: {
|
detail: {
|
||||||
previousContent: '',
|
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_many :comments, dependent: :destroy
|
||||||
has_one_attached :image, dependent: :purge_later
|
has_one_attached :image, dependent: :purge_later
|
||||||
|
|
||||||
has_markdown :description
|
has_rich_text :description
|
||||||
|
|
||||||
before_save :set_default_title, if: :published?
|
before_save :set_default_title, if: :published?
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class Comment < ApplicationRecord
|
|||||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||||
has_many :reactions, dependent: :delete_all
|
has_many :reactions, dependent: :delete_all
|
||||||
|
|
||||||
has_markdown :body
|
has_rich_text :body
|
||||||
searchable_by :body_plain_text, using: :comments_search_index, as: :body
|
searchable_by :body_plain_text, using: :comments_search_index, as: :body
|
||||||
|
|
||||||
scope :chronologically, -> { order created_at: :asc, id: :desc }
|
scope :chronologically, -> { order created_at: :asc, id: :desc }
|
||||||
@@ -19,6 +19,10 @@ class Comment < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def body_plain_text
|
||||||
|
body.to_plain_text
|
||||||
|
end
|
||||||
|
|
||||||
def watch_card_by_creator
|
def watch_card_by_creator
|
||||||
card.watch_by creator
|
card.watch_by creator
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ module Mentions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def mentionable_content
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -32,12 +32,12 @@ module Mentions
|
|||||||
collection.users
|
collection.users
|
||||||
end
|
end
|
||||||
|
|
||||||
def markdown_associations
|
def rich_text_associations
|
||||||
self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::Markdown }
|
self.class.reflect_on_all_associations(:has_one).filter { it.klass == ActionText::RichText }
|
||||||
end
|
end
|
||||||
|
|
||||||
def mentionable_content_changed?
|
def mentionable_content_changed?
|
||||||
markdown_associations.any? { send(it.name).content_previously_changed? }
|
rich_text_associations.any? { send(it.name).body_previously_changed? }
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_mentions_later
|
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 %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="comment__body markdown-content txt-align-start">
|
<div class="comment__body rich-text-content txt-align-start" data-controller="syntax-highlight">
|
||||||
<%= sanitize comment.body_html %>
|
<%= comment.body %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= render "cards/comments/reactions/reactions", comment: comment %>
|
<%= render "cards/comments/reactions/reactions", comment: comment %>
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<div class="comment__content flex-inline flex-column full-width">
|
<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",
|
<%= 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}",
|
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| %>
|
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.markdown_area :body, class: "input comment__input", required: true, placeholder: new_comment_placeholder(card),
|
<%= form.rich_textarea :body, 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" } %>
|
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 %>
|
<%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %>
|
||||||
<span>Post this comment</span>
|
<span>Post this comment</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<div class="comment__content flex flex-column flex-item-grow full-width">
|
<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",
|
<%= 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| %>
|
data: { controller: "form", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %>
|
||||||
<%= form.markdown_area :body, class: "input comment__input", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %>
|
<%= form.rich_textarea :body, required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %>
|
||||||
<div class="flex gap-half justify-start">
|
<div class="flex gap-half justify-start">
|
||||||
<%= form.button class: "btn btn--reversed", type: :submit do %>
|
<%= form.button class: "btn btn--reversed", type: :submit do %>
|
||||||
<span>Save changes</span>
|
<span>Save changes</span>
|
||||||
|
|||||||
@@ -4,23 +4,23 @@
|
|||||||
<h1 class="card__title flex align-start gap-half">
|
<h1 class="card__title flex align-start gap-half">
|
||||||
<%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>
|
<%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="card__description markdown-content margin-block-half">
|
<div class="card__description rich-text-content margin-block-half rich-text-content" data-controller="syntax-highlight">
|
||||||
<%= sanitize card.description_html %>
|
<%= card.description %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% 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| %>
|
<%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "auto-save" } do |form| %>
|
||||||
<h1 class="card__title">
|
<h1 class="card__title">
|
||||||
<%= form.text_area :title, placeholder: "Name it…",
|
<%= form.text_area :title, placeholder: "Name it…",
|
||||||
class: "input input--textarea full-width borderless txt-align-start",
|
class: "input input--textarea full-width borderless txt-align-start",
|
||||||
autofocus: card.title.blank?,
|
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" } %>
|
data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="card__description markdown-content margin-block-start-half margin-block-end">
|
<div class="card__description rich-text-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",
|
<%= form.rich_textarea :description, class: "card-field__description",
|
||||||
placeholder: "Add some notes, context, pictures, or video about this…",
|
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>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<%= turbo_frame_tag @card, :edit do %>
|
<%= 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: "form" } do |form| %>
|
||||||
<h1 class="card__title">
|
<h1 class="card__title">
|
||||||
<%= form.label :title, class: "flex flex-column align-center" do %>
|
<%= 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",
|
<%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title",
|
||||||
@@ -8,10 +8,10 @@
|
|||||||
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" } %>
|
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" } %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</h1>
|
</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",
|
<%= form.rich_textarea :description, class: "card-field__description rich-text-content",
|
||||||
placeholder: "Add some notes, context, pictures, or video about this…",
|
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" } %>
|
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
|
||||||
</div>
|
</div>
|
||||||
<%= form.button "Save changes", type: :submit, class: "btn" %>
|
<%= 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 %>
|
<%= 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 %>
|
<%= render "events/event/layout", card: event.eventable.card, event: event do %>
|
||||||
<span class="txt-break overflow-line-clamp txt-tight-lines">
|
<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>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="action-text-content">
|
||||||
|
<%= yield -%>
|
||||||
|
</div>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<%= page_title_tag %>
|
<%= 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="view-transition" content="same-origin">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
<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", to: "stimulus.min.js"
|
||||||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
||||||
pin "@rails/request.js", to: "@rails--request.js" # @0.0.11
|
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/controllers", under: "controllers"
|
||||||
pin_all_from "app/javascript/helpers", under: "helpers"
|
pin_all_from "app/javascript/helpers", under: "helpers"
|
||||||
pin_all_from "app/javascript/initializers", under: "initializers"
|
pin_all_from "app/javascript/initializers", under: "initializers"
|
||||||
pin "marked" # @15.0.11
|
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.
|
# 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|
|
create_table "accesses", force: :cascade do |t|
|
||||||
t.integer "collection_id", null: false
|
t.integer "collection_id", null: false
|
||||||
t.datetime "created_at", 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"
|
t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record"
|
||||||
end
|
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|
|
create_table "active_storage_attachments", force: :cascade do |t|
|
||||||
t.bigint "blob_id", null: false
|
t.bigint "blob_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
|||||||
+116
-78
@@ -1,7 +1,7 @@
|
|||||||
--- !ruby/object:ActiveRecord::ConnectionAdapters::SchemaCache
|
--- !ruby/object:ActiveRecord::ConnectionAdapters::SchemaCache
|
||||||
columns:
|
columns:
|
||||||
accesses:
|
accesses:
|
||||||
- &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: collection_id
|
name: collection_id
|
||||||
cast_type: &1 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
|
cast_type: &1 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
|
||||||
@@ -80,7 +80,7 @@ columns:
|
|||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &25 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: user_id
|
name: user_id
|
||||||
cast_type: *1
|
cast_type: *1
|
||||||
@@ -118,13 +118,13 @@ columns:
|
|||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: content
|
name: content
|
||||||
cast_type: &14 !ruby/object:ActiveRecord::Type::Text
|
cast_type: &11 !ruby/object:ActiveRecord::Type::Text
|
||||||
true: t
|
true: t
|
||||||
false: f
|
false: f
|
||||||
precision:
|
precision:
|
||||||
scale:
|
scale:
|
||||||
limit:
|
limit:
|
||||||
sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||||
sql_type: TEXT
|
sql_type: TEXT
|
||||||
type: :text
|
type: :text
|
||||||
limit:
|
limit:
|
||||||
@@ -159,17 +159,30 @@ columns:
|
|||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- *9
|
- *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
|
- &16 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: blob_id
|
name: record_id
|
||||||
cast_type: &11 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
|
cast_type: &14 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
|
||||||
precision:
|
precision:
|
||||||
scale:
|
scale:
|
||||||
limit:
|
limit:
|
||||||
max: 9223372036854775808
|
max: 9223372036854775808
|
||||||
min: -9223372036854775808
|
min: -9223372036854775808
|
||||||
sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
sql_type_metadata: &15 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||||
sql_type: bigint
|
sql_type: bigint
|
||||||
type: :integer
|
type: :integer
|
||||||
limit:
|
limit:
|
||||||
@@ -180,19 +193,23 @@ columns:
|
|||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- *5
|
- *13
|
||||||
- *6
|
- *9
|
||||||
- *10
|
active_storage_attachments:
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: record_id
|
name: blob_id
|
||||||
cast_type: *11
|
cast_type: *14
|
||||||
sql_type_metadata: *12
|
sql_type_metadata: *15
|
||||||
'null': false
|
'null': false
|
||||||
default:
|
default:
|
||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
|
- *5
|
||||||
|
- *6
|
||||||
|
- *10
|
||||||
|
- *16
|
||||||
- *13
|
- *13
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
@@ -208,8 +225,8 @@ columns:
|
|||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: byte_size
|
name: byte_size
|
||||||
cast_type: *11
|
cast_type: *14
|
||||||
sql_type_metadata: *12
|
sql_type_metadata: *15
|
||||||
'null': false
|
'null': false
|
||||||
default:
|
default:
|
||||||
default_function:
|
default_function:
|
||||||
@@ -247,7 +264,7 @@ columns:
|
|||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- *6
|
- *6
|
||||||
- &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: key
|
name: key
|
||||||
cast_type: *7
|
cast_type: *7
|
||||||
@@ -260,8 +277,8 @@ columns:
|
|||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: metadata
|
name: metadata
|
||||||
cast_type: *14
|
cast_type: *11
|
||||||
sql_type_metadata: *15
|
sql_type_metadata: *12
|
||||||
'null': true
|
'null': true
|
||||||
default:
|
default:
|
||||||
default_function:
|
default_function:
|
||||||
@@ -278,7 +295,7 @@ columns:
|
|||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
active_storage_variant_records:
|
active_storage_variant_records:
|
||||||
- *16
|
- *17
|
||||||
- *6
|
- *6
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
@@ -292,7 +309,7 @@ columns:
|
|||||||
comment:
|
comment:
|
||||||
ar_internal_metadata:
|
ar_internal_metadata:
|
||||||
- *5
|
- *5
|
||||||
- *17
|
- *18
|
||||||
- *9
|
- *9
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
@@ -305,7 +322,7 @@ columns:
|
|||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
assignees_filters:
|
assignees_filters:
|
||||||
- &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: assignee_id
|
name: assignee_id
|
||||||
cast_type: *1
|
cast_type: *1
|
||||||
@@ -315,7 +332,7 @@ columns:
|
|||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: filter_id
|
name: filter_id
|
||||||
cast_type: *1
|
cast_type: *1
|
||||||
@@ -326,7 +343,7 @@ columns:
|
|||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
assigners_filters:
|
assigners_filters:
|
||||||
- &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: assigner_id
|
name: assigner_id
|
||||||
cast_type: *1
|
cast_type: *1
|
||||||
@@ -336,11 +353,11 @@ columns:
|
|||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- *18
|
|
||||||
assignments:
|
|
||||||
- *19
|
- *19
|
||||||
|
assignments:
|
||||||
- *20
|
- *20
|
||||||
- &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- *21
|
||||||
|
- &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: card_id
|
name: card_id
|
||||||
cast_type: *1
|
cast_type: *1
|
||||||
@@ -368,14 +385,14 @@ columns:
|
|||||||
- *6
|
- *6
|
||||||
- *9
|
- *9
|
||||||
card_goldnesses:
|
card_goldnesses:
|
||||||
- *21
|
- *22
|
||||||
- *5
|
- *5
|
||||||
- *6
|
- *6
|
||||||
- *9
|
- *9
|
||||||
cards:
|
cards:
|
||||||
- *22
|
- *23
|
||||||
- *5
|
- *5
|
||||||
- &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: creator_id
|
name: creator_id
|
||||||
cast_type: *1
|
cast_type: *1
|
||||||
@@ -428,14 +445,14 @@ columns:
|
|||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: status
|
name: status
|
||||||
cast_type: *14
|
cast_type: *11
|
||||||
sql_type_metadata: *15
|
sql_type_metadata: *12
|
||||||
'null': false
|
'null': false
|
||||||
default: creating
|
default: creating
|
||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: title
|
name: title
|
||||||
cast_type: *7
|
cast_type: *7
|
||||||
@@ -461,7 +478,7 @@ columns:
|
|||||||
comment:
|
comment:
|
||||||
- *9
|
- *9
|
||||||
closures:
|
closures:
|
||||||
- *21
|
- *22
|
||||||
- *5
|
- *5
|
||||||
- *6
|
- *6
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
@@ -489,11 +506,11 @@ columns:
|
|||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: all_access
|
name: all_access
|
||||||
cast_type: &31 !ruby/object:ActiveModel::Type::Boolean
|
cast_type: &32 !ruby/object:ActiveModel::Type::Boolean
|
||||||
precision:
|
precision:
|
||||||
scale:
|
scale:
|
||||||
limit:
|
limit:
|
||||||
sql_type_metadata: &32 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
sql_type_metadata: &33 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||||
sql_type: boolean
|
sql_type: boolean
|
||||||
type: :boolean
|
type: :boolean
|
||||||
limit:
|
limit:
|
||||||
@@ -507,15 +524,15 @@ columns:
|
|||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: auto_close_period
|
name: auto_close_period
|
||||||
cast_type: *11
|
cast_type: *14
|
||||||
sql_type_metadata: *12
|
sql_type_metadata: *15
|
||||||
'null': true
|
'null': true
|
||||||
default:
|
default:
|
||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- *5
|
- *5
|
||||||
- *23
|
- *24
|
||||||
- *6
|
- *6
|
||||||
- *10
|
- *10
|
||||||
- *9
|
- *9
|
||||||
@@ -530,18 +547,18 @@ columns:
|
|||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
collections_filters:
|
collections_filters:
|
||||||
- *22
|
- *23
|
||||||
- *18
|
- *19
|
||||||
commands:
|
commands:
|
||||||
- *5
|
- *5
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: data
|
name: data
|
||||||
cast_type: &25 !ruby/object:ActiveRecord::Type::Json
|
cast_type: &26 !ruby/object:ActiveRecord::Type::Json
|
||||||
precision:
|
precision:
|
||||||
scale:
|
scale:
|
||||||
limit:
|
limit:
|
||||||
sql_type_metadata: &26 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||||
sql_type: json
|
sql_type: json
|
||||||
type: :json
|
type: :json
|
||||||
limit:
|
limit:
|
||||||
@@ -556,8 +573,8 @@ columns:
|
|||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: line
|
name: line
|
||||||
cast_type: *14
|
cast_type: *11
|
||||||
sql_type_metadata: *15
|
sql_type_metadata: *12
|
||||||
'null': true
|
'null': true
|
||||||
default:
|
default:
|
||||||
default_function:
|
default_function:
|
||||||
@@ -584,16 +601,16 @@ columns:
|
|||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- *9
|
- *9
|
||||||
- *24
|
- *25
|
||||||
comments:
|
comments:
|
||||||
- *21
|
- *22
|
||||||
- *5
|
- *5
|
||||||
- *23
|
- *24
|
||||||
- *6
|
- *6
|
||||||
- *9
|
- *9
|
||||||
creators_filters:
|
creators_filters:
|
||||||
- *23
|
- *24
|
||||||
- *18
|
- *19
|
||||||
events:
|
events:
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
@@ -605,9 +622,9 @@ columns:
|
|||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- *22
|
|
||||||
- *5
|
|
||||||
- *23
|
- *23
|
||||||
|
- *5
|
||||||
|
- *24
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: eventable_id
|
name: eventable_id
|
||||||
@@ -632,8 +649,8 @@ columns:
|
|||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: particulars
|
name: particulars
|
||||||
cast_type: *25
|
cast_type: *26
|
||||||
sql_type_metadata: *26
|
sql_type_metadata: *27
|
||||||
'null': true
|
'null': true
|
||||||
default: "{}"
|
default: "{}"
|
||||||
default_function:
|
default_function:
|
||||||
@@ -642,12 +659,12 @@ columns:
|
|||||||
- *9
|
- *9
|
||||||
filters:
|
filters:
|
||||||
- *5
|
- *5
|
||||||
- *23
|
- *24
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: fields
|
name: fields
|
||||||
cast_type: *25
|
cast_type: *26
|
||||||
sql_type_metadata: *26
|
sql_type_metadata: *27
|
||||||
'null': false
|
'null': false
|
||||||
default: "{}"
|
default: "{}"
|
||||||
default_function:
|
default_function:
|
||||||
@@ -666,7 +683,7 @@ columns:
|
|||||||
comment:
|
comment:
|
||||||
- *9
|
- *9
|
||||||
filters_stages:
|
filters_stages:
|
||||||
- *18
|
- *19
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: stage_id
|
name: stage_id
|
||||||
@@ -678,8 +695,8 @@ columns:
|
|||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
filters_tags:
|
filters_tags:
|
||||||
- *18
|
- *19
|
||||||
- &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: tag_id
|
name: tag_id
|
||||||
cast_type: *1
|
cast_type: *1
|
||||||
@@ -712,7 +729,7 @@ columns:
|
|||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- &27 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: source_id
|
name: source_id
|
||||||
cast_type: *1
|
cast_type: *1
|
||||||
@@ -722,7 +739,7 @@ columns:
|
|||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: source_type
|
name: source_type
|
||||||
cast_type: *7
|
cast_type: *7
|
||||||
@@ -756,16 +773,16 @@ columns:
|
|||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- *27
|
|
||||||
- *28
|
- *28
|
||||||
|
- *29
|
||||||
- *9
|
- *9
|
||||||
- *24
|
- *25
|
||||||
pins:
|
pins:
|
||||||
- *21
|
- *22
|
||||||
- *5
|
- *5
|
||||||
- *6
|
- *6
|
||||||
- *9
|
- *9
|
||||||
- *24
|
- *25
|
||||||
reactions:
|
reactions:
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
@@ -882,24 +899,24 @@ columns:
|
|||||||
default_function:
|
default_function:
|
||||||
collation:
|
collation:
|
||||||
comment:
|
comment:
|
||||||
- *24
|
- *25
|
||||||
taggings:
|
taggings:
|
||||||
- *21
|
- *22
|
||||||
- *5
|
- *5
|
||||||
- *6
|
- *6
|
||||||
- *29
|
- *30
|
||||||
- *9
|
- *9
|
||||||
tags:
|
tags:
|
||||||
- *5
|
- *5
|
||||||
- *6
|
- *6
|
||||||
- *30
|
- *31
|
||||||
- *9
|
- *9
|
||||||
users:
|
users:
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: active
|
name: active
|
||||||
cast_type: *31
|
cast_type: *32
|
||||||
sql_type_metadata: *32
|
sql_type_metadata: *33
|
||||||
'null': false
|
'null': false
|
||||||
default: true
|
default: true
|
||||||
default_function:
|
default_function:
|
||||||
@@ -940,16 +957,16 @@ columns:
|
|||||||
comment:
|
comment:
|
||||||
- *9
|
- *9
|
||||||
watches:
|
watches:
|
||||||
- *21
|
- *22
|
||||||
- *5
|
- *5
|
||||||
- *6
|
- *6
|
||||||
- *9
|
- *9
|
||||||
- *24
|
- *25
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||||
auto_increment:
|
auto_increment:
|
||||||
name: watching
|
name: watching
|
||||||
cast_type: *31
|
cast_type: *32
|
||||||
sql_type_metadata: *32
|
sql_type_metadata: *33
|
||||||
'null': false
|
'null': false
|
||||||
default: true
|
default: true
|
||||||
default_function:
|
default_function:
|
||||||
@@ -989,6 +1006,7 @@ primary_keys:
|
|||||||
accesses: id
|
accesses: id
|
||||||
accounts: id
|
accounts: id
|
||||||
action_text_markdowns: id
|
action_text_markdowns: id
|
||||||
|
action_text_rich_texts: id
|
||||||
active_storage_attachments: id
|
active_storage_attachments: id
|
||||||
active_storage_blobs: id
|
active_storage_blobs: id
|
||||||
active_storage_variant_records: id
|
active_storage_variant_records: id
|
||||||
@@ -1027,6 +1045,7 @@ data_sources:
|
|||||||
accesses: true
|
accesses: true
|
||||||
accounts: true
|
accounts: true
|
||||||
action_text_markdowns: true
|
action_text_markdowns: true
|
||||||
|
action_text_rich_texts: true
|
||||||
active_storage_attachments: true
|
active_storage_attachments: true
|
||||||
active_storage_blobs: true
|
active_storage_blobs: true
|
||||||
active_storage_variant_records: true
|
active_storage_variant_records: true
|
||||||
@@ -1131,6 +1150,25 @@ indexes:
|
|||||||
nulls_not_distinct:
|
nulls_not_distinct:
|
||||||
comment:
|
comment:
|
||||||
valid: true
|
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:
|
active_storage_attachments:
|
||||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||||
table: active_storage_attachments
|
table: active_storage_attachments
|
||||||
@@ -2161,4 +2199,4 @@ indexes:
|
|||||||
comment:
|
comment:
|
||||||
valid: true
|
valid: true
|
||||||
workflows: []
|
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
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
ApplicationRecord.with_each_tenant do |tenant|
|
||||||
|
puts "Processing tenant: #{tenant}"
|
||||||
|
|
||||||
|
ActionText::Markdown.find_each do |markdown|
|
||||||
|
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
|
||||||
@@ -214,9 +214,6 @@ class EnactTenanting
|
|||||||
assert_count Tagging, Tagging.count
|
assert_count Tagging, Tagging.count
|
||||||
assert_count Watch, Watch.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 Event, Event.count
|
||||||
assert_count Notification, Notification.count
|
assert_count Notification, Notification.count
|
||||||
|
|
||||||
@@ -228,22 +225,7 @@ class EnactTenanting
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update_action_text_urls
|
def update_action_text_urls
|
||||||
update_counter["ActionText::Markdown"] = 0
|
raise "No more markdown"
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def hardlink_active_storage
|
def hardlink_active_storage
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ ApplicationRecord.with_each_tenant do |tenant|
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Update action_text_markdowns table (polymorphic relationship)
|
# Update action_text_markdowns table (polymorphic relationship)
|
||||||
ActionText::Markdown.where(record_type: "Card").find_each do |markdown|
|
ActionText::RichText.where(record_type: "Card").find_each do |rich_text|
|
||||||
if id_mapping[markdown.record_id]
|
if id_mapping[rich_text.record_id]
|
||||||
markdown.update_column(:record_id, id_mapping[markdown.record_id])
|
rich_text.update_column(:record_id, id_mapping[rich_text.record_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ ApplicationRecord.with_each_tenant do |tenant|
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Update action_text_markdowns table (polymorphic relationship)
|
# Update action_text_markdowns table (polymorphic relationship)
|
||||||
ActionText::Markdown.where(record_type: "Collection").find_each do |markdown|
|
ActionText::RichText.where(record_type: "Collection").find_each do |rich_text|
|
||||||
if id_mapping[markdown.record_id]
|
if id_mapping[rich_text.record_id]
|
||||||
markdown.update_column(:record_id, id_mapping[markdown.record_id])
|
rich_text.update_column(:record_id, id_mapping[rich_text.record_id])
|
||||||
end
|
end
|
||||||
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" } }
|
put card_comment_path(cards(:logo), comments(:logo_agreement_kevin)), params: { comment: { body: "I've changed my mind" } }
|
||||||
|
|
||||||
assert_response :success
|
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
|
end
|
||||||
|
|
||||||
test "update another user's comment" do
|
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" } }
|
put card_comment_path(cards(:logo), comments(:logo_agreement_jz)), params: { comment: { body: "I've changed my mind" } }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_equal "moon.jpg", card.image.filename.to_s
|
assert_equal "moon.jpg", card.image.filename.to_s
|
||||||
assert_equal [ tags(:mobile) ], card.tags
|
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
|
end
|
||||||
|
|
||||||
test "users can only see cards in collections they have access to" do
|
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:
|
logo_agreement_jz:
|
||||||
record: logo_agreement_jz (Comment)
|
record: logo_agreement_jz (Comment)
|
||||||
name: body
|
name: body
|
||||||
content: I agree.
|
body: I agree.
|
||||||
|
|
||||||
logo_agreement_kevin:
|
logo_agreement_kevin:
|
||||||
record: logo_agreement_kevin (Comment)
|
record: logo_agreement_kevin (Comment)
|
||||||
name: body
|
name: body
|
||||||
content: Same, let’s do it.
|
body: Same, let’s do it.
|
||||||
|
|
||||||
layout_overflowing_david:
|
layout_overflowing_david:
|
||||||
record: layout_overflowing_david (Comment)
|
record: layout_overflowing_david (Comment)
|
||||||
name: body
|
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
|
fixtures :all
|
||||||
|
|
||||||
include ActiveJob::TestHelper
|
include ActiveJob::TestHelper
|
||||||
include CardTestHelper, ChangeTestHelper, SessionTestHelper
|
include ActionTextTestHelper, CardTestHelper, ChangeTestHelper, SessionTestHelper
|
||||||
end
|
end
|
||||||
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