Save worktree changes
This commit is contained in:
@@ -0,0 +1,594 @@
|
||||
(function () {
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function formatDashboardDate(value) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
var date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function getClientRowKey(client) {
|
||||
return String(client && client.id ? client.id : client && client.clientId ? client.clientId : '').trim();
|
||||
}
|
||||
|
||||
function getClientDisplayName(client) {
|
||||
if (client && client.client_name) {
|
||||
return String(client.client_name).trim();
|
||||
}
|
||||
var clientId = String(client && client.clientId ? client.clientId : '').trim();
|
||||
if (clientId) {
|
||||
return clientId;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function setButtonVariant(button, classesToRemove, classToAdd) {
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (button.classList) {
|
||||
classesToRemove.forEach(function (className) {
|
||||
button.classList.remove(className);
|
||||
});
|
||||
if (classToAdd) {
|
||||
button.classList.add(classToAdd);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var className = String(button.className || '');
|
||||
classesToRemove.forEach(function (removeClass) {
|
||||
className = className.replace(new RegExp('(^|\\s)' + removeClass + '(?=\\s|$)', 'g'), ' ');
|
||||
});
|
||||
if (classToAdd) {
|
||||
className += ' ' + classToAdd;
|
||||
}
|
||||
button.className = className.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function normalizeDisplayIp(value) {
|
||||
var ip = String(value || '').trim();
|
||||
if (!ip) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (ip.toLowerCase().indexOf('::ffff:') === 0) {
|
||||
return ip.slice(7).trim();
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function initConfirmForms() {
|
||||
document.addEventListener('submit', function (event) {
|
||||
var form = event.target;
|
||||
if (!form || !form.getAttribute) {
|
||||
return;
|
||||
}
|
||||
if (form.hasAttribute && form.hasAttribute('data-async-command')) {
|
||||
return;
|
||||
}
|
||||
var message = form.getAttribute('data-confirm-message');
|
||||
if (message && !window.confirm(message)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function markFormDirty(form) {
|
||||
if (!form || !form.hasAttribute || form.hasAttribute('data-clean-on-load')) {
|
||||
return;
|
||||
}
|
||||
form.dataset.dirty = 'true';
|
||||
}
|
||||
|
||||
function clearFormDirty(form) {
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
form.dataset.dirty = 'false';
|
||||
}
|
||||
|
||||
function isFormDirty(form) {
|
||||
return Boolean(form && form.dataset && form.dataset.dirty === 'true');
|
||||
}
|
||||
|
||||
function initDirtyTracking() {
|
||||
document.addEventListener('input', function (event) {
|
||||
var target = event.target;
|
||||
if (!target || !target.form) {
|
||||
return;
|
||||
}
|
||||
markFormDirty(target.form);
|
||||
}, true);
|
||||
|
||||
document.addEventListener('change', function (event) {
|
||||
var target = event.target;
|
||||
if (!target || !target.form) {
|
||||
return;
|
||||
}
|
||||
markFormDirty(target.form);
|
||||
}, true);
|
||||
}
|
||||
|
||||
function initCancelConfirm() {
|
||||
document.addEventListener('click', function (event) {
|
||||
var cancelTarget = event.target.closest('[data-confirm-unsaved]');
|
||||
if (!cancelTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
var form = cancelTarget.form || cancelTarget.closest('form') || document.querySelector('form[data-dirty="true"]');
|
||||
if (!isFormDirty(form)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var message = cancelTarget.getAttribute('data-confirm-unsaved') || 'You have unsaved changes. Leave this page?';
|
||||
if (!window.confirm(message)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
function initAsyncCommandForms() {
|
||||
document.addEventListener('submit', function (event) {
|
||||
var form = event.target;
|
||||
if (!form || !form.hasAttribute || !form.hasAttribute('data-async-command')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.dataset && form.dataset.busy === 'true') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
var message = form.getAttribute('data-confirm-message');
|
||||
if (message && !window.confirm(message)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
form.dataset.busy = 'true';
|
||||
|
||||
var formData = new FormData(form);
|
||||
var body = new URLSearchParams();
|
||||
formData.forEach(function (value, key) {
|
||||
body.append(key, value);
|
||||
});
|
||||
|
||||
fetch(form.action, {
|
||||
method: (form.method || 'POST').toUpperCase(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json, text/plain, */*'
|
||||
},
|
||||
body: body.toString(),
|
||||
credentials: 'same-origin'
|
||||
}).finally(function () {
|
||||
delete form.dataset.busy;
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
||||
function initAsyncSaveForms() {
|
||||
var refreshSequence = 0;
|
||||
|
||||
function setSaveActionValue(form, value) {
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hiddenInput = form.querySelector('input[type="hidden"][name="save_action"]');
|
||||
if (!hiddenInput) {
|
||||
hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = 'save_action';
|
||||
form.appendChild(hiddenInput);
|
||||
}
|
||||
|
||||
hiddenInput.value = String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function getResponseQueryValue(responseUrl, key) {
|
||||
try {
|
||||
var url = new URL(responseUrl, window.location.href);
|
||||
return String(url.searchParams.get(key) || '').trim();
|
||||
} catch (_error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function rebindRefreshTarget(targetElement) {
|
||||
if (!targetElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.initJsonTogglePanels === 'function') {
|
||||
window.initJsonTogglePanels(targetElement);
|
||||
}
|
||||
if (typeof window.initLocalDateTimes === 'function') {
|
||||
window.initLocalDateTimes(targetElement);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceRefreshTargetFromPage(refreshTargetSelector, sequenceId) {
|
||||
if (sequenceId !== refreshSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(window.location.href, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'text/html, application/xhtml+xml'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Unable to refresh saved data.');
|
||||
}
|
||||
return response.text();
|
||||
}).then(function (text) {
|
||||
if (sequenceId !== refreshSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
var responseDocument = new DOMParser().parseFromString(text || '', 'text/html');
|
||||
var currentTarget = document.querySelector(refreshTargetSelector);
|
||||
var nextTarget = responseDocument.querySelector(refreshTargetSelector);
|
||||
if (!currentTarget || !nextTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentTarget.outerHTML = nextTarget.outerHTML;
|
||||
rebindRefreshTarget(document.querySelector(refreshTargetSelector));
|
||||
}).catch(function (_error) {
|
||||
// Ignore refresh replacement failures and leave the existing content in place.
|
||||
});
|
||||
}
|
||||
|
||||
function watchRefreshTask(refreshStateUrl, refreshTaskId, refreshTargetSelector, sequenceId) {
|
||||
var pollDelayMs = 1000;
|
||||
var maxAttempts = 60;
|
||||
|
||||
function poll(attempt) {
|
||||
if (sequenceId !== refreshSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
var stateUrl;
|
||||
try {
|
||||
stateUrl = new URL(refreshStateUrl, window.location.href);
|
||||
} catch (_error) {
|
||||
return;
|
||||
}
|
||||
stateUrl.searchParams.set('refresh_task_id', refreshTaskId);
|
||||
|
||||
fetch(stateUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json, text/plain, */*'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Unable to check refresh status.');
|
||||
}
|
||||
return response.json();
|
||||
}).then(function (payload) {
|
||||
if (sequenceId !== refreshSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
var status = String(payload && payload.status || '').trim().toLowerCase();
|
||||
if (status === 'queued' || status === 'running') {
|
||||
if (attempt < maxAttempts) {
|
||||
window.setTimeout(function () {
|
||||
poll(attempt + 1);
|
||||
}, pollDelayMs);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
replaceRefreshTargetFromPage(refreshTargetSelector, sequenceId);
|
||||
}).catch(function () {
|
||||
if (attempt < maxAttempts) {
|
||||
window.setTimeout(function () {
|
||||
poll(attempt + 1);
|
||||
}, pollDelayMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
poll(0);
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var target = event.target;
|
||||
if (!target || !target.closest) {
|
||||
return;
|
||||
}
|
||||
|
||||
var button = target.closest('button[name="save_action"]');
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
var form = button.form || button.closest('form');
|
||||
if (!form || !form.hasAttribute || !form.hasAttribute('data-async-save')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSaveActionValue(form, button.value || '');
|
||||
form.dataset.submitterValue = String(button.value || '').trim().toLowerCase();
|
||||
}, true);
|
||||
|
||||
document.addEventListener('submit', function (event) {
|
||||
var form = event.target;
|
||||
if (!form || !form.hasAttribute || !form.hasAttribute('data-async-save')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.dataset && form.dataset.busy === 'true') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
var message = form.getAttribute('data-confirm-message');
|
||||
if (message && !window.confirm(message)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
form.dataset.busy = 'true';
|
||||
|
||||
var hiddenSaveAction = form.querySelector('input[type="hidden"][name="save_action"]');
|
||||
var formData = new FormData(form);
|
||||
var submitterValue = String((hiddenSaveAction && hiddenSaveAction.value) || form.dataset.submitterValue || '').trim().toLowerCase();
|
||||
if (event.submitter && event.submitter.name) {
|
||||
submitterValue = String(event.submitter.value || '').trim().toLowerCase();
|
||||
formData.set(event.submitter.name, event.submitter.value || '');
|
||||
}
|
||||
var hasFileValue = false;
|
||||
formData.forEach(function (value) {
|
||||
if (value && typeof value === 'object' && typeof value.name === 'string') {
|
||||
hasFileValue = true;
|
||||
}
|
||||
});
|
||||
|
||||
var isMultipart = hasFileValue || String(form.enctype || '').toLowerCase() === 'multipart/form-data';
|
||||
var body = isMultipart ? formData : new URLSearchParams();
|
||||
|
||||
if (!isMultipart) {
|
||||
formData.forEach(function (value, key) {
|
||||
body.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
fetch(form.action, {
|
||||
method: (form.method || 'POST').toUpperCase(),
|
||||
headers: Object.assign({
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'text/html, application/json, text/plain, */*'
|
||||
}, isMultipart ? {} : {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
}),
|
||||
body: isMultipart ? body : body.toString(),
|
||||
credentials: 'same-origin'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
return response.text().then(function (text) {
|
||||
var error = new Error(text || 'Unable to save changes.');
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
var actionUrl = '';
|
||||
try {
|
||||
actionUrl = new URL(form.action, window.location.href).pathname;
|
||||
} catch (_error) {
|
||||
actionUrl = String(form.action || '');
|
||||
}
|
||||
var refreshTargetSelector = String(form.getAttribute('data-async-save-refresh-target') || '').trim();
|
||||
var refreshStateUrl = String(form.getAttribute('data-async-save-refresh-state-url') || '').trim();
|
||||
var refreshTaskId = getResponseQueryValue(response.url || '', 'refresh_task_id');
|
||||
var shouldFollowRedirect = Boolean(form.hasAttribute('data-async-save-new-url')) && !/\/\d+(?:\/|$)/.test(actionUrl);
|
||||
if (submitterValue === 'close' || submitterValue === 'new') {
|
||||
var redirectUrl = submitterValue === 'close'
|
||||
? String(form.dataset.asyncSaveCloseUrl || response.url || window.location.href)
|
||||
: String(form.dataset.asyncSaveNewUrl || response.url || window.location.href);
|
||||
window.location.replace(redirectUrl);
|
||||
return;
|
||||
}
|
||||
if (shouldFollowRedirect && response.url) {
|
||||
clearFormDirty(form);
|
||||
window.location.replace(response.url);
|
||||
return;
|
||||
}
|
||||
clearFormDirty(form);
|
||||
return response.text().then(function (text) {
|
||||
var savedMessage = '';
|
||||
var responseDocument = null;
|
||||
try {
|
||||
responseDocument = new DOMParser().parseFromString(text || '', 'text/html');
|
||||
var toastBody = responseDocument.querySelector('.toast-body');
|
||||
if (toastBody && toastBody.textContent) {
|
||||
savedMessage = toastBody.textContent.trim();
|
||||
}
|
||||
} catch (_error) {
|
||||
savedMessage = '';
|
||||
}
|
||||
|
||||
if (refreshTargetSelector && refreshStateUrl && refreshTaskId) {
|
||||
refreshSequence += 1;
|
||||
watchRefreshTask(refreshStateUrl, refreshTaskId, refreshTargetSelector, refreshSequence);
|
||||
} else if (refreshTargetSelector && responseDocument) {
|
||||
var currentTarget = document.querySelector(refreshTargetSelector);
|
||||
var nextTarget = responseDocument.querySelector(refreshTargetSelector);
|
||||
if (currentTarget && nextTarget) {
|
||||
currentTarget.outerHTML = nextTarget.outerHTML;
|
||||
rebindRefreshTarget(document.querySelector(refreshTargetSelector));
|
||||
}
|
||||
}
|
||||
|
||||
showToast(savedMessage || 'Saved.', 'success');
|
||||
});
|
||||
}).catch(function (error) {
|
||||
if (typeof showToast === 'function') {
|
||||
var variant = Number(error && error.status) >= 400 && Number(error && error.status) < 500 ? 'warning' : 'danger';
|
||||
showToast(error.message || 'Unable to save changes.', variant);
|
||||
return;
|
||||
}
|
||||
window.alert(error.message || 'Unable to save changes.');
|
||||
}).finally(function () {
|
||||
delete form.dataset.busy;
|
||||
delete form.dataset.submitterValue;
|
||||
if (hiddenSaveAction) {
|
||||
hiddenSaveAction.value = '';
|
||||
}
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
||||
function initSubmitOnChange() {
|
||||
var fields = document.querySelectorAll('[data-submit-on-change]');
|
||||
Array.prototype.forEach.call(fields, function (field) {
|
||||
field.addEventListener('change', function () {
|
||||
var form = field.form || field.closest('form');
|
||||
if (form) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initJsonTogglePanels(root) {
|
||||
var scope = root && root.querySelectorAll ? root : document;
|
||||
var panels = scope.querySelectorAll('[data-json-toggle-panel]');
|
||||
Array.prototype.forEach.call(panels, function (panel) {
|
||||
var output = panel.querySelector('[data-json-toggle-output]');
|
||||
if (!output) {
|
||||
return;
|
||||
}
|
||||
|
||||
var card = panel.closest ? panel.closest('.card') : null;
|
||||
var button = card ? card.querySelector('[data-json-toggle]') : null;
|
||||
var label = button ? button.querySelector('[data-json-toggle-label]') : null;
|
||||
var sourceNode = panel.querySelector('[data-json-toggle-source]');
|
||||
var rawJson = '';
|
||||
try {
|
||||
rawJson = JSON.parse(String(sourceNode ? sourceNode.textContent : '""'));
|
||||
} catch (_error) {
|
||||
rawJson = '';
|
||||
}
|
||||
|
||||
if (typeof rawJson !== 'string' || !rawJson.trim()) {
|
||||
if (button) {
|
||||
button.classList.add('d-none');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var parsedJson;
|
||||
try {
|
||||
parsedJson = JSON.parse(rawJson);
|
||||
} catch (_error) {
|
||||
if (button) {
|
||||
button.classList.add('d-none');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var compactJson = JSON.stringify(parsedJson);
|
||||
var formattedJson = JSON.stringify(parsedJson, null, 2);
|
||||
var isFormatted = true;
|
||||
|
||||
function syncButtonLabel() {
|
||||
if (!button || !label) {
|
||||
return;
|
||||
}
|
||||
label.textContent = isFormatted
|
||||
? String(button.getAttribute('data-json-toggle-label-compact') || 'Unformat JSON')
|
||||
: String(button.getAttribute('data-json-toggle-label-formatted') || 'Format JSON');
|
||||
button.setAttribute('aria-pressed', isFormatted ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function syncOutput() {
|
||||
output.textContent = isFormatted ? formattedJson : compactJson;
|
||||
syncButtonLabel();
|
||||
}
|
||||
|
||||
output.textContent = formattedJson;
|
||||
syncButtonLabel();
|
||||
|
||||
if (button) {
|
||||
button.addEventListener('click', function () {
|
||||
isFormatted = !isFormatted;
|
||||
syncOutput();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initLocalDateTimes(root) {
|
||||
var scope = root && root.querySelectorAll ? root : document;
|
||||
var elements = scope.querySelectorAll('[data-local-datetime]');
|
||||
Array.prototype.forEach.call(elements, function (element) {
|
||||
var rawValue = String(element.getAttribute('datetime') || element.getAttribute('data-local-datetime') || element.textContent || '').trim();
|
||||
if (!rawValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
var date = new Date(rawValue);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.textContent = formatDashboardDate(date);
|
||||
});
|
||||
}
|
||||
|
||||
if (window.initSortableTables) {
|
||||
window.initSortableTables();
|
||||
}
|
||||
|
||||
initConfirmForms();
|
||||
initDirtyTracking();
|
||||
initCancelConfirm();
|
||||
initAsyncCommandForms();
|
||||
initAsyncSaveForms();
|
||||
initSubmitOnChange();
|
||||
initJsonTogglePanels();
|
||||
initLocalDateTimes();
|
||||
window.initJsonTogglePanels = initJsonTogglePanels;
|
||||
window.initLocalDateTimes = initLocalDateTimes;
|
||||
}());
|
||||
Reference in New Issue
Block a user