Use House for MD comments

This commit is contained in:
Jose Farias
2024-11-27 18:59:15 -06:00
parent 09b460c8be
commit d556e21287
28 changed files with 592 additions and 8 deletions
+3
View File
@@ -19,6 +19,9 @@ gem "thruster", require: false
# Features # Features
gem "bcrypt", "~> 3.1.7" gem "bcrypt", "~> 3.1.7"
gem "rqrcode" gem "rqrcode"
gem "redcarpet", "~> 3.6"
gem "rouge", "~> 4.5"
gem "jbuilder"
# Telemetry # Telemetry
gem "sentry-ruby" gem "sentry-ruby"
+8
View File
@@ -126,6 +126,9 @@ GEM
irb (1.14.1) irb (1.14.1)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.7.2) json (2.7.2)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
logger (1.6.1) logger (1.6.1)
@@ -223,10 +226,12 @@ GEM
rake (13.2.1) rake (13.2.1)
rdoc (6.7.0) rdoc (6.7.0)
psych (>= 4.0.0) psych (>= 4.0.0)
redcarpet (3.6.0)
regexp_parser (2.9.2) regexp_parser (2.9.2)
reline (0.5.10) reline (0.5.10)
io-console (~> 0.5) io-console (~> 0.5)
rexml (3.3.8) rexml (3.3.8)
rouge (4.5.1)
rqrcode (2.2.0) rqrcode (2.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
@@ -335,9 +340,12 @@ DEPENDENCIES
debug debug
hotwire_combobox! hotwire_combobox!
importmap-rails importmap-rails
jbuilder
propshaft propshaft
puma (>= 5.0) puma (>= 5.0)
rails (>= 8.0.0.rc1) rails (>= 8.0.0.rc1)
redcarpet (~> 3.6)
rouge (~> 4.5)
rqrcode rqrcode
rubocop-rails-omakase rubocop-rails-omakase
selenium-webdriver selenium-webdriver
+310
View File
@@ -0,0 +1,310 @@
/* Colors */
:root {
/* Named color values */
--house-md-black: 0% 0 0;
--house-md-white: 100% 0 0;
--house-md-gray-light: 96% 0.005 96;
--house-md-gray: 92% 0.005 96;
--house-md-gray-dark: 75% 0.005 96;
--house-md-blue: 54% 0.23 255;
--house-md-blue-light: 95% 0.03 255;
--house-md-blue-dark: 80% 0.08 255;
--house-md-orange: 70% 0.2 44;
--house-md-red: 51% 0.2 31;
--house-md-green: 65.59% 0.234 142.49;
--house-md-always-black: 0% 0 0;
/* Abstractions */
--house-color-negative: oklch(var(--house-md-red));
--house-color-positive: oklch(var(--house-md-green));
--house-color-bg: oklch(var(--house-md-white));
--house-color-ink: oklch(var(--house-md-black));
--house-color-ink-reversed: oklch(var(--house-md-white));
--house-color-link: oklch(var(--house-md-blue));
--house-color-subtle-light: oklch(var(--house-md-gray-light));
--house-color-subtle: oklch(var(--house-md-gray));
--house-color-subtle-dark: oklch(var(--house-md-gray-dark));
--house-color-selected: oklch(var(--house-md-blue-light));
--house-color-selected-dark: oklch(var(--house-md-blue-dark));
--house-color-alert: oklch(var(--house-md-orange));
--house-color-always-black: oklch(var(--house-md-always-black));
/* Redefine named color values for dark mode */
@media (prefers-color-scheme: dark) {
--house-md-black: 100% 0 0;
--house-md-white: 0% 0 0;
--house-md-gray: 25.2% 0 0;
--house-md-gray-dark: 30.12% 0 0;
--house-md-gray-darker: 44.95% 0 0;
--house-md-blue: 72.25% 0.16 248;
--house-md-blue-light: 28.11% 0.053 248;
--house-md-blue-dark: 42.25% 0.07 248;
--house-md-red: 73.8% 0.184 29.18;
--house-md-green: 75% 0.21 141.89;
}
}
/* Base styles */
:is(house-md-editor, house-md-toolbar) {
}
a:not([class]) {
color: var(--house-color-link);
text-decoration-skip-ink: auto;
}
:is(a, button, input, textarea) {
--outline-size: max(2px, 0.08em);
--outline-style: solid;
--house-outline-color: currentColor;
caret-color: var(--house-color-link);
&:focus {
outline: var(--outline-size) var(--outline-style) var(--house-outline-color);
outline-offset: var(--outline-offset, var(--outline-size));
}
&:focus-visible {
outline: var(--outline-size) var(--outline-style) var(--house-outline-color);
outline-offset: var(--outline-offset, var(--outline-size));
}
&:focus:not(:focus-visible) {
outline: none;
}
::selection {
background-color: var(--house-color-selected);
}
}
/* Editor */
house-md {
display: flex;
flex-direction: column;
flex-grow: 1;
&:invalid {
border: var(--house-color-negative) 2px solid;
}
}
/* Toolbar */
house-md-toolbar {
background-color: inherit;
border: 1px solid currentColor;
border-radius: 0.5em;
color: currentColor;
display: inline-flex;
font-size: inherit;
padding: 0.2em;
button, label {
aspect-ratio: 4/3;
appearance: none;
background-color: transparent;
block-size: 2em;
border: none;
border-radius: 0.5em;
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.9em;
fill: currentColor;
grid-area: 1/1;
inline-size: auto;
user-select: none;
}
&:is(:focus, :hover) {
background-color: var(--house-color-subtle);
}
&:is(:active) {
background-color: var(--house-color-selected);
}
}
}
/* Markdown Content */
.house-md-content {
caret-color: var(--color-link);
flex-grow: 1;
font-family: var(--font-sans);
min-block-size: 50dvh;
text-align: left;
white-space: break-spaces;
padding: 1rem;
&.house-md-content-empty::before {
content: attr(placeholder);
color: var(--house-color-ink);
opacity: 0.5;
pointer-events: none;
display: block;
}
&:active, &:focus{
border:none;
outline:none;
}
:is(.h1, .h2, .h3, .h4, .h5, .h6) {
display: inline-inblock;
font-weight: 600;
margin-block: 0;
}
.h1 {
font-size: 2em;
}
.h2 {
font-size: 1.5em;
}
.h3 {
font-size: 1.17em;
}
.h4 {
font-size: 1em;
}
.h5 {
font-size: 0.83em;
}
.h6 {
font-size: 0.67em;
}
.link, .img {
color: var(--house-color-link);
text-decoration: underline;
}
.hr {
border-color: var(--color-house-subtle-dark);
border-style: solid none none;
margin: 2lh auto;
}
.code {
font-family: ui-monospace, monospace;
font-weight: 600;
font-size: 0.85em;
}
.quote {
font-style: italic;
margin: 0 3ch;
}
.quote, .ol-li, .ul-li {
padding-left: 1em;
}
.comment {
color: var(--color-house-positive);
}
mark {
background-color: rgba(241, 241, 26, 0.678);
}
}
/* Uploads */
house-md-upload {
border-radius: 0.5em;
margin-block: 0.5ex;
position: relative;
&[status="failed"] {
background-color: var(--house-color-negative);
color: var(--house-color-ink-reversed);
font-weight: bold;
padding: 0.5em;
}
}
.md-close {
appearance: none;
display: none;
inline-size: 1em;
block-size: 1em;
border: none;
outline: none;
background-color: transparent;
position: absolute;
inset: 0.5em 0.5em auto auto;
cursor: pointer;
house-md-upload[status="failed"] & {
color: var(--house-color-ink-reversed);
display: inline-block;
}
&::before {
content: '×';
color: var(--house-color-ink-reversed);
font-size: 24px;
font-weight: bold;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.md-file {
font-weight: normal;
house-md-upload[status="complete"] & {
color: var(--house-color-positive);
}
}
.md-progress-bar {
-webkit-appearance: none;
appearance: none;
background-color: var(--house-color-subtle);
border-radius: 0.5em;
block-size: 1ex;
display: block;
inline-size: 100%;
line-height: inherit;
margin: 0;
house-md-upload[status="failed"] & ,
house-md-upload[status="complete"] &{
display: none;
}
&::-webkit-progress-bar {
border-radius: 0.5em;
background-color: var(--house-color-subtle);
}
&::-webkit-progress-inner-element {
border-radius: 0.5em;
}
&::-webkit-progress-value {
border-radius: 0.5em;
background-color: var(--house-color-positive);
}
&::-moz-progress-bar {
border-radius: 0.5em;
background-color: var(--house-color-positive);
}
}
@@ -0,0 +1,16 @@
class ActionText::Markdown::UploadsController < ApplicationController
include ActiveStorage::SetCurrent
def create
file = params[:file]
blob = ActiveStorage::Blob.create_and_upload! io: file, filename: file.original_filename, content_type: file.content_type
@upload = Current.account.uploads_attachments.create! blob: blob
render :create, status: :created, formats: :json
end
def show
@attachment = ActiveStorage::Attachment.find_by! slug: "#{params[:slug]}.#{params[:format]}"
expires_in 1.year, public: true
redirect_to @attachment.url
end
end
+1
View File
@@ -2,3 +2,4 @@
import "@hotwired/turbo-rails" import "@hotwired/turbo-rails"
import "initializers" import "initializers"
import "controllers" import "controllers"
import "house"
+2
View File
@@ -10,4 +10,6 @@ class Account < ApplicationRecord
has_many :stages, through: :workflows, class_name: "Workflow::Stage" has_many :stages, through: :workflows, class_name: "Workflow::Stage"
has_many :tags, dependent: :destroy has_many :tags, dependent: :destroy
has_many_attached :uploads
end end
+2
View File
@@ -4,4 +4,6 @@ class Comment < ApplicationRecord
belongs_to :creator, class_name: "User", default: -> { Current.user } belongs_to :creator, class_name: "User", default: -> { Current.user }
searchable_by :body, using: :comments_search_index searchable_by :body, using: :comments_search_index
has_markdown :body
end end
@@ -0,0 +1,4 @@
json.message "File uploaded successfully"
json.fileName @upload.filename.to_s
json.mimetype @upload.content_type
json.fileUrl @upload.slug_url
+1 -1
View File
@@ -13,7 +13,7 @@
<% end %> <% end %>
</div> </div>
<div class="comment__body txt-align-start"> <div class="comment__body txt-align-start">
<%= simple_format comment.body %> <%= comment.body.to_html %>
</div> </div>
</div> </div>
<% end %> <% end %>
+1 -1
View File
@@ -7,7 +7,7 @@
</div> </div>
<div class="comment__body txt-align-start margin-block-start-half"> <div class="comment__body txt-align-start margin-block-start-half">
<%= form_with model: Comment.new, url: bucket_bubble_comments_path(bubble.bucket, bubble), class: "flex flex-column gap full-width", data: { controller: "form", action: "keydown.meta+enter->form#submit" } do |form| %> <%= form_with model: Comment.new, url: bucket_bubble_comments_path(bubble.bucket, bubble), class: "flex flex-column gap full-width", data: { controller: "form", action: "keydown.meta+enter->form#submit" } do |form| %>
<%= form.text_area :body, class: "input border border-radius", required: true, rows: 4, placeholder: new_comment_placeholder(bubble) %> <%= form.markdown_area :body, class: "input border border-radius", required: true, placeholder: new_comment_placeholder(bubble) %>
<%= form.button class: "btn btn--reversed center" do %> <%= form.button class: "btn btn--reversed center" do %>
<%= image_tag "check.svg", aria: { hidden: "true" }, size: 24 %> <%= image_tag "check.svg", aria: { hidden: "true" }, size: 24 %>
<span class="for-screen-reader">Save</span> <span class="for-screen-reader">Save</span>
+1
View File
@@ -4,6 +4,7 @@ pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js" 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 "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"
+4
View File
@@ -0,0 +1,4 @@
ActiveSupport.on_load :action_text_markdown do
require "markdown_renderer"
ActionText::Markdown.renderer = -> { MarkdownRenderer.build }
end
+5
View File
@@ -0,0 +1,5 @@
Rails.application.config.after_initialize do
%w[ db files ].each do |dir|
Rails.root.join("storage", dir).mkpath
end
end
+5
View File
@@ -50,6 +50,11 @@ Rails.application.routes.draw do
resources :stages, module: :workflows resources :stages, module: :workflows
end end
namespace :action_text, path: nil do
get "/u/*slug" => "markdown/uploads#show", as: :markdown_upload
post "/uploads" => "markdown/uploads#create", as: :markdown_uploads
end
get "join/:join_code", to: "users#new", as: :join get "join/:join_code", to: "users#new", as: :join
post "join/:join_code", to: "users#create" post "join/:join_code", to: "users#create"
get "up", to: "rails/health#show", as: :rails_health_check get "up", to: "rails/health#show", as: :rails_health_check
+1 -1
View File
@@ -4,7 +4,7 @@ test:
local: local:
service: Disk service: Disk
root: <%= Rails.root.join("storage") %> root: <%= Rails.root.join("storage", "files") %>
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon: # amazon:
@@ -0,0 +1,13 @@
class CreateActionTextMarkdowns < ActiveRecord::Migration[8.0]
def change
create_table :action_text_markdowns do |t|
t.references :record, polymorphic: true, null: false
t.string :name, null: false
t.text :content, null: false, default: ""
t.timestamps
end
add_column :active_storage_attachments, :slug, :string
add_index :active_storage_attachments, :slug, unique: true
end
end
@@ -0,0 +1,5 @@
class RemoveBodyFromComments < ActiveRecord::Migration[8.0]
def change
remove_column :comments, :body, :text, null: false
end
end
Generated
+13 -2
View File
@@ -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.0].define(version: 2024_11_20_222444) do ActiveRecord::Schema[8.0].define(version: 2024_11_26_232658) do
create_table "accesses", force: :cascade do |t| create_table "accesses", force: :cascade do |t|
t.integer "bucket_id", null: false t.integer "bucket_id", null: false
t.integer "user_id", null: false t.integer "user_id", null: false
@@ -30,14 +30,26 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_20_222444) do
t.index ["name"], name: "index_accounts_on_name", unique: true t.index ["name"], name: "index_accounts_on_name", unique: true
end end
create_table "action_text_markdowns", force: :cascade do |t|
t.string "record_type", null: false
t.integer "record_id", null: false
t.string "name", null: false
t.text "content", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record"
end
create_table "active_storage_attachments", force: :cascade do |t| create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false
t.bigint "record_id", null: false t.bigint "record_id", null: false
t.bigint "blob_id", null: false t.bigint "blob_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.string "slug"
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
t.index ["slug"], name: "index_active_storage_attachments_on_slug", unique: true
end end
create_table "active_storage_blobs", force: :cascade do |t| create_table "active_storage_blobs", force: :cascade do |t|
@@ -116,7 +128,6 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_20_222444) do
end end
create_table "comments", force: :cascade do |t| create_table "comments", force: :cascade do |t|
t.text "body", null: false
t.integer "creator_id", null: false t.integer "creator_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
+33
View File
@@ -0,0 +1,33 @@
require "rouge/plugins/redcarpet"
class MarkdownRenderer < Redcarpet::Render::HTML
include Rouge::Plugins::Redcarpet
def self.build
renderer = MarkdownRenderer.new(ActionText::Markdown::DEFAULT_RENDERER_OPTIONS)
Redcarpet::Markdown.new(renderer, ActionText::Markdown::DEFAULT_MARKDOWN_EXTENSIONS)
end
def initialize(*args)
super
@id_counts = Hash.new(0)
end
def header(text, header_level)
unique_id(text).then do |id|
"<h#{header_level} id='#{id}'>#{text} <a href='##{id}' class='heading__link' aria-hidden='true'>#</a></h#{header_level}>"
end
end
def image(url, title, alt_text)
%(<a title="#{title}" data-action="lightbox#open:prevent" data-lightbox-target="image" data-lightbox-url-value="#{url}?disposition=attachment" href="#{url}"><img src="#{url}" alt="#{alt_text}"></a>)
end
private
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
end
+39
View File
@@ -0,0 +1,39 @@
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}?
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
+42
View File
@@ -0,0 +1,42 @@
module ActionText
class Markdown < Record
DEFAULT_RENDERER_OPTIONS = {
filter_html: false
}
DEFAULT_MARKDOWN_EXTENSIONS = {
autolink: true,
highlight: true,
no_intra_emphasis: true,
fenced_code_blocks: true,
lax_spacing: true,
strikethrough: true,
tables: true
}
mattr_accessor :renderer, default: Redcarpet::Markdown.new(
Redcarpet::Render::HTML.new(DEFAULT_RENDERER_OPTIONS), DEFAULT_MARKDOWN_EXTENSIONS)
belongs_to :record, polymorphic: true, touch: true
def to_html
(renderer.try(:call) || renderer).render(content).html_safe
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
+36
View File
@@ -0,0 +1,36 @@
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: action_text_markdown_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
+32
View File
@@ -0,0 +1,32 @@
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.action_text_markdown_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
+14
View File
@@ -0,0 +1,14 @@
logo_agreement_jz:
record: logo_agreement_jz (Comment)
name: body
content: I agree.
logo_agreement_kevin:
record: logo_agreement_kevin (Comment)
name: body
content: Same, lets do it.
layout_overflowing_david:
record: layout_overflowing_david (Comment)
name: body
content: The text is overflowing the container.
-3
View File
@@ -1,13 +1,10 @@
logo_agreement_jz: logo_agreement_jz:
body: I agree.
creator: jz creator: jz
created_at: <%= 2.days.ago %> created_at: <%= 2.days.ago %>
logo_agreement_kevin: logo_agreement_kevin:
body: Same, lets do it.
creator: kevin creator: kevin
created_at: <%= 2.hours.ago %> created_at: <%= 2.hours.ago %>
layout_overflowing_david: layout_overflowing_david:
body: The text is overflowing the container.
creator: david creator: david
View File
View File
File diff suppressed because one or more lines are too long