Add Create Container UI and Templates (Unraid XML) feature
Lets users create containers from the WebUI (image/name/ports/volumes/ env/network mode/restart policy/privileged) instead of only managing existing ones, and adds a Templates panel to save/reuse those configs as Unraid-Docker-compatible template XML, including browsing and importing the host's own existing Docker Manager templates directly. Also fixes bugs found via live testing along the way: container names with spaces/invalid characters now get a clear client- and server-side error with a suggested fix instead of podman's raw API error, the New Container modal's backdrop no longer renders transparent (was being appended outside the .podman-plugin CSS scope), and modal buttons now have real visual hierarchy (ghost/primary/danger) after Unraid's own site-wide button theme was found to override plain single-class rules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -162,7 +162,7 @@ window.Podman = (function () {
|
||||
'<div class="podman-modal-head"><h3>' + escapeHtml(opts.title) + '</h3></div>' +
|
||||
'<form class="podman-modal-body">' + fieldsHtml + '</form>' +
|
||||
'<div class="podman-modal-actions">' +
|
||||
'<button type="button" class="podman-btn" data-role="cancel">Cancel</button>' +
|
||||
'<button type="button" class="podman-btn podman-btn-ghost" data-role="cancel">Cancel</button>' +
|
||||
'<button type="button" class="podman-btn podman-btn-primary" data-role="submit">' +
|
||||
escapeHtml(opts.submitLabel || 'Create') + '</button>' +
|
||||
'</div></div>';
|
||||
|
||||
@@ -251,11 +251,11 @@
|
||||
function portRowHtml() {
|
||||
return '' +
|
||||
'<div class="podman-row-group-item">' +
|
||||
'<input type="text" class="mono" data-field="hostPort" placeholder="Host port">' +
|
||||
'<input type="text" class="mono podman-input-narrow" data-field="hostPort" placeholder="Host port">' +
|
||||
'<span>→</span>' +
|
||||
'<input type="text" class="mono" data-field="containerPort" placeholder="Container port">' +
|
||||
'<input type="text" class="mono podman-input-narrow" data-field="containerPort" placeholder="Container port">' +
|
||||
'<select data-field="protocol"><option value="tcp">TCP</option><option value="udp">UDP</option></select>' +
|
||||
'<button type="button" class="podman-btn podman-btn-icon" data-remove-row title="Remove">×</button>' +
|
||||
'<button type="button" class="podman-btn podman-btn-icon podman-row-remove-btn" data-remove-row title="Remove">×</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
'<input type="text" class="mono" data-field="source" placeholder="my-volume or /mnt/cache/...">' +
|
||||
'<span>→</span>' +
|
||||
'<input type="text" class="mono" data-field="containerPath" placeholder="/data">' +
|
||||
'<button type="button" class="podman-btn podman-btn-icon" data-remove-row title="Remove">×</button>' +
|
||||
'<button type="button" class="podman-btn podman-btn-icon podman-row-remove-btn" data-remove-row title="Remove">×</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -276,15 +276,20 @@
|
||||
'<input type="text" class="mono" data-field="key" placeholder="KEY">' +
|
||||
'<span>=</span>' +
|
||||
'<input type="text" class="mono" data-field="value" placeholder="value">' +
|
||||
'<button type="button" class="podman-btn podman-btn-icon" data-remove-row title="Remove">×</button>' +
|
||||
'<button type="button" class="podman-btn podman-btn-icon podman-row-remove-btn" data-remove-row title="Remove">×</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function addRow(groupEl, rowHtmlFn) {
|
||||
function addRow(groupEl, rowHtmlFn, values) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = rowHtmlFn();
|
||||
const row = div.firstElementChild;
|
||||
row.querySelector('[data-remove-row]').addEventListener('click', function () { row.remove(); });
|
||||
if (values) {
|
||||
row.querySelectorAll('[data-field]').forEach(function (input) {
|
||||
if (values[input.dataset.field] !== undefined) input.value = values[input.dataset.field];
|
||||
});
|
||||
}
|
||||
groupEl.appendChild(row);
|
||||
}
|
||||
|
||||
@@ -298,7 +303,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
function openCreateContainerModal() {
|
||||
/**
|
||||
* @param {object|null} prefill Optional template data (same shape
|
||||
* ajax/templates.php's "get" action returns) to seed the form with —
|
||||
* used by templates.js's "Use template" action. null/omitted opens a
|
||||
* blank form, same as the toolbar's "+ New Container" button.
|
||||
*/
|
||||
function openCreateContainerModal(prefill) {
|
||||
prefill = prefill || {};
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'podman-modal-backdrop';
|
||||
backdrop.innerHTML = '' +
|
||||
@@ -308,19 +320,20 @@
|
||||
'<div class="podman-modal-field"><label>Image</label>' +
|
||||
'<input type="text" id="cc-image" placeholder="docker.io/library/postgres:16"></div>' +
|
||||
'<div class="podman-modal-field"><label>Name (optional)</label>' +
|
||||
'<input type="text" id="cc-name" placeholder="my-container"></div>' +
|
||||
'<input type="text" id="cc-name" placeholder="my-container">' +
|
||||
'<div class="hint">Letters, digits, ".", "_", "-" only — no spaces.</div></div>' +
|
||||
'<div class="podman-modal-field"><label>Network</label>' +
|
||||
'<select id="cc-network"><option value="bridge">Bridge (default)</option>' +
|
||||
'<option value="host">Host</option><option value="none">None</option></select></div>' +
|
||||
'<div class="podman-modal-field"><label>Port mappings</label>' +
|
||||
'<div class="podman-row-group" id="cc-ports"></div>' +
|
||||
'<button type="button" class="podman-btn" data-add="port">+ Add port</button></div>' +
|
||||
'<button type="button" class="podman-btn podman-btn-ghost" data-add="port">+ Add port</button></div>' +
|
||||
'<div class="podman-modal-field"><label>Volumes</label>' +
|
||||
'<div class="podman-row-group" id="cc-volumes"></div>' +
|
||||
'<button type="button" class="podman-btn" data-add="volume">+ Add volume</button></div>' +
|
||||
'<button type="button" class="podman-btn podman-btn-ghost" data-add="volume">+ Add volume</button></div>' +
|
||||
'<div class="podman-modal-field"><label>Environment variables</label>' +
|
||||
'<div class="podman-row-group" id="cc-env"></div>' +
|
||||
'<button type="button" class="podman-btn" data-add="env">+ Add variable</button></div>' +
|
||||
'<button type="button" class="podman-btn podman-btn-ghost" data-add="env">+ Add variable</button></div>' +
|
||||
'<div class="podman-modal-field"><label>Restart policy</label>' +
|
||||
'<select id="cc-restart"><option value="no">No</option><option value="on-failure">On failure</option>' +
|
||||
'<option value="always">Always</option><option value="unless-stopped">Unless stopped</option></select></div>' +
|
||||
@@ -328,20 +341,35 @@
|
||||
'<input type="checkbox" id="cc-privileged"> Privileged</label></div>' +
|
||||
'<div class="podman-modal-field podman-modal-checkbox"><label>' +
|
||||
'<input type="checkbox" id="cc-start" checked> Start after create</label></div>' +
|
||||
'<div class="podman-modal-field podman-modal-checkbox"><label>' +
|
||||
'<input type="checkbox" id="cc-save-template"> Save as template</label></div>' +
|
||||
'<div class="podman-modal-field" id="cc-template-fields" style="display:none;">' +
|
||||
'<label>Template name</label><input type="text" id="cc-template-name" placeholder="my-template">' +
|
||||
'<label style="margin-top:10px;">Icon URL (optional)</label><input type="text" id="cc-template-icon" placeholder="https://...">' +
|
||||
'<label style="margin-top:10px;">Category (optional)</label><input type="text" id="cc-template-category" placeholder="Databases:">' +
|
||||
'<label style="margin-top:10px;">Description (optional)</label><input type="text" id="cc-template-overview" placeholder="What this template runs">' +
|
||||
'</div>' +
|
||||
'</form>' +
|
||||
'<div class="podman-modal-actions">' +
|
||||
'<button type="button" class="podman-btn" data-role="cancel">Cancel</button>' +
|
||||
'<button type="button" class="podman-btn podman-btn-ghost" data-role="cancel">Cancel</button>' +
|
||||
'<button type="button" class="podman-btn podman-btn-primary" data-role="submit">Create</button>' +
|
||||
'</div></div>';
|
||||
|
||||
(document.querySelector('.podman-plugin') || document.body).appendChild(backdrop);
|
||||
|
||||
if (prefill.image) backdrop.querySelector('#cc-image').value = prefill.image;
|
||||
if (prefill.networkMode) backdrop.querySelector('#cc-network').value = prefill.networkMode;
|
||||
if (prefill.privileged) backdrop.querySelector('#cc-privileged').checked = true;
|
||||
|
||||
const portsGroup = backdrop.querySelector('#cc-ports');
|
||||
const volumesGroup = backdrop.querySelector('#cc-volumes');
|
||||
const envGroup = backdrop.querySelector('#cc-env');
|
||||
addRow(portsGroup, portRowHtml);
|
||||
addRow(volumesGroup, volumeRowHtml);
|
||||
addRow(envGroup, envRowHtml);
|
||||
// A template may carry zero, one, or several rows of each kind — always
|
||||
// leave at least one (blank) row so the user has somewhere to type,
|
||||
// matching the blank-form behavior.
|
||||
(prefill.ports && prefill.ports.length ? prefill.ports : [{}]).forEach(function (row) { addRow(portsGroup, portRowHtml, row); });
|
||||
(prefill.volumes && prefill.volumes.length ? prefill.volumes : [{}]).forEach(function (row) { addRow(volumesGroup, volumeRowHtml, row); });
|
||||
(prefill.env && prefill.env.length ? prefill.env : [{}]).forEach(function (row) { addRow(envGroup, envRowHtml, row); });
|
||||
|
||||
backdrop.querySelector('[data-add="port"]').addEventListener('click', function () { addRow(portsGroup, portRowHtml); });
|
||||
backdrop.querySelector('[data-add="volume"]').addEventListener('click', function () { addRow(volumesGroup, volumeRowHtml); });
|
||||
@@ -362,6 +390,10 @@
|
||||
|
||||
backdrop.querySelector('#cc-image').focus();
|
||||
|
||||
backdrop.querySelector('#cc-save-template').addEventListener('change', function (e) {
|
||||
backdrop.querySelector('#cc-template-fields').style.display = e.target.checked ? '' : 'none';
|
||||
});
|
||||
|
||||
function close() { backdrop.remove(); }
|
||||
|
||||
function showError(message) {
|
||||
@@ -380,22 +412,58 @@
|
||||
showError('"Image" is required.');
|
||||
return;
|
||||
}
|
||||
const name = backdrop.querySelector('#cc-name').value.trim();
|
||||
// Same character set podman itself enforces — checked here too so
|
||||
// a typo (most commonly a space, e.g. copying a template's display
|
||||
// name straight in) gets caught before a round trip to the server.
|
||||
if (name && !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name)) {
|
||||
showError('"Name" can only contain letters, digits, ".", "_", "-" — no spaces. Try "' + name.replace(/[^a-zA-Z0-9_.-]+/g, '-') + '" instead.');
|
||||
return;
|
||||
}
|
||||
const ports = readRows(portsGroup).filter(function (r) { return r.hostPort && r.containerPort; });
|
||||
const volumes = readRows(volumesGroup).filter(function (r) { return r.source && r.containerPath; });
|
||||
const env = readRows(envGroup).filter(function (r) { return r.key; });
|
||||
|
||||
const saveAsTemplate = backdrop.querySelector('#cc-save-template').checked;
|
||||
const templateName = backdrop.querySelector('#cc-template-name').value.trim();
|
||||
if (saveAsTemplate && !templateName) {
|
||||
showError('"Template name" is required when "Save as template" is checked.');
|
||||
return;
|
||||
}
|
||||
|
||||
const networkMode = backdrop.querySelector('#cc-network').value;
|
||||
const privileged = backdrop.querySelector('#cc-privileged').checked;
|
||||
|
||||
const submitBtn = backdrop.querySelector('[data-role="submit"]');
|
||||
submitBtn.disabled = true;
|
||||
P.post('containers', 'create', {
|
||||
image: image,
|
||||
name: backdrop.querySelector('#cc-name').value.trim(),
|
||||
networkMode: backdrop.querySelector('#cc-network').value,
|
||||
networkMode: networkMode,
|
||||
ports: ports,
|
||||
volumes: volumes,
|
||||
env: env,
|
||||
restartPolicy: backdrop.querySelector('#cc-restart').value,
|
||||
privileged: backdrop.querySelector('#cc-privileged').checked,
|
||||
privileged: privileged,
|
||||
startAfterCreate: backdrop.querySelector('#cc-start').checked,
|
||||
}).then(function () {
|
||||
// Best-effort: a template-save failure shouldn't undo or block
|
||||
// the container that was just successfully created.
|
||||
if (!saveAsTemplate) return null;
|
||||
return P.post('templates', 'save', {
|
||||
name: templateName,
|
||||
image: image,
|
||||
networkMode: networkMode,
|
||||
privileged: privileged,
|
||||
ports: ports,
|
||||
volumes: volumes,
|
||||
env: env,
|
||||
icon: backdrop.querySelector('#cc-template-icon').value.trim(),
|
||||
category: backdrop.querySelector('#cc-template-category').value.trim(),
|
||||
overview: backdrop.querySelector('#cc-template-overview').value.trim(),
|
||||
}).catch(function (err) {
|
||||
alert('Container created, but saving the template failed: ' + err.message);
|
||||
});
|
||||
}).then(function () {
|
||||
close();
|
||||
return load();
|
||||
@@ -471,5 +539,10 @@
|
||||
return load();
|
||||
}
|
||||
|
||||
// Exposed for templates.js's "Use template" action, which needs to open
|
||||
// this same modal pre-filled — templates.js loads after containers.js
|
||||
// (see Podman.page's script list), so this is already set by then.
|
||||
P.openCreateContainerModal = openCreateContainerModal;
|
||||
|
||||
P.registerPanel('containers', { init: init, refresh: load });
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* javascript/templates.js
|
||||
*
|
||||
* Templates panel: reusable container configs saved as XML (Unraid
|
||||
* Docker-template-compatible schema — see ajax/templates.php's header
|
||||
* comment for why). "Use template" hands off to containers.js's Create
|
||||
* Container modal, pre-filled; templates are themselves created from
|
||||
* that same modal's "Save as template" checkbox, not from here.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
const P = window.Podman;
|
||||
let allTemplates = [];
|
||||
|
||||
function iconHtml(t) {
|
||||
if (t.icon) {
|
||||
return '<img class="podman-template-icon" src="' + P.escapeHtml(t.icon) + '" alt="" loading="lazy" ' +
|
||||
'onerror="this.replaceWith(Object.assign(document.createElement(\'div\'),{className:\'podman-template-icon podman-template-icon-fallback\',textContent:\'' +
|
||||
P.escapeHtml(t.name.slice(0, 1).toUpperCase()) + '\'}))">';
|
||||
}
|
||||
return '<div class="podman-template-icon podman-template-icon-fallback">' + P.escapeHtml(t.name.slice(0, 1).toUpperCase()) + '</div>';
|
||||
}
|
||||
|
||||
function cardHtml(t) {
|
||||
const overview = t.overview && t.overview.length > 110 ? t.overview.slice(0, 107) + '…' : (t.overview || '');
|
||||
return '' +
|
||||
'<div class="podman-template-card" data-name="' + P.escapeHtml(t.name) + '">' +
|
||||
iconHtml(t) +
|
||||
'<div class="podman-template-body">' +
|
||||
'<div class="podman-template-name">' + P.escapeHtml(t.name) + '</div>' +
|
||||
'<div class="podman-row-sub mono">' + P.escapeHtml(t.image) + '</div>' +
|
||||
(t.category ? '<span class="podman-badge">' + P.escapeHtml(t.category) + '</span>' : '') +
|
||||
(overview ? '<div class="podman-template-overview">' + P.escapeHtml(overview) + '</div>' : '') +
|
||||
'</div>' +
|
||||
'<div class="podman-template-actions">' +
|
||||
'<button class="podman-btn podman-btn-primary" data-action="use">Use</button>' +
|
||||
'<button class="podman-btn podman-btn-ghost" data-action="export">Export</button>' +
|
||||
'<button class="podman-btn podman-btn-ghost podman-btn-danger" data-action="delete">Delete</button>' +
|
||||
'</div></div>';
|
||||
}
|
||||
|
||||
function render() {
|
||||
const grid = P.el('templates-grid');
|
||||
grid.innerHTML = allTemplates.length
|
||||
? allTemplates.map(cardHtml).join('')
|
||||
: '<div class="podman-empty-note">No templates yet — save one from the "New Container" form, or import an XML template.</div>';
|
||||
}
|
||||
|
||||
function load() {
|
||||
const container = P.el('podman-panel-templates');
|
||||
if (!P.el('templates-grid')) {
|
||||
container.innerHTML = '' +
|
||||
'<div class="podman-card">' +
|
||||
'<div class="podman-toolbar">' +
|
||||
'<strong style="flex:1;">Reusable container configs</strong>' +
|
||||
'<button class="podman-btn podman-btn-ghost" id="templates-import-btn">⬆ Import Template</button>' +
|
||||
'</div>' +
|
||||
'<div class="podman-template-grid" id="templates-grid"></div>' +
|
||||
'</div>';
|
||||
P.el('templates-import-btn').addEventListener('click', openImportModal);
|
||||
P.el('templates-grid').addEventListener('click', handleCardClick);
|
||||
}
|
||||
return P.get('templates', 'list').then(function (data) {
|
||||
allTemplates = data;
|
||||
render();
|
||||
}).catch(function (err) {
|
||||
P.el('templates-grid').innerHTML = '<div class="podman-error">' + P.escapeHtml(err.message) + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function handleCardClick(e) {
|
||||
const btn = e.target.closest('button[data-action]');
|
||||
if (!btn) return;
|
||||
const name = btn.closest('.podman-template-card').dataset.name;
|
||||
|
||||
if (btn.dataset.action === 'use') {
|
||||
btn.disabled = true;
|
||||
P.get('templates', 'get', { name: name }).then(function (config) {
|
||||
btn.disabled = false;
|
||||
P.openCreateContainerModal(config);
|
||||
}).catch(function (err) {
|
||||
btn.disabled = false;
|
||||
alert('Could not load template: ' + err.message);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (btn.dataset.action === 'export') {
|
||||
btn.disabled = true;
|
||||
P.get('templates', 'export', { name: name }).then(function (data) {
|
||||
btn.disabled = false;
|
||||
const blob = new Blob([data.xml], { type: 'application/xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = name + '.xml';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}).catch(function (err) {
|
||||
btn.disabled = false;
|
||||
alert('Export failed: ' + err.message);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (btn.dataset.action === 'delete') {
|
||||
if (!confirm('Delete template "' + name + '"? This does not affect any running containers.')) return;
|
||||
btn.disabled = true;
|
||||
P.post('templates', 'remove', { name: name }).then(load).catch(function (err) {
|
||||
btn.disabled = false;
|
||||
alert('Delete failed: ' + err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openImportModal() {
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'podman-modal-backdrop';
|
||||
backdrop.innerHTML = '' +
|
||||
'<div class="podman-modal podman-modal-wide" role="dialog" aria-modal="true">' +
|
||||
'<div class="podman-modal-head"><h3>Import Template</h3></div>' +
|
||||
'<form class="podman-modal-body">' +
|
||||
'<div class="podman-modal-field"><label>Your existing Docker templates</label>' +
|
||||
'<input type="text" id="ti-local-search" placeholder="Search by name…">' +
|
||||
'<div class="podman-local-template-list" id="ti-local-list"><div class="podman-empty-note">Loading…</div></div>' +
|
||||
'</div>' +
|
||||
'<div class="podman-modal-field"><label>Or paste XML directly</label>' +
|
||||
'<textarea id="ti-xml" rows="8" class="mono" placeholder="This plugin\'s own export format, or an Unraid/Community Applications Docker template." style="width:100%; resize:vertical;"></textarea></div>' +
|
||||
'</form>' +
|
||||
'<div class="podman-modal-actions">' +
|
||||
'<button type="button" class="podman-btn podman-btn-ghost" data-role="cancel">Cancel</button>' +
|
||||
'<button type="button" class="podman-btn podman-btn-primary" data-role="submit">Import pasted XML</button>' +
|
||||
'</div></div>';
|
||||
|
||||
(document.querySelector('.podman-plugin') || document.body).appendChild(backdrop);
|
||||
|
||||
let localTemplates = [];
|
||||
function renderLocalList(filter) {
|
||||
const list = backdrop.querySelector('#ti-local-list');
|
||||
const visible = filter
|
||||
? localTemplates.filter(function (t) { return t.name.toLowerCase().indexOf(filter) !== -1; })
|
||||
: localTemplates;
|
||||
if (!visible.length) {
|
||||
list.innerHTML = '<div class="podman-empty-note">' + (localTemplates.length ? 'No match.' : 'None found.') + '</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = visible.map(function (t) {
|
||||
return '<div class="podman-local-template-item" data-file="' + P.escapeHtml(t.file) + '">' +
|
||||
'<span class="podman-local-template-name">' + P.escapeHtml(t.name) + '</span>' +
|
||||
'<span class="podman-row-sub mono">' + P.escapeHtml(t.image) + '</span>' +
|
||||
'<button type="button" class="podman-btn podman-btn-ghost" data-action="import-local">Import</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
P.get('templates', 'list_local').then(function (data) {
|
||||
localTemplates = data;
|
||||
renderLocalList('');
|
||||
}).catch(function () {
|
||||
backdrop.querySelector('#ti-local-list').innerHTML = '<div class="podman-empty-note">Could not read local templates.</div>';
|
||||
});
|
||||
backdrop.querySelector('#ti-local-search').addEventListener('input', function (e) {
|
||||
renderLocalList(e.target.value.trim().toLowerCase());
|
||||
});
|
||||
backdrop.querySelector('#ti-local-list').addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('[data-action="import-local"]');
|
||||
if (!btn) return;
|
||||
const file = btn.closest('.podman-local-template-item').dataset.file;
|
||||
btn.disabled = true;
|
||||
P.post('templates', 'import_local', { file: file }).then(function () {
|
||||
close();
|
||||
return load();
|
||||
}).catch(function (err) {
|
||||
btn.disabled = false;
|
||||
showError(err.message);
|
||||
});
|
||||
});
|
||||
|
||||
backdrop.querySelector('#ti-xml').focus();
|
||||
|
||||
function close() { backdrop.remove(); }
|
||||
|
||||
function showError(message) {
|
||||
let box = backdrop.querySelector('.podman-modal-error');
|
||||
if (!box) {
|
||||
box = document.createElement('div');
|
||||
box.className = 'podman-modal-error';
|
||||
backdrop.querySelector('.podman-modal-body').appendChild(box);
|
||||
}
|
||||
box.textContent = message;
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const xml = backdrop.querySelector('#ti-xml').value.trim();
|
||||
if (!xml) {
|
||||
showError('Paste a template XML first.');
|
||||
return;
|
||||
}
|
||||
const submitBtn = backdrop.querySelector('[data-role="submit"]');
|
||||
submitBtn.disabled = true;
|
||||
P.post('templates', 'import', { xml: xml }).then(function () {
|
||||
close();
|
||||
return load();
|
||||
}).catch(function (err) {
|
||||
submitBtn.disabled = false;
|
||||
showError(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
backdrop.querySelector('[data-role="cancel"]').addEventListener('click', close);
|
||||
backdrop.querySelector('[data-role="submit"]').addEventListener('click', submit);
|
||||
backdrop.querySelector('form').addEventListener('submit', function (e) { e.preventDefault(); submit(); });
|
||||
backdrop.addEventListener('click', function (e) { if (e.target === backdrop) close(); });
|
||||
document.addEventListener('keydown', function onKey(e) {
|
||||
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
|
||||
});
|
||||
}
|
||||
|
||||
P.registerPanel('templates', { init: load, refresh: load });
|
||||
})();
|
||||
Reference in New Issue
Block a user