From fa60f9d2a882df13cc8e8b47a4f227455897f815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aron=20Bergur=20J=C3=B3hannsson?= Date: Thu, 9 Mar 2023 13:21:13 +0000 Subject: [PATCH] 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 --- netbox/circuits/forms/bulk_edit.py | 5 +- netbox/dcim/forms/bulk_edit.py | 13 +---- netbox/extras/forms/__init__.py | 1 + netbox/extras/forms/misc.py | 14 ++++++ netbox/extras/urls.py | 2 + netbox/extras/views.py | 18 ++++++- netbox/ipam/forms/bulk_edit.py | 13 +---- netbox/project-static/dist/netbox-dark.css | Bin 374883 -> 375160 bytes netbox/project-static/dist/netbox-light.css | Bin 232430 -> 232605 bytes netbox/project-static/dist/netbox-print.css | Bin 726343 -> 726930 bytes netbox/project-static/dist/netbox.js | Bin 380966 -> 381466 bytes netbox/project-static/dist/netbox.js.map | Bin 353776 -> 354201 bytes netbox/project-static/src/buttons/index.ts | 2 + .../src/buttons/markdownPreview.ts | 45 ++++++++++++++++++ netbox/project-static/styles/netbox.scss | 29 ++++++++--- netbox/tenancy/forms/bulk_edit.py | 3 +- netbox/utilities/forms/fields/fields.py | 2 +- netbox/utilities/forms/widgets.py | 5 ++ .../templates/form_helpers/render_field.html | 2 +- .../templates/widgets/markdown_input.html | 22 +++++++++ netbox/virtualization/forms/bulk_edit.py | 4 +- netbox/wireless/forms/bulk_edit.py | 4 +- 22 files changed, 138 insertions(+), 46 deletions(-) create mode 100644 netbox/extras/forms/misc.py create mode 100644 netbox/project-static/src/buttons/markdownPreview.ts create mode 100644 netbox/utilities/templates/widgets/markdown_input.html diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index e1fe6338d..0449f8e99 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -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') ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 38fa55738..bd466ca48 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -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') ) diff --git a/netbox/extras/forms/__init__.py b/netbox/extras/forms/__init__.py index af0f7cf43..0825c9ca7 100644 --- a/netbox/extras/forms/__init__.py +++ b/netbox/extras/forms/__init__.py @@ -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 * diff --git a/netbox/extras/forms/misc.py b/netbox/extras/forms/misc.py new file mode 100644 index 000000000..b52338e76 --- /dev/null +++ b/netbox/extras/forms/misc.py @@ -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 + ) diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index f41a45f5a..304e5b9ea 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -92,4 +92,6 @@ urlpatterns = [ path('scripts/results//', views.ScriptResultView.as_view(), name='script_result'), re_path(r'^scripts/(?P.([^.]+)).(?P.(.+))/', views.ScriptView.as_view(), name='script'), + # Markdown + path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown") ] diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 2d2608ae8..91d3b5c58 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -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) diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index d0af43975..63352698b 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -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' ) diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index 8b6b37a4cc545892c5ab1d46c60ebe75f8bc5a40..74ab785a5794f756ce265e7c39fb9175b729c616 100644 GIT binary patch delta 139 zcmaF-LF~sTv4$4L7N!>F7M3ln+FKR%aubWPQ}WC6bjveS(o;(m^zstRbaOKEva6Hw zi&9dHrVF?+>GGqgn*Q36NqPE)HLP6hN%<+2x=HEN8ILfj@u8YEU3vqn3|JLgK~XAD Q(PYCZD%-WUvRW_$0C(my4*&oF delta 25 hcmezIN$l|lv4$4L7N!>F7M3ln+FQ3fY-6=x1^}5m3JL%K diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index 27e804c3084267eba5a70d9352ea5fb676a6d35f..f12291e44d1dc368699f6c51307d5d63d40dd267 100644 GIT binary patch delta 139 zcmaDiop0_;zJ?aY7N#xCOFFIfaubWPQ}WC6bjveS(o;(m^zstRbaOKEva6Hwi&9dH zbd&N+O7e593~JGpPT$(btUT>86PHp_eoCcoQo1IF5{PLfiAmERYBC9f)J}if$;`fe JaVN7k69A~~G`#=- delta 21 dcmbO`lkeShzJ?aY7N#xCOFFj)cQJc00RUrE2r2*o diff --git a/netbox/project-static/dist/netbox-print.css b/netbox/project-static/dist/netbox-print.css index f0220c0500b66f04fbdffb8e04f83bc86900c527..5d7f7342be9c1ee67fd170bc98a19cebc26f3aec 100644 GIT binary patch delta 302 zcmX@UN@vn~orV_17N!>F7M2#)7Pc1l7LFFq7OpMa|9J)UaubWPQ}WC6bjveS(o;*O ze_YO`$B!;J(UL{q9!*d|FE6o7HzzYMyE-YqC?&N>Hz~iQBtO^6pf;l~bS+R&eY(L~R!&J&iy$s8 gNlYrPo-SC!Da}?;l&YJQK3!3PQFQx%UTzO|02dEy?EnA( delta 37 tcmbQVUg!8KorV_17N!>F7M2#)7Pc1l7LFFq7OpMa|9Q8232=L`0|5V+3?Kji diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index ed6e127477872c2c111e2c18a555bdc54769df5f..f430604f967c52fa5ed13c20a60064e1cdff88ac 100644 GIT binary patch delta 17996 zcmZvE349yH_5WvPrQG-7#BmN=Q5-2=%W^^zVrS#Xl5ESCEXR`X7)7@gNvr#^9j+E= zq0oi}rY%=ODU_=L0xT2=_mu>a6ewpnN+|rw4J}Yg%m2-;Ea%7n_xU85o!QaMd-LA= zzBg}XbJM;hzrAtEOpQj%Z5zwFlaK5+D?P#~a zFm@Cc984Fo2{$seWxHJ(vrl77LC-3V&qN|kR+pyp;0MgQ^B0N^XFW+R+mhsZ z(i*d%zVLBmKKQ|fzpFrl2QRr{6~g{{;r8FDaJpVN{<|B>_d`7cV=XFOSnii*q=e4Y#Fa0(n)8MXWuqt!IO!4n0oJ@78D=gOgnM2m^m$QRd)YNwQor{G>Z`oMpt7q+{-#g=+F9OsL2s3ZpfCrr9i+Z$BaNO#x8mVUurEaZG&YOX0 z43pEe3a{MSG!(CAO{HEPMd|cIZ#t+k&P78|wmZ2Dty|;LDC`cX!)R>U&GkDqs$4jw z(z3jp>u~BEg{UVMVF#Vu4k2+{g`(ZrXp$(me=|+EF@RmwuGA%d6iN=OUDkK!!`&b-tK46ZP|HHYD7B$2QQNkM7W{EY!2ROTUZRxinRY zl$}#k%G^Tz9~VxI*{iFoQWF%+o#nP`RK8FaSXa5p4xh4I_{|^7=a}o+L}{Ri@aP{` zPl?#65(@Fhs)e}+R@b=d*|t&-FE9=AXESzbNgLZH1(dNj345PhOx{|776?B*P=>t1 z)d#wejh_t?28t+&SaR+UX{?1<|ETu>N1xMr@$t^qTHj+!JCS>dvS z`;lGH-!rzsP|voP=qVHjHrqL#3daQl0a zHD&FdhKcvng?0Cqml^BX9i<6^84e6z%Q}RCdtJyOJaF%(Dl7ahIssm}qB`a6T$Zvj z80T`ry!)C^Ug)}S5o@Yv`BGn}T{a;*$dwM7j&e@Wm$1J8w$jK8hR2o&2X3Aw9Jp^T zF#E`TivfLKzHfE4rJi+{1~J$<51q(ZoY-oY_1Gk=6qii5*|{Jc+9GTd*OrIvoYAi9 z^Si?~0McxP4Y^rQgzdBo`)^(>2x2+zvXl8bR4Ke5&PG<@P0@=C!uI<$0REx--=1v) z>QZTU+7J3{5qcizK_=n02d+M+)6NF{>9jxD5=%C8*(Wq65tB3~42WDJx4TGYB+89a zk?#fx(K2D@+?3F^;<|aG+-@pCHLOuo2cxqs(vzPL#FAb=+id3?l5S}nY2m3q-49Oc z><3k-McDt~a?~swdQiKd+pZl;@l9!di06knKi`lL{`KI#&1%rhkx)vjOT{uU()f5= zv0;z4=mC5wiSEvNHc+B_R5<=nEgBX6`_P)kF7P3x7k!|^l<-1?^QEBdEt*pgl(1as zm`tYA4ORdMeAdJ3QLC``;jO4oc;Vpzb*mkG2xs@~_IuM50oWe9R?Be{hSLOO4+tHH z29@35kD}z=I<$CsM{&x2SOXA!<4CmR4LgK44y{|>U*xDpHLRKtMou>0#KEdZ$}qUZ zGaoy@OkK}rON{vST%L}W1-)A=#J`)S3RC^zXId{q9~xMmHI@4Q%`P05nd&jIsYG~wC?NdzsirwS;7LnE z_=Jw9YmrYVJgw3V)U(#oixEBNq*fT$b1_O_R4)e%K|R+-)i^6W_w-(MRIl-Zh~qpL z*K2(6RN%Q0y(Yu60iNBUWrIAx-PzBSqwK+7KXWHiwu0l8Wj@^_uQ@nMDxGtmTZKHr zzUR!!j(XN6ciC9bb7s&)9Tba^aGQJ&E2u~d5kW)ZMJXLl*w>t`Mt~)A+L1Xm|>-Vtr+Qvi_ z@Y=3#A}7vb774#TvU%koxUte;hT_^7^;{>cNn=|P;HD}JdW}(-dvry;RWCbLlb*9u z(rtPPzgdf(8==M(hB|HUgp#60moR*^4uI;?qZJD*lJo$=xv*Yx7G1)hj;<-|q|Fkr z7hRA|ucaCIs&J+MaGngC{(gpWmu)+D&JufpPWP zh6H^+-BI=cj6)#GC)nQ7=xs#?)J1)05gvK|%;jw|$tj*w>oqZ67xDAK^a!Xo&$S2( zUwCO#GQkd%#;|2%rkjgnjq14`&|_VQ_xbbo0NbbMk~B;lD5}UTz`>?tYXE5c$65ip z?l@Klfy~KcS1sz*a~8UcJ$lYXv1}e8`~#Us#S2EVCQ$}!LznY{At-F&-Dw}oT%4D66|1U%pFA`4(hob z)E@l?&P2yd8zkr5I4JQk@6YoV4a(~T!=w$m02|{uzd<e?}3P~{KZWu zDRjM9Rv9yJ5x36gh9Guq51W!|od&0+*UYIpRRBX5zF2F?Bv`&QeOEDOcV@{E`!J?9rFjLP+-E zO9Q)n1_~8`AbWr#U24P~9Uu4d)zx0P0?6f+dZ_3t6txh)sU(t zYoOkS^+2mXXwJtqs1@;BKT-G*)4#`^K` zs!n0ko0}l+iN0yx;!3cE(g@848AV5BLt^_3a(LYf8b~#*5G=(D&B8}-u3OkA>3<`5 z2o2pygPrvXnv*KPjE<8VsvHT{RT{eow3dR5YjTVp;rf%c+j0rkTIw}WoUzrwnW>KF zf-=x_7$k^d`wg6fwzmUsbp3V+Gv3+&*1YYl+sg(GTp+`H)1erz(Ml?`khcN=)RkKbLbvM1PZX@I0r0+z<`B zLxSnOvnwLT2AW%$;HpE8;EV}YEvur%E0e_T4syM888b#1Y63+Xd_|r|j9iNH+>_ya zFn0(db$2?Jgt-$zt_qSB!ua3H1?Qlqr8#Z zLp#H2g7;y8M~a3H<4_W|0y!gK;l!%;$hu+!Z(U*}FX%p44Li2(4}4`7aKWV)PNQr| zcH!s;pHFid!D(w*yYS?PCPf&$z?BG2W4TI zjIv*_2}A$W2Ox3qf3koW>;AqGLtwM(<8PLjj2Z|JqdbI%Ls5QM+vty^{FMfQ|HObg zg*!hf2NXH{$tu{R{r!`QnVm*1l+~qR!_+0r`*bC=)_qzD{klF~0{uoltwCJ}ulRH| zTH6Jvo1O5Q-Jl^fZryE^!94-mNBXkCzFqe^$>;Sx*N;w{XQjp8@FG`E}!rL8C6P2eK6qgzW!%aZSp~ z2HdH1hriH}w9?%;-Q`R3hO8j1mZaZ5>Tify_cZnk?|xmTZvjv*ty07)W24t98&$^2 zMS#oGa3UbA{iey`wdxGXq`SazT$&#r4_JpX)?ukqg5F3#l2vnp&6S4tS*3j{J5n4z zUYss!<>JBv-)yajS|Pg&`*hXz?WH-PWyCt<6Xtyjd%y&nFO3$o$~Kr48o%9&vO@mb zet?w|->v|?`|R7VHuVDPmpXfjoeMw$m7>`y@5fwLdB5g>lD6lBrQbb)T*8~*oeiPM z(7y*DRJixwmc<1tgj`zf7Z}wHvPP>63|1>= zqmX9;UII|sKr?CS3&^@W5|ATaioM=4~u@)R;5qo_sbLxikk0fPd_ zLJAD}#bTRPm?T3AUAD90@DY z^75|Y+Qy4?bQkH`P4+3#s;vnt*CPW_oAgx55V2}tII9c5zJ+ZmJzD54hHa)66^>B- zC-bMEKBN{yQ;-s&7809^{!rO%)j%*CllEI|LaK~WKhXeDTgcbYOg2wLE75M!I}NQv zZREUZNRPV2yQiThtZ0YL2c^EeKPxZvsFh37h3qRXa4%8ML~Ez_Smlh`0CCPl*RE}~ za)Xl~(+hS{OrWXQ9a*+Lt8MJDR#y*@rL)kwd3{!$pLcsAei$~G;#x`fEVLPoiWkp9 zf5K>xnCBuk+;4@5IVNF7rJM_gz`5NDG3?G%%{cs3*9F-fv_4c@Gpx@K>1(&RYXig3 z9%2V+yWTUTub1n76RpRf&t|zHZQ@FSF;8|ZMa?KlR?R~V(?TYWH;s=ciF+Pep^up+ za4TN&wkZ=AGwCLBR;=43=Vj<#I70q74>Tb}j?F_$keBQ%M*(7+52L%qU(H9;5Q>mF z6(}dpTZmpjC_~;}gl>d>KV6K@ho1XZpgQCu$5$X#S-?btoa*Y7NlqX@h2#23tPE{} zE;p5-YuC41Srud|Xfz7jv_Y#b?N3ShvB_=R__!ZPEjBl$$(&WF4DBGlSb|op%$O!p zc^(Ol{B3N&GvVt-$gWjrDS3Mdni`Iq{tr<^>5`r%iXg53AEL-gOFtI8790 zSrjf=6b=(-22r>qQS8Y5AEL;cxZG)?aGoX#`^0mIXY+y69Yit3;gE%7kc5Or$P?wr zfSjUo85NUV1TpQ!$SBLiD$B%Jlu7<{nZ(Jol^_!wKxHglN6NO}T zA33T*SEF7svKnBihX|_?M_ScD(Z{RN5NahSR-+2yRipK&gS=jiR*-{g#6kZr)ToT> zF*P>FCh4QeaBkp7Z9p2=`lB|80l5xhT7z<^pFFe%tt3O$NK2S%)QCn&Q#D!+<^nS> zBiU+n27uFT)kuf^Hu6R_iuClG8nda>X7%|a{|2!|_k(X4hg9UYy-gU^%?xsCyesH22Oa^;Zn%U3B zLcU72A24!;(Ks(or!T{14SO-lH)E}Ohlb=c(Gb&gEap)*#SB{C(xAwlV~(ZHW( zCLK0S3Z5W^krQ1yq(kh8nr$wPnzzY^0#2LcWE-0&O+9bZILQNbXf=wGlXVF8E##Xz zVAMjE>(N8A!tyWJRu2~@r}XGAo3d)Qzcg^CO%8Lqiru<_Y@1F_HZ}H7&eBOfuSbhe zH&KE$Re99xj?xgji$f%eL$ukr1WBxi-n-==wUMjWBQGkDAJ(ICP7Mn}H$g2nuEztV zzrc0Sve_n|+l|`fR6?IkI;)aSsd{Z(JMGg$dN!a+#ej`#CWQ^?lO?S-t|c%&-VA@! zh4JxT+xU2ot+AEtIRmYp*(a?v-Bqc{ZD*i;s717HM0FSqknzpvJTTY)ZbpUq9X2^q zOadVg5jJ*^pPh-?k%PQ=CaSLLw@IyRtROE`svt-8?KXL%xkFsG1x-O<^i>V$IAFq; z4d}P1om|?8R47O8Yy^o6lEaOt0Se_-R8^K&v);ml+w@yx!KTU9t>_Kxw~%sxD6qvi zhnBB5s99gBYrrCFl+Pl^4}nN5mSjgP^q45=k4M~qiE(m>1GtWow>jip7qv`iZ47Rd zaI#^*Nerw=z#{Fky>Mt^fov&>Y(rlmpSY_D-KbE+TG%w%-U-g^S}U3<_L|UFQx&Ne zHc1=HNJAVoi16lm1W^jtiSC+)BI0LoGG30nVpSZqs1%tN)=fK`#XB!YZ_Y#>a_M#G za#Rp2uLpd9(8;+EpgTl9--mvMlHz&$(Pap#^L~dG!*8m82g-w7r13`dbyY0P^%p?) zsW6u|)6&c(Y3UAgFSo|$m5{y+c@AsgDn z>pnrvqGO<-+y8|wL7qMrB?x@(S{r}cqSVNMmUPhB#tmoE?43eWVRa56sKV9Wx&PmR$>iU z4-mIpJfOt0@%#Z7OB4BYA1;AfD|vn@zGzOXiyjZd*)NSXJBVi*cA*aO@oD&SCF&P@ z=3qSnQ@L<1t^!l}!(99sY8QV!51*W}+?E8k7>$|iU4mD_RVeK? zPT!;|01tUp;{ZQo79U@NYcSa0r{%blyiqCBy+8~v!+l_y;u94ZL~ABrFUMERFejUg z$p%BR!7N^}0x!jQ&~?@KXp#8%DqN0`pM0?zxcjyeEAbq^V^3pA=xH)@HGZF5zD6G1 zi_&63jTa#W5536M)wr4*ScC5e5^vCBCoR9J$Cu0it^CPWtRWw7#j}XG6>ppo zaTH^5ulV&=JcdAzliTn*q{=wBdjBRiK#L7*vzJq)lWsm0qM2Vm?^Cfpa;OQ9f*EYt zjvqr1$Nt-Z{|)N@O*6J5fPhvL26Kp$pS0p9P+V+n!>~~t5p5Q1Mh#I1S2uK)Zg`!h ziQB2ut<&z*jqBFcu|a63^*S~SPYn$_eXy-!d%aZc27DZxbCaX(xE=uDzwP*4x(M&s zZ~?i*!A?Aiq50)5+=Z4$9W)q*O*qfN9Rrtx`gk}7gxe_~EJHV*jR2PI4t!`@Q0mEg z#KlgyO9!pHdhig+iVyW*UIF{1nn64V5KGR$^z)P?scOb#F_z6-D2+)0_pq|-58#dgu>ly0Kh4dehvU6hL5 z@ai;<66A$lcwJfBB*EQ|J?gRDv}+4FFpOE;=^zhZfR~b0j{#LdH<)~a{K}0lMr~q+ z2j9yeH96tOOB&VmH=&XLHGB)q2BCi}XY~hQp_?bF-J#$J)JGlMMm=2IBdY_r148jw z0N-&=kAoWA2x6$$xZ$x|ViCp9A1i`7427K zER*uX1v7uL*w{bO81cKa6GOE_V>*`cj=-&(WLkZWJi(5M)@ZEMb8w;!ET=f4KgaEg z$3k?D(uWO)eJ)(8LhYu*>~(!-9Fip|2CA?gEa zJ*iq)P}pkF6H7800>vqazYF13!~}CJx$9B9Sj1tRN96Cj@qF=;2-Yy51NX%6HgIZR z#qgD|Y5RE`Tdzt0%)J}OtH4*F1il7?L%c49R|6_Np2BMpPyU|5X6(%On;PxBDFb(LN5oq*6jlbv z{aJiABucK$;R@2715n9|@XRptBDn^4j;1Ixe3LiVkP+`3#lN7?%#Gt87(j0BT>Ob5 zlmni1=i#+WlQ~wpM+`f=N;qWex~z*L68S^r?|pPCYi7 z0#(QrtDw_TJrDH}$p)mVQ?B~_6YuTNP}CggpdK@mTh7NVvkf4U5x;D4M)CXeG2|tU zV)jDZNs;oz#kfLz=OTUHz_qmJsApLwo+R)C)Op`;Z~-&TIW4*KBV0#ryBa@&y2S7` z*oa`ybl_UttF+``yGrIh2u85#I^2&?Sj=CKXJK;NemsX#~h>za73j{kz)OX|2#r`~}ipG5INUg{14bv4?v1#$(-FO|w z{yaJNAYMlPbq}78An>^tcPMt`v@F?wu3{mH-V2isiu>=yVN}7xFrXIFNB~l@PVjjU zdy=dbVc|n$M8w8u*)Uh|LQrP?SZ-15I&V-T2=v0qJjvrz2MlJ27r zntI3uNAX}~0eZ-00X=rH=3*7hLaMvS4@V(}a*+Dx@uKqF#9HZmp;R=KN)>zM$yv_> z^f^iXc~DKec=hu*fym9raT%HSAcUm5KE=}{J_+zSU2H#whiPaG+|`g5kK;D%Dzdiu z1h(QSo;;k$@nrrZST8>CBCbPJb|kLDYw3i7a)Zr&)VrStRjiO z;Ezy{48MYH$Uts;1rIJZ<{{tfPKMmILpyV|!|Q0;-Ab0d3Ttd4gRkO?kd6HHRoq%* z$kUvll+9_5#lUg%8f$(cC#~aSK7X$}lJP?}$wZv5;a@_Sbm}#%1%#XPI=&afFi*aT zduzk8UZ!D)bO==2E^B6(N~R4YL%r3+yH3)260dsRbbXuaY@6vjYZ`!_|KB$rQ$od-4+8;n6%8ljfFmizQ7s6a3nf#514?wrkBgaGSK&0!7#DExXZ^97tOi5D+m_9^DN zov;bfNm~FoF-egvOW>=W5V?2>(+|_Uw*-PHzmvEh2XOZ;W!lkv-YEwG6W%{b4wW;S zbG$Hl+U)@lg9kuO>A5{6twhENj0Pm?Xf!45KYYNZ900V__+Uh?N&$e>5U1-R#Sc<7 zTC4(MNcAApi+Bok_1KgEog+)a%b4}^Jf|;&iY<^kmNDA^2|ixN*p|3Xe_2{32kEL{ zc3`iQJoGnQPNI+E8RY2-=C_dBb1!Gc$So_FD)OJ@jEbCE!AN+skhHB}+{}oRC68VM z8|UX%Fcml|EmfKL{R(CeL?q(qDrOhbWSwv%Or0JiMj`1tvDhA`wlM|w@g`Q%CE_Y( zJyHanteKptVhBVr$5qUtb8?io`~=67sMtcW)CujS2m;d2EjDP8r6N@=#p=Y6VRGqeW*zQyl6@xum5;4v zIskeq)XYEOxMlttW*4?OsduR)!Wza1XCJSuVZsWF6Ws5yb<8rdtC~59tx}g|WMD00 z28VzBT83v7c_-UN=GQQ5NvwuhMfz%(rQ|01bQyg*Inf5Y%yO;E}xVS!e=OOleDqjK0dyKSZkS;ZG(1pEW@YV0e?f4zL-wZ1Kio97f|zhDN!ojg9}G1LwsdUuZ1%KV}^5(yK6y|dGdZOW1a6b$a&aI zkn_S7pGGHX)-f9wgp19=AQz?0VS_OIbrrc)$E?>UitpOsMjWJWA<&M%zKm5HI1jxf z)B>0O3~)zVYK4Q(1esL_DA7Xnb<8Tr)OFP{TVTmAu49%DwZWYgIJVY#N8Cw6npGFC z?0B>C7ZWW;Zcw@mmYukBUpaBD3XmQyFv*AVqtX*>fq;w_xZRL}gX|W9^e|mBsnRn` zz)x(^0}Z2OSkE|thP(C5=K0-fDgr;GxMdM^kni*i4;iyiJ#)ddg+3d&&a`J7R5uO# zu1Fm_BBBOn4u!(3T^P-7)9Ez*V%=N0M?f*Z;tg7mb>|uj8Zv93* zn-%+7fEMhrk$2Ak1-YV?xg5Oq+BRkw0+|cinD@Z7hb&M)gktLeT`Y)uJD55IE;buiWwn82 zd$w)fLW`|uZfLaI$m}{sr8Ge*-$wr3!>qyv8@)(hPB!*3`l)JZsYc1K>X@=EyoKwf zcMXyj`Swc4B40#l4ajE8-TKrClB^AX99BnqnAm8*9i47C#R+? za0J7Kh^?PFbGq9i-}a1<%lesB^Sl5G`CJMA6OWJu!xqdO;9q&RDlj}tsa$_p0xs+hURK_Mk`;3{(9C~}aq za3eGup0`Mxuud3{T3tWf5VFvP^$smvT{15|29C`B6PNr|F!$NP1n92HK#xvD! z(HJbSx4n9pX#!YzW0*M?@ObB0j2lLU>$Fz!iL;ou9H1~a+|7K;C~X$V>0R|(g^I-Q zV-}H@?*sI*i4`KFn}#f6`yR~*wXn_Pr>`(3aFfry!n`oO0EY_F zCo#nRjAAjl_f@6^7U=s|nI44uETr=WR80!6GrcQ!SSI3LZPNfm`FHnF~;l_|8e@b0tK2C*ET;iXO{Q zFZs`VOo&{6im41;6^Fq@jy$Wl0iq`UImMOBTw(gviq#C?yiFb!Ip{i(M;~R% z%QeM>E!+U`XG8v6Z82f1754WnngidpLCVez$_nlaoAh*fb_r8e;q&_$-ekb~8&W8h_N2+oc|Z_woF$J!Hor#bV*kix!8hZVP$OL;v#K0+{_bR$rlD$cAs_f#I5m*hdG=vN9fn*`<6*_>6}d2{veNM@ZLn=r zv3aPQ;D=I`>^-blsH&yu^HKQnB|YOj;rd03%`JA3aQR;-83?4l!z zi&36je?$RaK9GlxC_H57sA4gA^o>Uq)lgI(RV*dxql#yj+QY0NJgnt;YABNmVI-eC zuUJiT&nq&3!0>z)MFhAAP+eUh&KDGyEtmj^s)=?d`Ns>2>#!+IacMER<(Ojg9Ao&j zG5g4;#}upASi@Q=(2{eHm9fB(2?$8To&U+{;R4KKibcfwGP8i}Jg(SXHejaFM-MDB z{J3}#sIrxucK{IZ%i{_-@e?afD9)b?@yK=WC`RW%S^k+~GX}KS@r5FV+s&l%OaSxO zzfiQnVcY626%GX;Q0{BR%kynvZi8MwIjlt-{zma40;8+{mqLN2#9iRLM#RSN6fH`y zxAT8c{9J+B#aRmFZ>E5O-ZVqG1^mN1GnCyJ{LqHk$_fP>o!RFrpO^u$$-?DIGIfzF z$(i_?`bJlBD|g2FS_feFTZAU6{m;KE0R~OR@RU?YUQG7uH+gM zT)1$F>(t6=7!CzN$*6frzuX|ks+C#AG}_8Kc#b@Jm2#OlwMKd0JYeilqjD2~;y)Xe zn;9Ucwn@2pHk5v=Qa?ir#Pm{KmcAa-nBjs7jOt3l@q2B(e6pDfL60Gqc&JzT7lvZ& zpz_^WuHvyLymXQChm;&3%cDceJ}9d0fs|X#PUY^2=Ua9vWAI!)tkgl#Hmuwj4Kv{7vq89W;1ZQ1%30GF7+kE;RRU~-tHvm!>(*<@=NYAXy2lTff2l>1XEI9FN_PMN2hQbO zoYSlGrc&LRh(A^Qau0G!>$A$A&ds@MRII8d@2au8pyyRU$t)Ugat?SgL7ND)1t6a# zCiBV zG9@T+5UwvMH(^EArOA?AzfrCw_lzkQiEFM@>ZgD@_Fk>5M7ZE0+s{(25U;#Oc?(_3 zrt6fgP+WAKvKN;At?QJZ(U9haeM*Z0`$25KSH6wIk|8f5b8b>@!JT1p*Fk0>nRl~t z8u8tvB)Ds`<$Ca_ciyb*fK<`ElN0&O_N7%RTePsk|DuJZiA;-*ivBo%xtfj4Hwwa0&6$3 zeixhZvaM#8cd;=S>vpkTXmPOt*N;)NO{_f%X0K=ige9YQFsr6C(HR=Zc{)XfaM^VW MAfWlv^U4VRe}M}_w*UYD delta 17547 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 M1lM}!Q8tMGA3BYxr2qf` diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index dd1ec47c90c838edcfb2385e1f8e8a65c560d54a..753576bc313c4c012b2bb99d14f14c2fd9eda6dc 100644 GIT binary patch delta 465 zcmYjNtxm&G6waUs6l!Q>6OLP;j^^A$S(>JlrdgKCWi+fCFjkUvU@8zOFo=b>z)d4i z2qZ6o;1M|Iv>_AsC*R-s?*092zkapfYvu z+FaSL^?q8-PdWxt#6tsJ#0vsK>Kw~G1fKzgGZ-Qc;m#mABwSy%22GI`3ij5(48vOK zb08|62%SJE$+6sc(oLmQC?XH~O>hDwPdd=ijUf^P`@0ZfD+kNd>4*D@$x)n_C?`JWeZhMNEoiSzmm423^ F*8vBm6S4pR diff --git a/netbox/project-static/src/buttons/index.ts b/netbox/project-static/src/buttons/index.ts index e677ff599..fe2ccaaef 100644 --- a/netbox/project-static/src/buttons/index.ts +++ b/netbox/project-static/src/buttons/index.ts @@ -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(); } diff --git a/netbox/project-static/src/buttons/markdownPreview.ts b/netbox/project-static/src/buttons/markdownPreview.ts new file mode 100644 index 000000000..224b2beab --- /dev/null +++ b/netbox/project-static/src/buttons/markdownPreview.ts @@ -0,0 +1,45 @@ +import { isTruthy } from 'src/util'; + +/** + * interface for htmx configRequest event + */ +declare global { + interface HTMLElementEventMap { + 'htmx:configRequest': CustomEvent<{ + parameters: Record; + headers: Record; + }>; + } +} + +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('.markdown-widget')) { + initMarkdownPreview(markdownWidget); + } +} diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index e486bc7db..37f6c21c4 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -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'; } @@ -416,18 +416,18 @@ nav.search { } } -// Styles for the quicksearch and its clear button; +// Styles for the quicksearch and its clear button; // Overrides input-group styles and adds transition effects .quicksearch { - input[type="search"] { - border-radius: $border-radius !important; + input[type='search'] { + border-radius: $border-radius !important; } button { margin-left: -32px !important; z-index: 100 !important; outline: none !important; - border-radius: $border-radius !important; + border-radius: $border-radius !important; transition: visibility 0s, opacity 0.2s linear; } @@ -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; diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index 183a8e851..ab882fe7e 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -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' ) diff --git a/netbox/utilities/forms/fields/fields.py b/netbox/utilities/forms/fields/fields.py index bb6c3f73b..ee9543452 100644 --- a/netbox/utilities/forms/fields/fields.py +++ b/netbox/utilities/forms/fields/fields.py @@ -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""" diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index 1802306f1..bd828bb8f 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -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): diff --git a/netbox/utilities/templates/form_helpers/render_field.html b/netbox/utilities/templates/form_helpers/render_field.html index ec9ceb09a..85c04df92 100644 --- a/netbox/utilities/templates/form_helpers/render_field.html +++ b/netbox/utilities/templates/form_helpers/render_field.html @@ -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 %}