feat(admin): preserve list return target for back and close actions

This commit is contained in:
2026-03-11 23:32:11 +01:00
parent bc9506866f
commit 277168f7e8
24 changed files with 525 additions and 96 deletions

View File

@@ -152,6 +152,10 @@ const readSubmitLockMode = (submitter, form) => normalizeSubmitLock(
const disableSubmitControlsForForm = (form, submitter, options = {}) => {
const controls = findSubmitControlsForForm(form);
controls.forEach((control) => {
if (submitter instanceof HTMLElement && control === submitter) {
// Keep the clicked submitter enabled so its name/value stays part of form submission.
return;
}
control.disabled = true;
});
if (submitter instanceof HTMLElement) {

View File

@@ -1,6 +1,6 @@
import { warnOnce } from './app-dom.js';
import { confirmDialog } from './app-confirm-dialog.js';
import { gridFiltersFromSchema } from '../pages/app-list-utils.js';
import { gridFiltersFromSchema, withCurrentListReturn } from '../pages/app-list-utils.js';
import { initListFilterExperience } from '../pages/app-list-filter-experience.js';
import { buildChipsFromMeta, removeChipFromMetaState, clearMetaState } from '../pages/app-list-filter-state.js';
@@ -294,7 +294,11 @@ export function createServerGrid(options) {
return '';
}
const url = rowDblClick.getUrl(rowData, rowIndex);
return String(url || '').trim();
const normalized = String(url || '').trim();
if (normalized === '') {
return '';
}
return withCurrentListReturn(normalized);
};
const getInput = (input) => {

View File

@@ -1,7 +1,7 @@
import { initStandardListPage } from '../core/app-grid-factory.js';
import { readPageConfig } from '../core/app-page-config.js';
import { warnOnce } from '../core/app-dom.js';
import { getAppBase } from './app-list-utils.js';
import { getAppBase, withCurrentListReturn } from './app-list-utils.js';
const config = readPageConfig('admin-api-audit-index');
if (config) {
@@ -60,7 +60,7 @@ if (config) {
if (!cell.uuid) {
return gridjs.html(label);
}
const url = new URL(`admin/users/edit/${cell.uuid}`, appBase).toString();
const url = withCurrentListReturn(new URL(`admin/users/edit/${cell.uuid}`, appBase).toString());
return gridjs.html(`<a href="${url}">${label}</a>`);
},
},
@@ -75,7 +75,7 @@ if (config) {
if (!cell.uuid) {
return gridjs.html(label);
}
const url = new URL(`admin/tenants/edit/${cell.uuid}`, appBase).toString();
const url = withCurrentListReturn(new URL(`admin/tenants/edit/${cell.uuid}`, appBase).toString());
return gridjs.html(`<a href="${url}">${label}</a>`);
},
},

View File

@@ -1,7 +1,7 @@
import { initStandardListPage } from '../core/app-grid-factory.js';
import { readPageConfig } from '../core/app-page-config.js';
import { warnOnce } from '../core/app-dom.js';
import { getAppBase } from './app-list-utils.js';
import { getAppBase, withCurrentListReturn } from './app-list-utils.js';
const config = readPageConfig('admin-import-audit-index');
if (config) {
@@ -56,7 +56,7 @@ if (config) {
if (!cell.uuid) {
return gridjs.html(label);
}
const url = new URL(`admin/users/edit/${cell.uuid}`, appBase).toString();
const url = withCurrentListReturn(new URL(`admin/users/edit/${cell.uuid}`, appBase).toString());
return gridjs.html(`<a href="${url}">${label}</a>`);
},
},

View File

@@ -1,7 +1,7 @@
import { initStandardListPage } from '../core/app-grid-factory.js';
import { readPageConfig } from '../core/app-page-config.js';
import { warnOnce } from '../core/app-dom.js';
import { getAppBase } from './app-list-utils.js';
import { getAppBase, withCurrentListReturn } from './app-list-utils.js';
const config = readPageConfig('admin-system-audit-index');
if (config) {
@@ -42,7 +42,7 @@ if (config) {
if (!cell.uuid) {
return gridjs.html(label);
}
const url = new URL(`admin/users/edit/${cell.uuid}`, appBase).toString();
const url = withCurrentListReturn(new URL(`admin/users/edit/${cell.uuid}`, appBase).toString());
return gridjs.html(`<a href="${url}">${label}</a>`);
},
},

View File

@@ -8,6 +8,83 @@ export const getAppBase = () => {
return `${window.location.origin}/`;
};
const resolveAppBaseUrl = () => {
try {
return new URL(getAppBase(), `${window.location.origin}/`);
} catch {
return new URL(`${window.location.origin}/`);
}
};
const normalizeBasePathname = (pathname) => {
const trimmed = String(pathname ?? '').trim();
if (trimmed === '' || trimmed === '/') {
return '/';
}
return `/${trimmed.replace(/^\/+|\/+$/g, '')}/`;
};
const toAppRelativePath = (pathname, appBasePathname) => {
const normalizedPath = `/${String(pathname ?? '').replace(/^\/+/, '')}`;
const normalizedBasePath = normalizeBasePathname(appBasePathname);
let relativePath = normalizedPath;
if (normalizedBasePath !== '/' && relativePath.startsWith(normalizedBasePath)) {
relativePath = relativePath.slice(normalizedBasePath.length - 1);
} else if (normalizedBasePath !== '/' && relativePath === normalizedBasePath.slice(0, -1)) {
relativePath = '/';
}
return relativePath.replace(/^\/+/, '');
};
const parseAppUrl = (value, baseUrl = resolveAppBaseUrl()) => {
const raw = String(value ?? '').trim();
if (raw === '') {
return null;
}
try {
return new URL(raw, baseUrl);
} catch {
return null;
}
};
export const normalizeListReturnTarget = (sourceUrl = window.location.href) => {
const appBaseUrl = resolveAppBaseUrl();
const parsed = parseAppUrl(sourceUrl, appBaseUrl);
if (!parsed) {
return '';
}
const relativePath = toAppRelativePath(parsed.pathname, appBaseUrl.pathname);
if (relativePath === '') {
return '';
}
const query = new URLSearchParams(parsed.search);
query.delete('return');
const queryString = query.toString();
return `${relativePath}${queryString ? `?${queryString}` : ''}`;
};
export const withReturnTarget = (targetUrl, returnTarget = '') => {
const appBaseUrl = resolveAppBaseUrl();
const parsedTarget = parseAppUrl(targetUrl, appBaseUrl);
if (!parsedTarget) {
return String(targetUrl ?? '');
}
if (parsedTarget.origin !== window.location.origin) {
return String(targetUrl ?? '');
}
const normalizedReturn = normalizeListReturnTarget(returnTarget);
parsedTarget.searchParams.delete('return');
if (normalizedReturn !== '') {
parsedTarget.searchParams.set('return', normalizedReturn);
}
return parsedTarget.toString();
};
export const withCurrentListReturn = (targetUrl) => withReturnTarget(targetUrl, window.location.href);
export const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
'&': '&amp;',
'<': '&lt;',