Development
This commit is contained in:
parent
59d3d65d18
commit
ac0aa4de22
4 changed files with 98 additions and 113 deletions
|
|
@ -1,7 +1,7 @@
|
|||
from flask import Blueprint, request, redirect, flash, session
|
||||
from auth import require_level
|
||||
from config_utils import (flush_pending_to_queue, flush_selected_to_queue,
|
||||
delete_pending_by_uuids, get_dashboard_pending,
|
||||
from config_utils import (flush_pending_to_queue, get_dashboard_pending,
|
||||
revert_snapshot_to_core,
|
||||
_is_locked, _format_timing, _seconds_until_next_run)
|
||||
|
||||
bp = Blueprint('action_actions', __name__)
|
||||
|
|
@ -36,13 +36,22 @@ def actions_cardpending_applynow():
|
|||
return redirect(_VIEW)
|
||||
|
||||
|
||||
@bp.route('/action/actions_cardpending_revertselected', methods=['POST'])
|
||||
@bp.route('/action/actions_cardhistory_revertselected', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def actions_cardpending_revertselected():
|
||||
def actions_cardhistory_revertselected():
|
||||
selected_uuids = request.form.getlist('selected_uuids')
|
||||
if not selected_uuids:
|
||||
flash('No items selected.', 'info')
|
||||
return redirect(_VIEW)
|
||||
delete_pending_by_uuids(selected_uuids)
|
||||
flash('Selected changes reverted.', 'success')
|
||||
succeeded, failed = 0, 0
|
||||
for uuid in selected_uuids:
|
||||
msg, ok = revert_snapshot_to_core(uuid)
|
||||
if ok:
|
||||
succeeded += 1
|
||||
else:
|
||||
flash(msg, 'error')
|
||||
failed += 1
|
||||
if succeeded:
|
||||
plural = 's' if succeeded != 1 else ''
|
||||
flash(f'{succeeded} change{plural} reverted.', 'success')
|
||||
return redirect(_VIEW)
|
||||
|
|
|
|||
|
|
@ -181,35 +181,6 @@ def flush_pending_to_queue():
|
|||
_trim_if_needed()
|
||||
|
||||
|
||||
def _remove_pending_by_uuids(uuid_set):
|
||||
try:
|
||||
lines = open(DASHBOARD_PENDING).read().splitlines()
|
||||
except Exception:
|
||||
return
|
||||
kept = [l for l in lines if l.strip() and l.split(None, 1)[0] not in uuid_set]
|
||||
with open(DASHBOARD_PENDING, 'w') as f:
|
||||
f.write('\n'.join(kept) + ('\n' if kept else ''))
|
||||
|
||||
|
||||
def flush_selected_to_queue(selected_uuids):
|
||||
if not selected_uuids:
|
||||
return
|
||||
selected_set = set(selected_uuids)
|
||||
items = _read_dashboard_pending()
|
||||
done_set = _load_done_set()
|
||||
existing_ids = {uu for uu, *_ in _read_pending(done_set)}
|
||||
with open(DASHBOARD_QUEUE, 'a') as f:
|
||||
for entry_uuid, entry_ts, entry_cmd, entry_user in items:
|
||||
if entry_uuid in selected_set and entry_uuid not in existing_ids:
|
||||
f.write(f'{entry_uuid} {entry_ts} [{entry_cmd}] ({entry_user})\n')
|
||||
_remove_pending_by_uuids(selected_set)
|
||||
_trim_if_needed()
|
||||
|
||||
|
||||
def delete_pending_by_uuids(selected_uuids):
|
||||
if not selected_uuids:
|
||||
return
|
||||
_remove_pending_by_uuids(set(selected_uuids))
|
||||
|
||||
|
||||
def _queue_pending_command(cmd):
|
||||
|
|
|
|||
|
|
@ -442,7 +442,7 @@ def _blocklist_stats_html(cfg):
|
|||
if not rows:
|
||||
return ''
|
||||
return (
|
||||
'<table class="data-table" style="margin-bottom:1rem">'
|
||||
'<table class="data-table">'
|
||||
'<thead><tr>'
|
||||
'<th class="table-header">Blocklist</th>'
|
||||
'<th class="table-header">Entries</th>'
|
||||
|
|
@ -593,55 +593,46 @@ def collect_tokens():
|
|||
|
||||
pending_items = get_dashboard_pending()
|
||||
if pending_items:
|
||||
# Group by command; each group = one row in the Pending Actions table.
|
||||
from collections import defaultdict
|
||||
groups = defaultdict(list)
|
||||
for _uuid, _ts, cmd, user in pending_items:
|
||||
groups[cmd].append((_uuid, user))
|
||||
rows = ''
|
||||
_tr_onclick = (
|
||||
'onclick="if(event.target.type!==\'checkbox\')'
|
||||
'this.nextElementSibling.hidden=!this.nextElementSibling.hidden"'
|
||||
)
|
||||
for _uuid, ts, _cmd, user in pending_items:
|
||||
snap = load_snapshot_for_uuid(_uuid)
|
||||
dt_str = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M')
|
||||
snap_desc = e(snap['description']) if snap else ''
|
||||
before_val = snap.get('before') if snap else None
|
||||
after_val = snap.get('after') if snap else None
|
||||
snap_id = e(_uuid[:8]) if snap else ''
|
||||
rows += (f'<tr style="cursor:pointer" {_tr_onclick}>'
|
||||
f'<td class="table-cell"><input type="checkbox" name="selected_uuids" value="{e(_uuid)}"/></td>'
|
||||
f'<td class="table-cell">{e(dt_str)}</td>'
|
||||
f'<td class="table-cell">{snap_desc}</td>'
|
||||
f'<td class="table-cell">{_render_snap_val(before_val)}</td>'
|
||||
f'<td class="table-cell">{_render_snap_val(after_val)}</td>'
|
||||
f'<td class="table-cell">{snap_id}</td>'
|
||||
f'<td class="table-cell">{e(user)}</td>'
|
||||
f'</tr>'
|
||||
f'{_snap_expand_row(before_val, after_val, 7)}')
|
||||
select_all = (
|
||||
'<input type="checkbox" '
|
||||
'onchange="document.querySelectorAll(\'[name=selected_uuids]\').forEach(c=>c.checked=this.checked)"/>'
|
||||
)
|
||||
for cmd, entries in groups.items():
|
||||
users = ', '.join(sorted({u for _, u in entries}))
|
||||
required_by_parts = []
|
||||
for _uuid, _ in entries:
|
||||
snap = load_snapshot_for_uuid(_uuid)
|
||||
required_by_parts.append(snap.get('description', _uuid[:8]) if snap else _uuid[:8])
|
||||
required_by = ', '.join(required_by_parts)
|
||||
rows += (f'<tr>'
|
||||
f'<td class="table-cell">{e(cmd)}</td>'
|
||||
f'<td class="table-cell">{e(users)}</td>'
|
||||
f'<td class="table-cell">{e(required_by)}</td>'
|
||||
f'</tr>')
|
||||
pending_html = (
|
||||
'<table class="data-table" style="margin-bottom:1rem">'
|
||||
'<table class="data-table">'
|
||||
'<thead><tr>'
|
||||
f'<th class="table-header">{select_all}</th>'
|
||||
'<th class="table-header">Time</th>'
|
||||
'<th class="table-header">Description</th>'
|
||||
'<th class="table-header">Before</th>'
|
||||
'<th class="table-header">After</th>'
|
||||
'<th class="table-header">Snapshot</th>'
|
||||
'<th class="table-header">Command</th>'
|
||||
'<th class="table-header">User</th>'
|
||||
'<th class="table-header">Required By</th>'
|
||||
'</tr></thead>'
|
||||
f'<tbody>{rows}</tbody>'
|
||||
'</table>'
|
||||
)
|
||||
else:
|
||||
pending_html = '<p class="text-muted">No pending changes.</p>'
|
||||
tokens['PENDING_CHANGES_HTML'] = pending_html
|
||||
pending_html = '<p class="text-muted">No pending actions.</p>'
|
||||
tokens['PENDING_ACTIONS_HTML'] = pending_html
|
||||
tokens['NO_PENDING'] = 'true' if not pending_items else ''
|
||||
|
||||
done_items = get_dashboard_done()
|
||||
if done_items:
|
||||
hist_rows = ''
|
||||
_hist_onclick = 'onclick="this.nextElementSibling.hidden=!this.nextElementSibling.hidden"'
|
||||
_hist_onclick = (
|
||||
'onclick="if(event.target.type!==\'checkbox\')'
|
||||
'this.nextElementSibling.hidden=!this.nextElementSibling.hidden"'
|
||||
)
|
||||
for _uuid, applied_ts in done_items:
|
||||
snap = load_snapshot_for_uuid(_uuid)
|
||||
if applied_ts:
|
||||
|
|
@ -653,7 +644,8 @@ def collect_tokens():
|
|||
after_val = snap.get('after') if snap else None
|
||||
snap_id = e(_uuid[:8]) if snap else ''
|
||||
snap_user = e(snap['user']) if snap else ''
|
||||
hist_rows += (f'<tr style="cursor:pointer" {_hist_onclick}>'
|
||||
hist_rows += (f'<tr class="row-expandable" {_hist_onclick}>'
|
||||
f'<td class="table-cell"><input type="checkbox" name="selected_uuids" value="{e(_uuid)}"/></td>'
|
||||
f'<td class="table-cell">{e(dt_str)}</td>'
|
||||
f'<td class="table-cell">{snap_desc}</td>'
|
||||
f'<td class="table-cell">{_render_snap_val(before_val)}</td>'
|
||||
|
|
@ -661,10 +653,15 @@ def collect_tokens():
|
|||
f'<td class="table-cell">{snap_id}</td>'
|
||||
f'<td class="table-cell">{snap_user}</td>'
|
||||
f'</tr>'
|
||||
f'{_snap_expand_row(before_val, after_val, 6)}')
|
||||
f'{_snap_expand_row(before_val, after_val, 7)}')
|
||||
select_all = (
|
||||
'<input type="checkbox" '
|
||||
'onchange="document.querySelectorAll(\'[name=selected_uuids]\').forEach(c=>c.checked=this.checked)"/>'
|
||||
)
|
||||
history_html = (
|
||||
'<table class="data-table" style="margin-bottom:1rem">'
|
||||
'<table class="data-table">'
|
||||
'<thead><tr>'
|
||||
f'<th class="table-header">{select_all}</th>'
|
||||
'<th class="table-header">Applied</th>'
|
||||
'<th class="table-header">Description</th>'
|
||||
'<th class="table-header">Before</th>'
|
||||
|
|
@ -678,6 +675,7 @@ def collect_tokens():
|
|||
else:
|
||||
history_html = '<p class="text-muted">No change history.</p>'
|
||||
tokens['CHANGE_HISTORY_HTML'] = history_html
|
||||
tokens['NO_HISTORY'] = 'true' if not done_items else ''
|
||||
|
||||
servers = dns.get('upstream_servers', [])
|
||||
tokens['DNS_STRICT_ORDER'] = 'true' if dns.get('strict_order') else 'false'
|
||||
|
|
@ -843,19 +841,16 @@ def _render_snap_val(val):
|
|||
|
||||
def _snap_expand_row(before_val, after_val, colspan):
|
||||
"""Return a hidden <tr> that expands with full before/after content."""
|
||||
pre = ('max-height:200px;overflow-y:auto;white-space:pre-wrap;'
|
||||
'font-size:0.85em;background:#fff;border:1px solid #ddd;'
|
||||
'padding:0.5rem;margin:0.25rem 0')
|
||||
def box(label, val):
|
||||
text = _snap_text(val) if val is not None else ''
|
||||
if isinstance(val, (dict, list)):
|
||||
text = json.dumps(val, indent=2)
|
||||
body = e(text) if text else '<em>(none)</em>'
|
||||
return f'<div style="flex:1;min-width:0"><strong>{label}</strong><pre style="{pre}">{body}</pre></div>'
|
||||
inner = f'<div style="display:flex;gap:1rem">{box("Before", before_val)}{box("After", after_val)}</div>'
|
||||
return (f'<tr hidden>'
|
||||
f'<td colspan="{colspan}" style="padding:0.5rem 1rem;background:#f8f8f8">'
|
||||
f'{inner}</td></tr>')
|
||||
body = e(text) if text else '<em class="snap-expand-none">(none)</em>'
|
||||
return (f'<div class="snap-expand-col">'
|
||||
f'<span class="snap-expand-label">{label}</span>'
|
||||
f'<pre class="snap-expand-pre">{body}</pre></div>')
|
||||
inner = f'<div class="snap-expand-cols">{box("Before", before_val)}{box("After", after_val)}</div>'
|
||||
return f'<tr hidden><td colspan="{colspan}" class="snap-expand-cell">{inner}</td></tr>'
|
||||
|
||||
|
||||
def apply_tokens(text, tokens):
|
||||
|
|
@ -944,7 +939,7 @@ def _render_item(item, tokens, inherited_req=None):
|
|||
formaction = e(apply_tokens(formaction, tokens))
|
||||
return f'<button type="submit" class="btn {e(cls)}" formaction="{formaction}"{disabled}>{text}</button>'
|
||||
if item.get('method', '').lower() == 'post':
|
||||
return (f'<form method="post" action="{action}" style="display:inline">'
|
||||
return (f'<form method="post" action="{action}" class="form-inline">'
|
||||
f'<button type="submit" class="btn {e(cls)}"{disabled}>{text}</button></form>')
|
||||
return f'<a href="{action}" class="btn {e(cls)}">{text}</a>'
|
||||
|
||||
|
|
@ -985,7 +980,7 @@ def _render_item(item, tokens, inherited_req=None):
|
|||
return (
|
||||
f'<div class="{cls}">'
|
||||
f'<div class="stat-card-label">{label}</div>'
|
||||
f'<div style="display:flex;align-items:center;gap:0.5em">'
|
||||
f'<div class="stat-card-value-row">'
|
||||
f'<span class="stat-card-value">{value}</span>'
|
||||
f'<button type="button" class="btn btn-ghost btn-sm"'
|
||||
f' data-reveal-card="{e(reveal_card_id)}">Edit</button>'
|
||||
|
|
@ -996,9 +991,9 @@ def _render_item(item, tokens, inherited_req=None):
|
|||
if edit_action and edit_field:
|
||||
min_attr = f' min="{e(edit_min)}"' if edit_min else ''
|
||||
suffix_html = f'<span>{e(edit_suffix)}</span>' if edit_suffix else ''
|
||||
input_wrap = (f'<div style="display:flex;align-items:center;gap:0.5em">'
|
||||
input_wrap = (f'<div class="stat-card-value-row">'
|
||||
f'<input type="{e(edit_input_type)}" name="{e(edit_field)}" value="{e(edit_raw)}"'
|
||||
f' data-original="{e(edit_raw)}" class="form-input"{min_attr} style="width:5rem"/>'
|
||||
f' data-original="{e(edit_raw)}" class="form-input stat-card-edit-input"{min_attr}/>'
|
||||
f'{suffix_html}</div>')
|
||||
return (
|
||||
f'<div class="{cls} stat-card-editable">'
|
||||
|
|
@ -1007,10 +1002,10 @@ def _render_item(item, tokens, inherited_req=None):
|
|||
f'<span class="stat-card-value">{value}</span>'
|
||||
f'<button type="button" class="btn btn-ghost btn-sm stat-card-edit-btn">Edit</button>'
|
||||
f'</div>'
|
||||
f'<form class="stat-card-edit-form" style="display:none" action="{e(edit_action)}" method="post">'
|
||||
f'<form class="stat-card-edit-form hidden" action="{e(edit_action)}" method="post">'
|
||||
f'<input type="hidden" name="config_hash" value="{e(config_hash())}"/>'
|
||||
f'{input_wrap}'
|
||||
f'<div style="margin-top:0.5em;display:flex;gap:0.5em">'
|
||||
f'<div class="stat-card-edit-actions">'
|
||||
f'<button type="submit" class="btn btn-primary btn-sm" disabled>Save</button>'
|
||||
f'<button type="button" class="btn btn-secondary btn-sm stat-card-cancel-btn">Cancel</button>'
|
||||
f'</div>'
|
||||
|
|
@ -1027,10 +1022,10 @@ def _render_item(item, tokens, inherited_req=None):
|
|||
if t == 'card':
|
||||
label = item.get('label', '')
|
||||
id_attr = f' id="{e(item["id"])}"' if item.get('id') else ''
|
||||
style = ' style="display:none"' if item.get('hidden') else ''
|
||||
cls_hidden = ' hidden' if item.get('hidden') else ''
|
||||
header = f'<div class="card-header"><h2 class="card-title">{e(label)}</h2></div>' if label else ''
|
||||
body = render_items(item.get('items', []), tokens, req)
|
||||
return f'<div class="card"{id_attr}{style}>{header}<div class="card-body">{body}</div></div>'
|
||||
return f'<div class="card{cls_hidden}"{id_attr}>{header}<div class="card-body">{body}</div></div>'
|
||||
|
||||
if t == 'field_status':
|
||||
label = e(item.get('label', ''))
|
||||
|
|
@ -1060,11 +1055,11 @@ def _render_item(item, tokens, inherited_req=None):
|
|||
psel = e(item.get('provider_select', 'provider'))
|
||||
return (
|
||||
f'<div class="credential-fields" data-provider-select="{psel}">'
|
||||
f'<div class="cred-group-token" style="display:none">'
|
||||
f'<div class="cred-group-token hidden">'
|
||||
f'<div class="form-group"><label class="form-label">API Token</label>'
|
||||
f'<input type="text" name="api_token" class="form-input"/></div>'
|
||||
f'</div>'
|
||||
f'<div class="cred-group-noip" style="display:none">'
|
||||
f'<div class="cred-group-noip hidden">'
|
||||
f'<div class="form-group"><label class="form-label">Username</label>'
|
||||
f'<input type="text" name="username" class="form-input"/></div>'
|
||||
f'<div class="form-group"><label class="form-label">Password</label>'
|
||||
|
|
@ -1131,7 +1126,7 @@ def _render_item(item, tokens, inherited_req=None):
|
|||
f'<input type="number" name="{prefix_name}" value="{pf}" min="1" max="30" class="form-input subnet-prefix-input"/>'
|
||||
f'<span class="subnet-dotted">{e(dotted)}</span>'
|
||||
f'</div>'
|
||||
f'<p class="form-hint field-dyn-hint" style="display:none"></p>'
|
||||
f'<p class="form-hint field-dyn-hint hidden"></p>'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
|
|
@ -1228,7 +1223,7 @@ def _render_field(item, tokens):
|
|||
if input_type == 'number':
|
||||
min_attr = f' min="{item["min"]}"' if 'min' in item else ''
|
||||
max_attr = f' max="{item["max"]}"' if 'max' in item else ''
|
||||
dyn_hint_html = '<p class="form-hint field-dyn-hint" style="display:none"></p>'
|
||||
dyn_hint_html = '<p class="form-hint field-dyn-hint hidden"></p>'
|
||||
inp = (f'<input type="number" name="{name}" value="{e(value)}"{min_attr}{max_attr}'
|
||||
f' class="form-input{extra_cls}"{readonly}'
|
||||
f' data-validate="positive_int" />')
|
||||
|
|
@ -1349,7 +1344,7 @@ def _render_field(item, tokens):
|
|||
|
||||
validate = item.get('validate', '')
|
||||
validate_attr = f' data-validate="{e(validate)}"' if validate else ''
|
||||
dyn_hint = '<p class="form-hint field-dyn-hint" style="display:none"></p>' if (item.get('readonly') or item.get('dyn_hint') or validate) else ''
|
||||
dyn_hint = '<p class="form-hint field-dyn-hint hidden"></p>' if (item.get('readonly') or item.get('dyn_hint') or validate) else ''
|
||||
return (f'<div class="form-group"><label class="form-label">{label}</label>'
|
||||
f'<input type="{e(input_type)}" name="{name}" value="{e(value)}"'
|
||||
f' placeholder="{placeholder}" class="form-input{extra_cls}"{readonly}{validate_attr}/>{hint_html}{dyn_hint}</div>')
|
||||
|
|
@ -1484,7 +1479,7 @@ def _render_table(item, tokens, inherited_req=None):
|
|||
if disable_if and row.get(disable_if.get('field')) == disable_if.get('value'):
|
||||
btns += f'<button type="button" class="btn {cls}" disabled>{text}</button>'
|
||||
continue
|
||||
btns += (f'<form method="post" action="{action}" style="display:inline">'
|
||||
btns += (f'<form method="post" action="{action}" class="form-inline">'
|
||||
f'<input type="hidden" name="row_index" value="{idx}"/>'
|
||||
f'<input type="hidden" name="config_hash" value="{e(hash_val)}"/>'
|
||||
f'<button type="submit" class="btn {cls}">{text}</button></form>')
|
||||
|
|
@ -1545,7 +1540,7 @@ def _render_table_cell(value, render_fn, col_class='', field='', row_idx=None,
|
|||
else:
|
||||
label = 'Disabled'; badge_cls = 'badge-disabled'
|
||||
if toggle_action and row_idx is not None and toggle_allowed:
|
||||
inner = (f'<form method="post" action="{e(toggle_action)}" style="display:inline">'
|
||||
inner = (f'<form method="post" action="{e(toggle_action)}" class="form-inline">'
|
||||
f'<input type="hidden" name="row_index" value="{row_idx}"/>'
|
||||
f'<button type="submit" class="btn-badge">'
|
||||
f'<span class="badge {badge_cls}">{label}</span></button></form>')
|
||||
|
|
@ -1749,7 +1744,7 @@ def _render_nav_item(item, active_view, level, in_dropdown=False, inherited_req=
|
|||
is_active = ' active' if map_to and map_to == active_view else ''
|
||||
cls = f'dropdown-item{is_active}' if in_dropdown else f'nav-item{is_active}'
|
||||
if action:
|
||||
return (f'<form method="post" action="/action/{e(action)}" style="display:inline">'
|
||||
return (f'<form method="post" action="/action/{e(action)}" class="form-inline">'
|
||||
f'<button type="submit" class="{cls}">{label}</button></form>')
|
||||
if map_to:
|
||||
return f'<a href="/view/{e(map_to)}" class="{cls}">{label}</a>'
|
||||
|
|
@ -2222,7 +2217,7 @@ document.addEventListener('click', function(e) {
|
|||
var maxAttr = fDef.max !== undefined ? ' max="' + esc(String(fDef.max)) + '"' : '';
|
||||
td.innerHTML = '<input type="number" name="' + field + '" value="' + esc(String(val)) +
|
||||
'"' + minAttr + maxAttr + ' class="form-input inline-edit-input" data-validate="positive_int"/>' +
|
||||
'<p class="form-hint field-dyn-hint" style="display:none"></p>';
|
||||
'<p class="form-hint field-dyn-hint hidden"></p>';
|
||||
if (typeof validateEl === 'function') validateEl(td.querySelector('input'));
|
||||
} else if (inputType === 'textarea') {
|
||||
var textVal;
|
||||
|
|
@ -2234,7 +2229,7 @@ document.addEventListener('click', function(e) {
|
|||
td.innerHTML = buildCredentialsHtml(rowData.provider || 'noip', rowData);
|
||||
} else {
|
||||
var validateAttr = fDef.validate ? ' data-validate="' + esc(fDef.validate) + '"' : '';
|
||||
var hintHtml = fDef.validate ? '<p class="form-hint field-dyn-hint" style="display:none"></p>' : '';
|
||||
var hintHtml = fDef.validate ? '<p class="form-hint field-dyn-hint hidden"></p>' : '';
|
||||
td.innerHTML = '<input type="' + inputType + '" name="' + field +
|
||||
'" value="' + esc(String(val)) + '" class="form-input inline-edit-input"' + validateAttr + '/>' + hintHtml;
|
||||
if (fDef.validate && typeof validateEl === 'function') validateEl(td.querySelector('input'));
|
||||
|
|
|
|||
|
|
@ -603,7 +603,7 @@
|
|||
},
|
||||
{
|
||||
"type": "card",
|
||||
"label": "Pending Changes",
|
||||
"label": "Pending Actions",
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -613,22 +613,15 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "raw_html",
|
||||
"html": "%PENDING_CHANGES_HTML%"
|
||||
"html": "%PENDING_ACTIONS_HTML%"
|
||||
},
|
||||
{
|
||||
"type": "button_row",
|
||||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"formaction": "/action/actions_cardpending_applynow",
|
||||
"text": "Apply Now",
|
||||
"disabled": "%NO_PENDING%"
|
||||
},
|
||||
{
|
||||
"type": "button_secondary",
|
||||
"formaction": "/action/actions_cardpending_revertselected",
|
||||
"text": "Revert Selected",
|
||||
"disabled": "%NO_PENDING%"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -648,7 +641,7 @@
|
|||
"name": "apply_changes_immediately",
|
||||
"input_type": "checkbox",
|
||||
"value": "%GENERAL_APPLY_ON_SAVE%",
|
||||
"hint": "When enabled, saved changes are queued immediately. When disabled, changes accumulate in Pending Changes until you click Apply Now."
|
||||
"hint": "When enabled, saved changes are queued immediately. When disabled, changes accumulate in Pending Actions until you click Apply Now."
|
||||
},
|
||||
{
|
||||
"type": "button_row",
|
||||
|
|
@ -675,8 +668,25 @@
|
|||
"client_requirement": "client_is_administrator+",
|
||||
"items": [
|
||||
{
|
||||
"type": "raw_html",
|
||||
"html": "%CHANGE_HISTORY_HTML%"
|
||||
"type": "form",
|
||||
"action": "/action/actions_cardhistory_revertselected",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
"type": "raw_html",
|
||||
"html": "%CHANGE_HISTORY_HTML%"
|
||||
},
|
||||
{
|
||||
"type": "button_row",
|
||||
"items": [
|
||||
{
|
||||
"type": "button_danger",
|
||||
"text": "Revert Selected",
|
||||
"disabled": "%NO_HISTORY%"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue