Closes #11294: Markdown Preview (#11894)

* MarkdownWidget

* Change border and color of active markdown tab

* Fix template name typo

* Add render markdown endpoint

* Static assets for markdown widget

* widget style fix and unique ids based on name

* Replace SmallTextArea with SmallMarkdownWidget

* Clear innerHTML before swapping

* render markdown directly in template

* change render markdown view path

* remove small markdown widget

* Simplify rendering logic

* Use a form to clean input Markdown data

---------

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
This commit is contained in:
Aron Bergur Jóhannsson 2023-03-09 13:21:13 +00:00 committed by GitHub
parent 33286aad39
commit fa60f9d2a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 138 additions and 46 deletions

View File

@ -7,7 +7,7 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea,
add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
StaticSelect,
)
@ -35,7 +35,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label=_('Comments')
)
@ -63,7 +62,6 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label=_('Comments')
)
@ -125,7 +123,6 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label=_('Comments')
)

View File

@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect, SelectSpeedWidget,
DynamicModelMultipleChoiceField, form_from_model, StaticSelect, SelectSpeedWidget
)
__all__ = (
@ -138,7 +138,6 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -309,7 +308,6 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -345,7 +343,6 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -406,7 +403,6 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -441,7 +437,6 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -551,7 +546,6 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -594,7 +588,6 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -644,7 +637,6 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -668,7 +660,6 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -714,7 +705,6 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -776,7 +766,6 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label=_('Comments')
)

View File

@ -2,6 +2,7 @@ from .model_forms import *
from .filtersets import *
from .bulk_edit import *
from .bulk_import import *
from .misc import *
from .mixins import *
from .config import *
from .scripts import *

View File

@ -0,0 +1,14 @@
from django import forms
__all__ = (
'RenderMarkdownForm',
)
class RenderMarkdownForm(forms.Form):
"""
Provides basic validation for markup to be rendered.
"""
text = forms.CharField(
required=False
)

View File

