Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: markdown field (easy_mde replacement) #3584

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CONTRIBUTING.MD
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,32 @@ To keep track of the schema structure for the models, run `annotate --models --e
## Using VSCode?

We compiled [an extension pack](https://marketplace.visualstudio.com/items?itemName=adrianthedev.vsruby) with a few extensions that should help you with Ruby development. We use them with Avo.

## Adding I18n Keys

When incorporating I18n keys, such as `avo.preview`, it's important to ensure that all locale files include the key with appropriate translations.

### Step 1: Add and Normalize
First, add the key to the `avo.en.yml` file. Then, run the following command to normalize the locale files alphabetically:
```bash
i18n-tasks normalize
```

### Step 2: Adding Missing Keys with or without Translation

When adding missing keys, you have two options: proceed without translation or automatically translate them using OpenAI.

#### Option 1: Add Missing Keys Without Translation
Run the following command to add the missing keys to all locale files:
```bash
i18n-tasks add-missing
```
This will populate the missing keys but retain the original label without translations applied.

#### Option 2: Add and Automatically Translate Missing Keys
If you prefer to automatically translate the missing keys, skip the previous step and execute this command with your OpenAI API token:
```bash
OPENAI_API_KEY=TOKEN bundle exec i18n-tasks translate-missing --backend=openai
```

Replace `TOKEN` with your actual OpenAI API key. This step will both add the missing keys and translate them automatically.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,5 @@ gem "avo-record_link_field"
gem "pagy", "> 8"

gem "csv"

gem "redcarpet"
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ GEM
rbs (2.8.4)
rdoc (6.10.0)
psych (>= 4.0.0)
redcarpet (3.6.0)
redis (5.3.0)
redis-client (>= 0.22.0)
redis-client (0.23.0)
Expand Down Expand Up @@ -741,6 +742,7 @@ DEPENDENCIES
rails (>= 8.0.0)
rails-controller-testing
ransack (>= 4.2.0)
redcarpet
redis (~> 5.0)
ripper-tags
rspec-rails (~> 6.0, >= 6.0.3)
Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/avo.base.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@import './css/fields/status.css';
@import './css/fields/code.css';
@import './css/fields/progress.css';
@import './css/fields/markdown.css';
@import './css/fields/trix.css';
@import './css/fields/tags.css';
@import './css/fields/tiptap.css';
Expand Down
128 changes: 128 additions & 0 deletions app/assets/stylesheets/css/fields/markdown.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
.markdown-content {
line-height: 1.5;
overflow-wrap: break-word;
word-break: break-word;

& h1,
& h2,
& h3,
& h4,
& h5,
& h6,
& table,
& div,
& pre,
& p,
& ul,
& ol,
& blockquote {
@apply mt-4;
}

& ul {
@apply list-disc;
}

& ol {
@apply list-decimal;
}
}

/* original `trix-content` styles taken from the trix.css */
.markdown-content {
line-height: 1.5;
overflow-wrap: break-word;
word-break: break-word;
}
.markdown-content * {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.markdown-content h1 {
font-size: 1.2em;
line-height: 1.2;
}
.markdown-content blockquote {
border: 0 solid #ccc;
border-left-width: 0.3em;
margin-left: 0.3em;
padding-left: 0.6em;
}
.markdown-content [dir=rtl] blockquote,
.markdown-content blockquote[dir=rtl] {
border-width: 0;
border-right-width: 0.3em;
margin-right: 0.3em;
padding-right: 0.6em;
}
.markdown-content li {
margin-left: 1em;
}
.markdown-content [dir=rtl] li {
margin-right: 1em;
}
.markdown-content pre {
display: inline-block;
width: 100%;
vertical-align: top;
font-family: monospace;
font-size: 0.9em;
padding: 0.5em;
white-space: pre;
background-color: #eee;
overflow-x: auto;
}
.markdown-content img {
max-width: 100%;
height: auto;
}
.markdown-content .attachment {
display: inline-block;
position: relative;
max-width: 100%;
}
.markdown-content .attachment a {
color: inherit;
text-decoration: none;
}
.markdown-content .attachment a:hover, .markdown-content .attachment a:visited:hover {
color: inherit;
}
.markdown-content .attachment__caption {
text-align: center;
}
.markdown-content .attachment__caption .attachment__name + .attachment__size::before {
content: " •";
}
.markdown-content .attachment--preview {
width: 100%;
text-align: center;
}
.markdown-content .attachment--preview .attachment__caption {
color: #666;
font-size: 0.9em;
line-height: 1.2;
}
.markdown-content .attachment--file {
color: #333;
line-height: 1;
margin: 0 2px 2px 2px;
padding: 0.4em 1em;
border: 1px solid #bbb;
border-radius: 5px;
}
.markdown-content .attachment-gallery {
display: flex;
flex-wrap: wrap;
position: relative;
}
.markdown-content .attachment-gallery .attachment {
flex: 1 0 33%;
padding: 0 0.5em;
max-width: 33%;
}
.markdown-content .attachment-gallery.attachment-gallery--2 .attachment, .markdown-content .attachment-gallery.attachment-gallery--4 .attachment {
flex-basis: 50%;
max-width: 50%;
}
1 change: 1 addition & 0 deletions app/assets/svgs/avo/heading.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/assets/svgs/avo/list-todo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/assets/svgs/avo/quote.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions app/components/avo/fields/easy_mde_field/edit_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<%= field_wrapper **field_wrapper_args, full_width: true do %>
<div data-controller="easy-mde">
<%= @form.text_area @field.id,
value: @field.value,
class: classes("w-full js-has-easy-mde-editor"),
data: {
view: view,
'easy-mde-target': 'element',
'component-options': @field.options.to_json,
},
disabled: disabled?,
placeholder: @field.placeholder,
autofocus: @autofocus,
style: @field.get_html(:style, view: view, element: :input)
%>
</div>
<% end %>
4 changes: 4 additions & 0 deletions app/components/avo/fields/easy_mde_field/edit_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

class Avo::Fields::EasyMdeField::EditComponent < Avo::Fields::EditComponent
end
11 changes: 11 additions & 0 deletions app/components/avo/fields/easy_mde_field/show_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<%= field_wrapper **field_wrapper_args, full_width: true do %>
<div data-controller="easy-mde">
<%= text_area_tag @field.id, @field.value,
class: helpers.input_classes('w-full js-has-easy-mde-editor'),
placeholder: @field.placeholder,
disabled: disabled?,
'data-easy-mde-target': 'element',
'data-component-options': @field.options.to_json,
'data-view': :show %>
</div>
<% end %>
4 changes: 4 additions & 0 deletions app/components/avo/fields/easy_mde_field/show_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

class Avo::Fields::EasyMdeField::ShowComponent < Avo::Fields::ShowComponent
end
72 changes: 58 additions & 14 deletions app/components/avo/fields/markdown_field/edit_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,17 +1,61 @@
<%= field_wrapper **field_wrapper_args, full_width: true do %>
<div data-controller="easy-mde">
<%= @form.text_area @field.id,
value: @field.value,
class: classes("w-full js-has-easy-mde-editor"),
data: {
view: view,
'easy-mde-target': 'element',
'component-options': @field.options.to_json,
},
disabled: disabled?,
placeholder: @field.placeholder,
autofocus: @autofocus,
style: @field.get_html(:style, view: view, element: :input)
%>
<%= content_tag :div,
class: "flex flex-col w-full border rounded",
data: {
controller: "markdown-field",
markdown_field_target: "toolbar",
markdown_field_preview_url_value: helpers.avo.markdown_previews_path,
markdown_field_active_tab_class: "bg-white",
markdown_field_attach_url_value: rails_direct_uploads_url,
markdown_field_resource_class_value: @resource.class.name,
markdown_field_field_id_value: @field.id,
} do %>
<div class="w-full flex-1 grow flex justify-bewteen bg-zinc-50 rounded px-2 py-1">
<div class="flex-1 flex items-center">
<button class="<%= BUTTON_CLASSES %>" data-action="click->markdown-field#switchToPreview" data-markdown-field-target="previewTabButton">
<%= t('avo.preview').humanize %>
</button>
<button class="<%= BUTTON_CLASSES %> hidden bg-zinc-200" data-action="click->markdown-field#switchToWrite" data-markdown-field-target="writeTabButton">
<%= t('avo.write').humanize %>
</button>
</div>

<markdown-toolbar for="<%= @field.id %>" class="flex space-x-1">
<md-bold title="<%= t('avo.bold').humanize %>" data-tippy="tooltip" class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/bold", class: "inline size-4" %></md-bold>
<md-header title="<%= t('avo.heading').humanize %>" data-tippy="tooltip" class="<%= BUTTON_CLASSES %>"><%= helpers.svg "avo/heading", class: "inline size-4" %></md-header>
<md-italic title="<%= t('avo.italic').humanize %>" data-tippy="tooltip" class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/italic", class: "inline size-4" %></md-italic>
<md-quote title="<%= t('avo.quote').humanize %>" data-tippy="tooltip" class="<%= BUTTON_CLASSES %>"><%= helpers.svg "avo/quote", class: "inline size-4" %></md-quote>
<md-code title="<%= t('avo.code').humanize %>" data-tippy="tooltip" class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/code", class: "inline size-4" %></md-code>
<md-link title="<%= t('avo.link').humanize %>" data-tippy="tooltip" class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/link", class: "inline size-4" %></md-link>
<md-image title="<%= t('avo.image').humanize %>" data-tippy="tooltip" class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/photo", class: "inline size-4" %></md-image>
<md-unordered-list title="<%= t('avo.unordered_list').humanize %>" data-tippy="tooltip" class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/list-bullet", class: "inline size-4" %></md-unordered-list>
<md-ordered-list title="<%= t('avo.ordered_list').humanize %>" data-tippy="tooltip" class="<%= BUTTON_CLASSES %>"><%= helpers.svg "heroicons/outline/numbered-list", class: "inline size-4" %></md-ordered-list>
<md-task-list title="<%= t('avo.task_list').humanize %>" data-tippy="tooltip" class="<%= BUTTON_CLASSES %>"><%= helpers.svg "avo/list-todo", class: "inline size-4" %></md-task-list>
</markdown-toolbar>
</div>

<div class="border-t">
<%= @form.text_area @field.id,
id: @field.id,
value: @field.value,
class: ("flex flex-1 rounded border-none w-full py-2 px-3"),
rows: 20,
data: {
markdown_field_target: "fieldElement",
action: "drop->markdown-field#dropUpload paste->markdown-field#pasteUpload"
},
disabled: disabled?,
placeholder: @field.placeholder,
autofocus: @autofocus,
style: @field.get_html(:style, view: view, element: :input)
%>
<%= content_tag :div, class: "hidden markdown-preview", id: "markdown-preview-#{@field.id}", data: { markdown_field_target: "previewElement" } do %>
<div class="button-spinner">
<div class="double-bounce1"></div>
<div class="double-bounce2"></div>
</div>
<% end %>
</div>
</div>
<% end %>
<% end %>
1 change: 1 addition & 0 deletions app/components/avo/fields/markdown_field/edit_component.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true

class Avo::Fields::MarkdownField::EditComponent < Avo::Fields::EditComponent
BUTTON_CLASSES = "cursor-pointer py-1 px-1.5 hover:bg-zinc-200 rounded"
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= content_tag :div, class: "prose prose-zinc Xmarkdown-content" do %>
<%= sanitize(@body, tags: %w(table th tr td span) + ActionView::Helpers::SanitizeHelper.sanitizer_vendor.safe_list_sanitizer.allowed_tags.to_a) %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class Avo::Fields::MarkdownField::RenderedContentComponent < Avo::BaseComponent
prop :body
end
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
<%= field_wrapper **field_wrapper_args, full_width: true do %>
<div data-controller="easy-mde">
<%= text_area_tag @field.id, @field.value,
class: helpers.input_classes('w-full js-has-easy-mde-editor'),
placeholder: @field.placeholder,
disabled: disabled?,
'data-easy-mde-target': 'element',
'data-component-options': @field.options.to_json,
'data-view': :show %>
</div>
<%= render Avo::Fields::MarkdownField::RenderedContentComponent.new(body: parsed_body) %>
<% end %>
3 changes: 3 additions & 0 deletions app/components/avo/fields/markdown_field/show_component.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# frozen_string_literal: true

class Avo::Fields::MarkdownField::ShowComponent < Avo::Fields::ShowComponent
def parsed_body
Avo::Fields::MarkdownField.parser.render(@field.value)
end
end
7 changes: 7 additions & 0 deletions app/controllers/avo/markdown_previews_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Avo
class MarkdownPreviewsController < ApplicationController
def create
@result = Avo::Fields::MarkdownField.parser.render(params[:body])
end
end
end
2 changes: 2 additions & 0 deletions app/javascript/js/controllers.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ItemSelectAllController from './controllers/item_select_all_controller'
import ItemSelectorController from './controllers/item_selector_controller'
import KeyValueController from './controllers/fields/key_value_controller'
import LoadingButtonController from './controllers/loading_button_controller'
import MarkdownController from './controllers/fields/markdown_controller'
import MenuController from './controllers/menu_controller'
import ModalController from './controllers/modal_controller'
import MultipleSelectFilterController from './controllers/multiple_select_filter_controller'
Expand Down Expand Up @@ -96,6 +97,7 @@ application.register('code-field', CodeFieldController)
application.register('date-field', DateFieldController)
application.register('easy-mde', EasyMdeController)
application.register('key-value', KeyValueController)
application.register('markdown-field', MarkdownController)
application.register('progress-bar-field', ProgressBarFieldController)
application.register('reload-belongs-to-field', ReloadBelongsToFieldController)
application.register('tags-field', TagsFieldController)
Expand Down
Loading
Loading