Skip to content

Instantly share code, notes, and snippets.

@vr-greycube
Last active April 26, 2025 07:05
Show Gist options
  • Save vr-greycube/b87266e0df159d57f101a02214db9b08 to your computer and use it in GitHub Desktop.
Save vr-greycube/b87266e0df159d57f101a02214db9b08 to your computer and use it in GitHub Desktop.
class StatusStyler {
constructor() {
this.btnTemplate = BUTTON_TEMPLATE
}
getSelector(prefix, field) {
return `${prefix}[data-fieldname="${field}"]`
}
getNextStatus(current) {
const index = STATUS_OPTIONS.indexOf(current);
return (index === -1 || index === STATUS_OPTIONS.length - 1)
? STATUS_OPTIONS[0]
: STATUS_OPTIONS[index + 1];
}
applyStyle($elements, status) {
if (!status) return;
const safeClass = 'status-' + frappe.scrub(status);
const elements = ($elements instanceof jQuery ? $elements.toArray() : Array.isArray($elements) ? $elements : [$elements]);
elements.forEach(el => {
const $el = $(el);
if ($el.length) {
const newClassList = ($el.attr('class') || '')
.split(/\s+/)
.filter(c => !c.startsWith('status-'))
.concat(safeClass)
.join(' ');
$el.attr('class', newClassList);
}
});
}
injectStatusButton($target, click_handler) {
if ($target.find('.float-status-btn').length > 0) {
return $target.find('.float-status-btn').first();
}
const $btn = $(this.btnTemplate);
$target.append($btn);
$btn.on('click', () => {
if (typeof click_handler === 'function') {
click_handler($target)
}
});
return $btn;
}
inject_style() {
if (!document.getElementById('status-styler')) {
const style = document.createElement('style');
style.id = 'status-styler';
style.innerHTML = STATUS_CSS;
document.head.appendChild(style);
}
}
}
class GridRowObserver {
constructor(frm, fields) {
this.frm = frm;
this.fields = fields
this.observer = new MutationObserver(this.handleMutations.bind(this));
$(document).on("form-unload", (e, me) => {
this.disconnect();
});
this.styler = new StatusStyler()
}
handleMutations(mutationsList) {
const styler = this.styler;
const tags = ['input', 'select'];
const selector = this.fields
.flatMap(f => tags.map(tag => styler.getSelector(tag, f)))
.join(', ');
mutationsList.forEach((mutation) => {
const target = mutation.target;
if (
target.nodeType === 1 &&
target.tagName === 'DIV' &&
target.classList.contains('editable-row')
) {
$(target).find(selector).each(function () {
styler.applyStyle($(this), $(this).val());
});
}
});
}
observeGridRow(grid_row) {
const rowEl = grid_row.wrapper?.[0]?.querySelector('.data-row');
if (rowEl) {
this.observer.observe(rowEl, {
attributes: true,
attributeFilter: ['class'],
});
}
}
disconnect() {
if (this.observer) {
this.observer.disconnect();
console.log("MutationObserver disconnected on form unload.");
}
}
}
class StatusFieldEnhancer {
constructor(frm, { grid_fields = [], fieldnames = [] } = {}) {
this.frm = frm
this.grid_fields = grid_fields
this.fieldnames = fieldnames
this.styler = new StatusStyler()
this.gridObserver = new GridRowObserver(frm, fieldnames)
}
init = () => {
this.styler.inject_style()
this.observeGridRows()
}
observeGridRows = () => {
$(this.frm.wrapper).on('grid-row-render', (e, grid_row) => {
if (!this.grid_fields.includes(grid_row.parent_df.fieldname)) return
this.gridObserver.observeGridRow(grid_row)
this.fieldnames
.filter(field => grid_row.columns[field])
.forEach(field => {
const selector = this.styler.getSelector('.col', field)
const $el = grid_row.wrapper.find(selector)
this.styler.applyStyle($el, grid_row.doc[field])
this.styler.injectStatusButton($el, $target => {
const $input = $target.find('input, select').first()
const next_value = this.styler.getNextStatus($input.val())
frappe.model.set_value(grid_row.doc.doctype, grid_row.doc.name, field, next_value).then(() => {
this.styler.applyStyle([$input, $target], next_value)
})
})
})
})
}
}
frappe.ui.form.on('Travel Request', {
onload(frm) {
new StatusFieldEnhancer(frm, {
grid_fields: ['itinerary'],
fieldnames: ['travel_to', 'mode_of_travel']
}).init()
}
})
const STATUS_OPTIONS = [
"To Do",
"Pass",
"Fail Minor",
"Fail Major",
"Undetermined",
]
BUTTON_TEMPLATE = `
<button class="float-status-btn" title="Change Status">
<svg class="icon icon-xs" aria-hidden="true">
<use href="#icon-arrow-right"></use>
</svg>
</button>
`;
const STATUS_CSS = `
.float-status-btn {
position: absolute;
top: 5px;
right: 5px;
background: none;
border: none;
cursor: pointer;
padding: 2px;
z-index: 10;
}
.float-status-btn svg {
fill: #333;
width: 14px;
height: 14px;
}
.float-status-btn:hover svg {
fill: #007bff;
}
.status-to_do {
background-color: #B3E5FC!important
}
.status-pass {
background-color: #DCE775!important
}
.status-fail_minor {
background-color: #FFD54F!important
}
.status-fail_major {
background-color: #EF9A9A!important
}
.status-undetermined {
background-color: #E0E0E0!important
}
`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment