refactor: modernize frontend with hoover framework components and docs
- Add quick modal, table, service status, and confirmation dialog components - Refactor all pages (certs, dhcp, proxy, etc.) to use new component patterns - Introduce refactor load utility and render guard for consistent UX - Add hoover documentation and update AGENTS.md, architecture, overview
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
* avoid fighting with the main render cycle.
|
||||
*/
|
||||
|
||||
import { esc } from '../helpers.js';
|
||||
import { att_esc } from '../helpers.js';
|
||||
import { esc } from '../helpers.js?v=6';
|
||||
import { att_esc } from '../helpers.js?v=6';
|
||||
import { apiSubmit } from '../api.js?v=6';
|
||||
|
||||
const _modalQueue = [];
|
||||
|
||||
@@ -78,7 +79,8 @@ export function formModal(inner, title, fields, actions) {
|
||||
inner.innerHTML = '<h2 class="modal-title">' + esc(title) + '</h2><div class="modal-body">'
|
||||
+ fields.map(f => {
|
||||
if (f.tag === 'select')
|
||||
return '<div class="form-group"><label>' + esc(f.label) + '</label><select id="' + att_esc(f.id) + '">'
|
||||
return '<div class="form-group"><label>' + esc(f.label) + '</label><select id="' + att_esc(f.id) + '"'
|
||||
+ (f.multiple ? ' multiple' : '') + '>'
|
||||
+ (f.options || []).map(o =>
|
||||
typeof o === 'string'
|
||||
? '<option value="' + att_esc(o) + '">' + esc(o) + '</option>'
|
||||
@@ -101,3 +103,103 @@ export function formModal(inner, title, fields, actions) {
|
||||
if (btn) btn.addEventListener('click', a.handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory that returns a function to open a multi-select modal.
|
||||
*
|
||||
* @param {object} props
|
||||
* @param {string} props.title - Modal title
|
||||
* @param {string} props.url - API POST URL
|
||||
* @param {string[]} props.options - All selectable options
|
||||
* @param {string[]} props.selected - Currently selected values
|
||||
* @param {string} props.fieldKey - JSON key for the field
|
||||
* @param {string} [props.successMsg] - Success toast message
|
||||
* @param {function} [props.reload] - () => Promise, called on success
|
||||
* @returns {function} () => void, calls openModal
|
||||
*/
|
||||
export function MultiSelectModal(props = {}) {
|
||||
return () => {
|
||||
const selectId = 'ms-' + props.fieldKey;
|
||||
openModal((inner) => {
|
||||
formModal(inner, props.title,
|
||||
[{
|
||||
label: props.fieldKey,
|
||||
id: selectId,
|
||||
tag: 'select',
|
||||
multiple: true,
|
||||
options: (props.options || []).map(o => [o, (props.selected || []).includes(o)]),
|
||||
}],
|
||||
[
|
||||
{ label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal() },
|
||||
...apiSubmit({
|
||||
url: props.url,
|
||||
body: () => ({
|
||||
[props.fieldKey]: Array.from(document.getElementById(selectId).selectedOptions)
|
||||
.map(o => o.value),
|
||||
}),
|
||||
successMsg: props.successMsg || 'Updated',
|
||||
reload: props.reload,
|
||||
closeModal: () => closeModal(),
|
||||
}),
|
||||
],
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory that returns a function to open a modal with form fields and apiSubmit.
|
||||
* Accepts an optional `data` argument forwarded to title, fields, submit.url, submit.body resolvers.
|
||||
*
|
||||
* @param {object} props
|
||||
* @param {string|function} props.title - Modal title or (data) => string
|
||||
* @param {object[]|function} props.fields - Form field descriptors or (data) => object[]
|
||||
* @param {object} props.submit - Submit configuration
|
||||
* @param {string|function} props.submit.url - API URL or (data) => string
|
||||
* @param {string} [props.submit.method] - HTTP method (default: 'POST')
|
||||
* @param {function} [props.submit.body] - (data) => object
|
||||
* @param {function} [props.submit.validate] - (body) => string|null
|
||||
* @param {string|function} [props.submit.successMsg] - Toast message or (data) => string
|
||||
* @param {function} [props.reload] - (data) => Promise, called on success with the data argument
|
||||
* @param {function} [props.handler] - Custom submit handler override (bypasses apiSubmit)
|
||||
* @param {string} [props.submitLabel] - Submit button label (default: 'Submit')
|
||||
* @returns {function} (data) => void, calls openModal
|
||||
*/
|
||||
export function QuickModal(props = {}) {
|
||||
return (data) => {
|
||||
const title = typeof props.title === 'function' ? props.title(data) : props.title;
|
||||
const fields = typeof props.fields === 'function' ? props.fields(data) : props.fields;
|
||||
const url = typeof props.submit.url === 'function' ? props.submit.url(data) : props.submit.url;
|
||||
|
||||
openModal((inner) => {
|
||||
let actions;
|
||||
if (props.handler) {
|
||||
actions = [
|
||||
{ label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal() },
|
||||
{
|
||||
label: props.submitLabel || 'Submit',
|
||||
cls: 'btn-primary',
|
||||
action: 's',
|
||||
handler: () => props.handler(data, () => closeModal()),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
actions = [
|
||||
{ label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal() },
|
||||
...apiSubmit({
|
||||
url,
|
||||
method: props.submit.method || 'POST',
|
||||
body: props.submit.body ? () => props.submit.body(data) : undefined,
|
||||
validate: props.submit.validate,
|
||||
successMsg: typeof props.submit.successMsg === 'function'
|
||||
? props.submit.successMsg(data)
|
||||
: (props.submit.successMsg || 'Done'),
|
||||
reload: props.reload ? () => props.reload(data) : undefined,
|
||||
closeModal: () => closeModal(),
|
||||
}),
|
||||
];
|
||||
}
|
||||
formModal(inner, title, fields, actions);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user