@ -92,4 +92,6 @@ urlpatterns = [
path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
re_path(r'^scripts/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ScriptView.as_view(), name='script'),
# Markdown
path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown")
]

View File

@ -1,7 +1,7 @@
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, Q
from django.http import Http404, HttpResponseForbidden
from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.generic import View
@ -10,6 +10,7 @@ from rq import Worker
from netbox.views import generic
from utilities.htmx import is_htmx
from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
from . import filtersets, forms, tables
@ -885,3 +886,18 @@ class JobResultBulkDeleteView(generic.BulkDeleteView):
queryset = JobResult.objects.all()
filterset = filtersets.JobResultFilterSet
table = tables.JobResultTable
#
# Markdown
#
class RenderMarkdownView(View):
def post(self, request):
form = forms.RenderMarkdownForm(request.POST)
if not form.is_valid():
HttpResponseBadRequest()
rendered = render_markdown(form.cleaned_data['text'])
return HttpResponse(rendered)

View File

@ -10,7 +10,7 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, NumericArrayField,
SmallTextarea, StaticSelect, DynamicModelMultipleChoiceField,
StaticSelect, DynamicModelMultipleChoiceField
)
__all__ = (
@ -48,7 +48,6 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -69,7 +68,6 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -116,7 +114,6 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -145,7 +142,6 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -227,7 +223,6 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -266,7 +261,6 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -314,7 +308,6 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -359,7 +352,6 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -442,7 +434,6 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -474,7 +465,6 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -504,7 +494,6 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,6 +4,7 @@ import { initMoveButtons } from './moveOptions';
import { initReslug } from './reslug';
import { initSelectAll } from './selectAll';
import { initSelectMultiple } from './selectMultiple';
import { initMarkdownPreviews } from './markdownPreview';
export function initButtons(): void {
for (const func of [
@ -13,6 +14,7 @@ export function initButtons(): void {
initSelectAll,
initSelectMultiple,
initMoveButtons,
initMarkdownPreviews,
]) {
func();
}

View File

@ -0,0 +1,45 @@
import { isTruthy } from 'src/util';
/**
* interface for htmx configRequest event
*/
declare global {
interface HTMLElementEventMap {
'htmx:configRequest': CustomEvent<{
parameters: Record<string, string>;
headers: Record<string, string>;
}>;
}
}
function initMarkdownPreview(markdownWidget: HTMLDivElement) {
const previewButton = markdownWidget.querySelector('button.preview-button') as HTMLButtonElement;
const textarea = markdownWidget.querySelector('textarea') as HTMLTextAreaElement;
const preview = markdownWidget.querySelector('div.preview') as HTMLDivElement;
/**
* Make sure the textarea has style attribute height
* So that it can be copied over to preview div.
*/
if (!isTruthy(textarea.style.height)) {
const { height } = textarea.getBoundingClientRect();
textarea.style.height = `${height}px`;
}
/**
* Add the value of the textarea to the body of the htmx request
* and copy the height of text are to the preview div
*/
previewButton.addEventListener('htmx:configRequest', e => {
e.detail.parameters = { text: textarea.value || '' };
e.detail.headers['X-CSRFToken'] = window.CSRF_TOKEN;
preview.style.minHeight = textarea.style.height;
preview.innerHTML = '';
});
}
export function initMarkdownPreviews(): void {
for (const markdownWidget of document.querySelectorAll<HTMLDivElement>('.markdown-widget')) {
initMarkdownPreview(markdownWidget);
}
}

View File

@ -236,12 +236,12 @@ table {
}
th.asc > a::after {
content: "\f0140";
content: '\f0140';
font-family: 'Material Design Icons';
}
th.desc > a::after {
content: "\f0143";
content: '\f0143';
font-family: 'Material Design Icons';
}
@ -419,7 +419,7 @@ nav.search {
// Styles for the quicksearch and its clear button;
// Overrides input-group styles and adds transition effects
.quicksearch {
input[type="search"] {
input[type='search'] {
border-radius: $border-radius !important;
}
@ -998,9 +998,24 @@ div.card-overlay {
padding: 8px;
}
/* Markdown widget */
.markdown-widget {
.nav-link {
border-bottom: 0;
&.active {
background-color: var(--nbx-body-bg);
}
}
.nav-tabs {
background-color: var(--nbx-pre-bg);
}
}
// Preformatted text blocks
td pre {
margin-bottom: 0
margin-bottom: 0;
}
pre.block {
padding: $spacer;

View File

@ -2,7 +2,7 @@ from django import forms
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import *
from utilities.forms import CommentField, DynamicModelChoiceField, SmallTextarea
from utilities.forms import CommentField, DynamicModelChoiceField
__all__ = (
'ContactBulkEditForm',
@ -106,7 +106,6 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)

View File

@ -27,7 +27,7 @@ class CommentField(forms.CharField):
"""
A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`.
"""
widget = forms.Textarea
widget = widgets.MarkdownWidget
help_text = f"""
<i class="mdi mdi-information-outline"></i>
<a href="{static('docs/reference/markdown/')}" target="_blank" tabindex="-1">

View File

@ -16,6 +16,7 @@ __all__ = (
'ColorSelect',
'DatePicker',
'DateTimePicker',
'MarkdownWidget',
'NumericArrayField',
'SelectDurationWidget',
'SelectSpeedWidget',
@ -116,6 +117,10 @@ class SelectDurationWidget(forms.NumberInput):
template_name = 'widgets/select_duration.html'
class MarkdownWidget(forms.Textarea):
template_name = 'widgets/markdown_input.html'
class NumericArrayField(SimpleArrayField):
def clean(self, value):

View File

@ -6,7 +6,7 @@
{# Render the field label, except for: #}
{# 1. Checkboxes (label appears to the right of the field #}
{# 2. Textareas with no label set (will expand across entire row) #}
{% if field|widget_type == 'checkboxinput' or field|widget_type == 'textarea' and not label %}
{% if field|widget_type == 'checkboxinput' or field|widget_type == 'textarea' or field|widget_type == 'markdownwidget' and not label %}
{% else %}
<label for="{{ field.id_for_label }}" class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}">
{{ label }}

View File

@ -0,0 +1,22 @@
<div class="border rounded markdown-widget">
<ul class="nav nav-tabs px-3 pt-2 rounded-top border-0">
<li class="nav-item" role="presentation">
<button class="nav-link active " id="{{ widget.name }}-input-tab" data-bs-toggle="tab" data-bs-target="#{{ widget.name }}-input" type="button" role="tab" aria-controls="{{ widget.name }}-input" aria-selected="true">
Write
</button>
</li>
<li class="nav-item" role="presentation">
<button hx-target="#{{ widget.name }}-preview" hx-swap="innerHTML" hx-post="{% url 'extras:render_markdown' %}" class="nav-link preview-button" id="{{ widget.name }}-markdown-preview-tab" data-bs-toggle="tab" data-bs-target="#{{ widget.name }}-markdown-preview" type="button" role="tab" aria-controls="{{ widget.name }}-markdown-preview" aria-selected="false">
Preview
</button>
</li>
</ul>
<div class="tab-content bg-body rounded-bottom border-top">
<div class="tab-pane show active" id="{{ widget.name }}-input" role="tabpanel" aria-labelledby="{{ widget.name }}-input-tab">
{% include "django/forms/widgets/textarea.html" %}
</div>
<div class="tab-pane show" id="{{ widget.name }}-markdown-preview" role="tabpanel" aria-labelledby="{{ widget.name }}-markdown-preview-tab">
<div id="{{ widget.name }}-preview" class="preview px-3 py-2">Testing</div>
</div>
</div>
</div>

View File

@ -9,7 +9,7 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, SmallTextarea, StaticSelect
DynamicModelMultipleChoiceField, StaticSelect
)
from virtualization.choices import *
from virtualization.models import *
@ -90,7 +90,6 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label=_('Comments')
)
@ -163,7 +162,6 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label=_('Comments')
)

View File

@ -5,7 +5,7 @@ from dcim.choices import LinkStatusChoices
from ipam.models import VLAN
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField, SmallTextarea
from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField
from wireless.choices import *
from wireless.constants import SSID_MAX_LENGTH
from wireless.models import *
@ -74,7 +74,6 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
@ -119,7 +118,6 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)