feat: syntax highlighting in field description (#33791)

This commit is contained in:
Raffael Meyer 2025-09-03 03:21:34 +02:00 committed by GitHub
parent 08de97b1ce
commit cc82ab19ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 102 additions and 50 deletions

View file

@ -44,16 +44,15 @@ frappe.ui.form.on("Server Script", {
},
setup_help(frm) {
frm.get_field("help_html").html(`
const help_field = frm.get_field("help_html");
help_field.html(`
<h4>DocType Event</h4>
<p>Add logic for standard doctype events like Before Insert, After Submit, etc.</p>
<pre>
<code>
<pre><code class="language-python">
# set property
if "test" in doc.description:
doc.status = 'Closed'
# validate
if "validate" in doc.description:
raise frappe.ValidationError
@ -65,13 +64,11 @@ if doc.allocated_to:
owner = doc.allocated_to,
description = doc.subject
)).insert()
</code>
</pre>
</code></pre>
<h5>Payment processing</h5>
<p>Payment processing events have a special state. See the <a href="https://github.com/frappe/payments/blob/develop/payments/controllers/payment_controller.py">PaymentController in Frappe Payments</a> for details.</p>
<pre>
<code>
<pre><code class="language-python">
# retreive payment session state
ps = doc.flags.payment_session
@ -81,7 +78,10 @@ if ps.is_success:
# custom process return values
doc.flags.payment_session.result = {
"message": "Thank you for your payment",
"action": {"href": "https://shop.example.com", "label": "Return to shop"},
"action": {
"href": "https://shop.example.com",
"label": "Return to shop"
}
}
if ps.is_pre_authorized:
if ps.changed: # could be an idempotent run
@ -92,21 +92,20 @@ if ps.is_processing:
if ps.is_declined:
if ps.changed: # could be an idempotent run
...
</code>
</pre>
</code></pre>
<p>The <i>On Payment Failed</i> (<code>on_payment_failed</code>) event only transports the error message which the controller implementation had extracted from the transaction.</p>
<pre>
<code>
<pre><code class="language-python">
msg = doc.flags.payment_failure_message
doc.my_failure_message_field = msg
</code>
</pre>
</code></pre>
<hr>
<h4>API Call</h4>
<p>Respond to <code>/api/method/&lt;method-name&gt;</code> calls, just like whitelisted methods</p>
<pre><code>
<pre><code class="language-python">
# respond to API
if frappe.form_dict.message == "ping":
@ -119,12 +118,16 @@ else:
<h4>Permission Query</h4>
<p>Add conditions to the where clause of list queries.</p>
<pre><code>
# generate dynamic conditions and set it in the conditions variable
tenant_id = frappe.db.get_value(...)
conditions = f'tenant_id = {tenant_id}'
<p>Generate dynamic conditions and set it in the conditions variable:</p>
# resulting select query
<pre><code class="language-python">
tenant_id = frappe.db.get_value(...) # -> 2
conditions = f'tenant_id = {tenant_id}'
</code></pre>
<p>The resulting select query is:</p>
<pre><code class="language-sql">
select name from \`tabPerson\`
where tenant_id = 2
order by creation desc
@ -135,15 +138,13 @@ order by creation desc
<h4>Workflow Task</h4>
<p>Execute when a particular <a href="/app/workflow-action-master">Workflow Action Master</a> is executed.</p>
<p>Gets the document which the action is being applied on in the <code>doc</code> variable.</p>
<code><pre>
<pre><code class="language-python">
# create a customer with the same name as the given document
customer = frappe.new_doc("Customer")
customer.customer_name = doc.first_name + " " + doc.last_name # we get this from the workflow action
customer.customer_name = doc.first_name + " " + doc.last_name # we get this doc from the workflow action
customer.customer_type = "Company"
c.save()
</code></pre>
`);
customer.save()
</code></pre>`);
frappe.utils.highlight_pre(help_field.$wrapper);
},
});

View file

@ -3,7 +3,9 @@
frappe.ui.form.on("Client Script", {
setup(frm) {
frm.get_field("sample").html(SAMPLE_HTML);
const sample_field = frm.get_field("sample");
sample_field.html(SAMPLE_HTML);
frappe.utils.highlight_pre(sample_field.$wrapper);
},
refresh(frm) {
if (frm.doc.dt && frm.doc.script) {
@ -106,53 +108,50 @@ frappe.ui.form.on('${doctype}', {
const SAMPLE_HTML = `<h3>Client Script Help</h3>
<p>Client Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started</p>
<pre><code>
<pre><code class="language-javascript">
// fetch local_tax_no on selection of customer
// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname);
cur_frm.add_fetch("customer", "local_tax_no', 'local_tax_no');
// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname);
cur_frm.add_fetch("customer", "local_tax_no", "local_tax_no");
// additional validation on dates
frappe.ui.form.on('Task', 'validate', function(frm) {
frappe.ui.form.on("Task", "validate", function(frm) {
if (frm.doc.from_date &lt; get_today()) {
msgprint('You can not select past date in From Date');
msgprint("You can not select past date in From Date");
validated = false;
}
});
// make a field read-only after saving
frappe.ui.form.on('Task', {
frappe.ui.form.on("Task", {
refresh: function(frm) {
// use the __islocal value of doc, to check if the doc is saved or not
frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);
frm.set_df_property("myfield", "read_only", frm.is_new() ? 0 : 1);
}
});
// additional permission check
frappe.ui.form.on('Task', {
frappe.ui.form.on("Task", {
validate: function(frm) {
if(user=='user1@example.com' &amp;&amp; frm.doc.purpose!='Material Receipt') {
msgprint('You are only allowed Material Receipt');
if(user === "user1@example.com" &amp;&amp; frm.doc.purpose !== "Material Receipt") {
msgprint("You are only allowed Material Receipt");
validated = false;
}
}
});
// calculate sales incentive
frappe.ui.form.on('Sales Invoice', {
frappe.ui.form.on("Sales Invoice", {
validate: function(frm) {
// calculate incentives for each person on the deal
total_incentive = 0
$.each(frm.doc.sales_team, function(i, d) {
for (const row of frm.doc.sales_team) {
// calculate incentive
var incentive_percent = 2;
if(frm.doc.base_grand_total &gt; 400) incentive_percent = 4;
// actual incentive
d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;
total_incentive += flt(d.incentives)
row.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;
total_incentive += flt(row.incentives)
});
frm.doc.total_incentive = total_incentive;
}
})
</code></pre>`;

View file

@ -112,7 +112,7 @@ frappe.notification = {
if (frm.doc.channel === "Email") {
template = `<h5>Message Example</h5>
<pre>&lt;h3&gt;Order Overdue&lt;/h3&gt;
<pre><code class="language-xml">&lt;h3&gt;Order Overdue&lt;/h3&gt;
&lt;p&gt;Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.&lt;/p&gt;
@ -127,7 +127,7 @@ Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
&lt;li&gt;Customer: {{ doc.customer }}&lt;/li&gt;
&lt;li&gt;Amount: {{ doc.grand_total }}&lt;/li&gt;
&lt;/ul&gt;
</pre>
</code></pre>
`;
} else if (["Slack", "System Notification", "SMS"].includes(frm.doc.channel)) {
template = `<h5>Message Example</h5>
@ -148,7 +148,11 @@ Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
</pre>`;
}
if (template) {
frm.set_df_property("message_examples", "options", template);
const message_examples_field = frm.get_field("message_examples");
message_examples_field.html(template);
if (frm.doc.channel === "Email") {
frappe.utils.highlight_pre(message_examples_field.$wrapper);
}
}
},
};

View file

@ -206,7 +206,17 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
return;
}
if (this.df.description) {
this.$wrapper.find(".help-box").html(__(this.df.description, null, this.df.parent));
const description = __(this.df.description, null, this.df.parent);
const help_box = this.$wrapper.find(".help-box");
help_box.html(description);
if (description.includes("<code")) {
frappe.require("syntax_highlighting.bundle.js").then(() => {
help_box.find("code").each(function () {
hljs.highlightElement(this);
this.style.display = "inline"; // override hljs's "block" display
});
});
}
this.toggle_description(true);
} else {
this.set_empty_description();

View file

@ -1837,4 +1837,29 @@ Object.assign(frappe.utils, {
}
}
},
/**
* Adds syntax highlighting to all <pre> tags in the given jQuery wrapper.
* Example wrapper:
*
* ```html
* <pre><code class="language-python">
* def add(a, b):
* return a + b
*
* print(add(1, 2))
*
* # Output: 3
* </code></pre>
* ```
*
* @param {jQuery} $wrapper - The jQuery wrapper to add syntax highlighting to.
*/
highlight_pre($wrapper) {
frappe.require("syntax_highlighting.bundle.js").then(() => {
$wrapper.find("pre").each(function () {
hljs.highlightElement(this);
});
});
},
});

View file

@ -0,0 +1,12 @@
import hljs from "highlight.js/lib/core";
import javascript from "highlight.js/lib/languages/javascript";
import python from "highlight.js/lib/languages/python";
import xml from "highlight.js/lib/languages/xml";
import sql from "highlight.js/lib/languages/sql";
hljs.registerLanguage("javascript", javascript);
hljs.registerLanguage("python", python);
hljs.registerLanguage("xml", xml);
hljs.registerLanguage("sql", sql);
window.hljs = hljs;

View file

@ -9,3 +9,4 @@
@import "frappe/public/js/lib/leaflet_easy_button/easy-button.css";
@import "frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.css";
@import "frappe/public/js/lib/leaflet_draw/leaflet.draw.css";
@import "frappe/public/node_modules/highlight.js/styles/tomorrow.css";