listen ansichten verbessert

This commit is contained in:
2026-03-05 11:17:42 +01:00
parent 4b31fc7664
commit c5f657c8c8
133 changed files with 2806 additions and 636 deletions

View File

@@ -57,6 +57,20 @@
font-size: 11px !important;
}
.app-list-toolbar .app-grid-page-size select {
min-width: 84px;
}
.app-list-toolbar .app-grid-page-size {
margin-inline-start: auto !important;
}
@media (max-width: 768px) {
.app-list-toolbar .app-grid-page-size {
margin-inline-start: 0 !important;
}
}
.app-list-toolbar
.multi-select
.multi-select-header

View File

@@ -135,14 +135,29 @@
}
.grid-avatar {
width: 36px;
height: 36px;
border-radius: 999px;
--grid-avatar-width: 36px;
--grid-avatar-height: 36px;
--grid-avatar-radius: 999px;
--grid-avatar-fit: cover;
--grid-avatar-padding: 0;
width: var(--grid-avatar-width);
height: var(--grid-avatar-height);
border-radius: var(--grid-avatar-radius);
border: 1px solid var(--app-muted-border-color);
background: var(--app-card-background-color);
object-fit: cover;
object-fit: var(--grid-avatar-fit);
padding: var(--grid-avatar-padding);
display: inline-flex;
flex-shrink: 0;
box-sizing: border-box;
}
.grid-avatar-tenant {
--grid-avatar-width: 48px;
--grid-avatar-height: 28px;
--grid-avatar-radius: var(--app-border-radius);
--grid-avatar-fit: contain;
--grid-avatar-padding: 2px;
}
.grid-avatar-placeholder {
@@ -154,6 +169,10 @@
background: var(--app-background-color);
}
.grid-avatar-tenant.grid-avatar-placeholder {
font-size: 10px;
}
.grid-name-cell {
display: inline-flex;
align-items: center;
@@ -198,7 +217,25 @@
font-size: small;
}
tr.gridjs-tr {
.gridjs-container .grid-actions {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.gridjs-container .grid-actions-row-open {
justify-content: flex-end;
width: 100%;
}
.gridjs-container .grid-actions .app-grid-row-open {
padding: 4px 10px;
font-size: 12px;
line-height: 1.2;
white-space: nowrap;
}
tr.gridjs-tr[data-row-open-url] {
cursor: pointer;
}
@@ -206,6 +243,19 @@
background: var(--app-table-row-stripped-background-color);
}
tr.gridjs-tr[data-row-open-url]:focus-visible td {
background: var(--app-table-row-stripped-background-color);
box-shadow: inset 0 0 0 var(--app-outline-width) var(--app-primary-focus);
}
tr.gridjs-tr[data-row-open-url]:focus-visible {
outline: none;
}
.gridjs-container table td .grid-actions .app-grid-row-open:focus-visible {
box-shadow: 0 0 0 var(--app-outline-width) var(--app-primary-focus);
}
html[data-theme="dark"] button.gridjs-sort,
html[data-theme="dark"] button.gridjs-sort-neutral {
filter: invert(1);

View File

@@ -3,6 +3,55 @@ import { gridFiltersFromSchema } 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';
const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
const PAGE_SIZE_STORAGE_PREFIX = 'grid:page-size';
const parsePositiveInt = (value) => {
const parsed = Number.parseInt(String(value ?? ''), 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
return parsed;
};
const normalizePageSizeOptions = (options) => {
const source = Array.isArray(options) && options.length
? options
: DEFAULT_PAGE_SIZE_OPTIONS;
const normalized = [];
source.forEach((option) => {
const parsed = parsePositiveInt(option);
if (parsed !== null && !normalized.includes(parsed)) {
normalized.push(parsed);
}
});
return normalized.length ? normalized : [...DEFAULT_PAGE_SIZE_OPTIONS];
};
const safeStorageGet = (key) => {
const storageKey = String(key || '').trim();
if (storageKey === '') {
return null;
}
try {
return window.localStorage.getItem(storageKey);
} catch {
return null;
}
};
const safeStorageSet = (key, value) => {
const storageKey = String(key || '').trim();
if (storageKey === '') {
return;
}
try {
window.localStorage.setItem(storageKey, value);
} catch {
// localStorage can be unavailable; fail safe.
}
};
export function createServerGrid(options) {
const {
container,
@@ -22,8 +71,10 @@ export function createServerGrid(options) {
gridjs,
rowDblClick = null,
actions = null,
rowInteraction = null,
rowDataset = null,
selection = null
selection = null,
pageSize = null
} = options || {};
const normalizedFilterBindingMode = String(filterBindingMode || 'live').trim().toLowerCase() === 'manual'
@@ -47,14 +98,18 @@ export function createServerGrid(options) {
pageParam: 'page',
sortOrderParam: 'order',
sortDirParam: 'dir',
limitParam: 'limit',
cleanFirstPage: true,
cleanDefaultLimit: true,
syncPage: true,
syncSort: true,
syncLimit: true,
...(urlState || {})
};
const pageParam = urlStateConfig.pageParam || 'page';
const sortOrderParam = urlStateConfig.sortOrderParam || 'order';
const sortDirParam = urlStateConfig.sortDirParam || 'dir';
const limitParam = urlStateConfig.limitParam || 'limit';
const resolveUrl = (path) => new URL(path, appBase).toString();
const addParam = (url, key, value) => {
const nextUrl = new URL(url);
@@ -84,6 +139,68 @@ export function createServerGrid(options) {
const normalized = String(rawValue ?? '').trim().toLowerCase();
return normalized === 'asc' || normalized === 'desc' ? normalized : '';
};
const pageSizeConfig = pageSize && typeof pageSize === 'object'
? pageSize
: {};
const pageSizeEnabled = pageSizeConfig.enabled !== false;
const pageSizeOptions = normalizePageSizeOptions(pageSizeConfig.options);
const defaultLimit = pageSizeEnabled
? (
pageSizeOptions.includes(parsePositiveInt(paginationLimit))
? Number(parsePositiveInt(paginationLimit))
: pageSizeOptions[0]
)
: (parsePositiveInt(paginationLimit) ?? 10);
const syncLimit = pageSizeEnabled && urlStateConfig.syncLimit !== false;
const cleanDefaultLimit = urlStateConfig.cleanDefaultLimit !== false;
const dataUrlPath = (() => {
try {
return new URL(dataUrl, appBase).pathname || String(dataUrl || '');
} catch {
return String(dataUrl || '');
}
})();
const containerKey = typeof container === 'string'
? container
: (containerEl.id ? `#${containerEl.id}` : 'grid');
const autoPageSizeStorageKey = [
PAGE_SIZE_STORAGE_PREFIX,
encodeURIComponent(window.location.pathname || ''),
encodeURIComponent(dataUrlPath || ''),
encodeURIComponent(containerKey || '')
].join(':');
const pageSizeStorageKey = pageSizeEnabled
? (String(pageSizeConfig.storageKey || autoPageSizeStorageKey).trim() || autoPageSizeStorageKey)
: '';
const parseAllowedLimit = (value) => {
if (!pageSizeEnabled) {
return parsePositiveInt(value);
}
const parsed = parsePositiveInt(value);
if (parsed === null) {
return null;
}
return pageSizeOptions.includes(parsed) ? parsed : null;
};
const pageSizeLabel = String(
pageSizeConfig.label
|| language?.pagination?.rowsPerPage
|| 'Rows per page'
).trim() || 'Rows per page';
let currentLimit = defaultLimit;
const urlLimit = syncLimit ? parseAllowedLimit(params.get(limitParam)) : null;
if (urlLimit !== null) {
currentLimit = urlLimit;
} else if (pageSizeEnabled) {
const storedLimit = parseAllowedLimit(safeStorageGet(pageSizeStorageKey));
if (storedLimit !== null) {
currentLimit = storedLimit;
}
}
if (pageSizeEnabled) {
safeStorageSet(pageSizeStorageKey, String(currentLimit));
}
let currentPage = urlStateConfig.syncPage ? parsePage(params.get(pageParam)) : 1;
let currentSort = null;
if (urlStateConfig.syncSort) {
@@ -93,6 +210,36 @@ export function createServerGrid(options) {
currentSort = { order: initialOrder, dir: initialDir };
}
}
const rowInteractionConfig = rowInteraction && typeof rowInteraction === 'object'
? rowInteraction
: {};
const hasRowOpenHandler = Boolean(rowDblClick && typeof rowDblClick.getUrl === 'function');
const rowInteractionEnabled = hasRowOpenHandler && rowInteractionConfig.enabled !== false;
const keepDblClick = rowInteractionEnabled && rowInteractionConfig.keepDblClick !== false;
const enableEnterOnRow = rowInteractionEnabled && rowInteractionConfig.enableEnterOnRow !== false;
const showRowActionButton = rowInteractionEnabled && rowInteractionConfig.showActionButton !== false;
const rowActionColumnLabel = String(
rowInteractionConfig.actionColumnLabel
|| language?.actions?.column
|| 'Actions'
).trim() || 'Actions';
const rowActionOpenLabel = String(language?.actions?.open || 'Open').trim() || 'Open';
const rowActionEditLabel = String(language?.actions?.edit || 'Edit').trim() || 'Edit';
const defaultResolveRowActionLabel = (url) => (
/(^|\/)edit(\/|$|\?)/i.test(String(url || '').trim())
? rowActionEditLabel
: rowActionOpenLabel
);
const resolveActionLabel = typeof rowInteractionConfig.resolveActionLabel === 'function'
? rowInteractionConfig.resolveActionLabel
: defaultResolveRowActionLabel;
const resolveRowOpenUrl = (rowData, rowIndex = -1) => {
if (!rowInteractionEnabled) {
return '';
}
const url = rowDblClick.getUrl(rowData, rowIndex);
return String(url || '').trim();
};
const getInput = (input) => {
if (!input) {return null;}
@@ -223,6 +370,15 @@ export function createServerGrid(options) {
} else {
query.delete(pageParam);
}
if (syncLimit) {
if (!cleanDefaultLimit || currentLimit !== defaultLimit) {
query.set(limitParam, String(currentLimit));
} else {
query.delete(limitParam);
}
} else {
query.delete(limitParam);
}
return query;
};
@@ -244,6 +400,7 @@ export function createServerGrid(options) {
url.searchParams.delete(sortOrderParam);
url.searchParams.delete(sortDirParam);
url.searchParams.delete(pageParam);
url.searchParams.delete(limitParam);
const query = buildUrlParams();
query.forEach((value, key) => url.searchParams.set(key, value));
if (historyMode === 'push') {
@@ -253,7 +410,7 @@ export function createServerGrid(options) {
window.history.replaceState({}, '', url.toString());
};
const actionConfig = actions && actions.enabled ? {
const explicitActionConfig = actions && actions.enabled ? {
label: actions.label || 'Actions',
index: Number.isInteger(actions.index) ? actions.index : null,
items: Array.isArray(actions.items) ? actions.items : [],
@@ -261,6 +418,26 @@ export function createServerGrid(options) {
wrapperClass: actions.wrapperClass || 'grid-actions',
onAction: typeof actions.onAction === 'function' ? actions.onAction : null
} : null;
const rowOpenActionConfig = !explicitActionConfig && showRowActionButton ? {
label: rowActionColumnLabel,
index: null,
items: [{
key: 'row-open',
type: 'link',
className: 'outline secondary app-grid-row-open',
label: (rowData, url) => {
const dynamicLabel = resolveActionLabel(url, rowData);
const normalizedLabel = String(dynamicLabel || '').trim();
return normalizedLabel || defaultResolveRowActionLabel(url);
},
href: ({ id }) => id
}],
// For row-open actions the URL itself is the stable row identifier.
getRowId: (rowData) => resolveRowOpenUrl(rowData),
wrapperClass: 'grid-actions grid-actions-row-open',
onAction: null
} : null;
const actionConfig = explicitActionConfig || rowOpenActionConfig;
const selectionConfig = selection && selection.enabled ? {
id: selection.id || 'selectRow',
index: Number.isInteger(selection.index) ? selection.index : 0,
@@ -402,18 +579,22 @@ export function createServerGrid(options) {
} : { enabled: false };
const paginationConfig = {
limit: paginationLimit,
limit: currentLimit,
page: Math.max(currentPage - 1, 0),
// We control page resets explicitly (search/filter/reset actions) to keep deep-link pages stable.
resetPageOnUpdate: false,
server: {
url: (prev, page, limit) => {
const parsedPage = Number.parseInt(String(page ?? ''), 10);
const parsedLimit = Number.parseInt(String(limit ?? ''), 10);
const parsedLimit = parseAllowedLimit(limit);
const nextPage = Number.isFinite(parsedPage) && parsedPage >= 0 ? parsedPage : 0;
const nextLimit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : paginationLimit;
const nextLimit = parsedLimit ?? currentLimit;
currentLimit = nextLimit;
if (pageSizeEnabled) {
safeStorageSet(pageSizeStorageKey, String(currentLimit));
}
currentPage = nextPage + 1;
if (urlSync && urlStateConfig.syncPage) {
if (urlSync && (urlStateConfig.syncPage || syncLimit)) {
updateUrl();
}
const offset = nextPage * nextLimit;
@@ -476,22 +657,37 @@ export function createServerGrid(options) {
}
const applyRowDataset = () => {
if (typeof rowDataset !== 'function') {return;}
const shouldApplyCustomDataset = typeof rowDataset === 'function';
if (!shouldApplyCustomDataset && !rowInteractionEnabled) {return;}
const rowEls = containerEl.querySelectorAll('.gridjs-tbody .gridjs-tr');
const dataRows = grid.config.store.getState().data?.rows || [];
rowEls.forEach((rowEl, index) => {
const rowData = dataRows[index] || null;
if (!rowData) {return;}
const attrs = rowDataset(rowData, index) || {};
Object.keys(attrs).forEach((key) => {
const value = attrs[key];
const attr = `data-${key}`;
if (value === null || typeof value === 'undefined' || value === '') {
rowEl.removeAttribute(attr);
if (rowInteractionEnabled) {
const rowOpenUrl = resolveRowOpenUrl(rowData, index);
if (rowOpenUrl !== '') {
rowEl.setAttribute('data-row-open-url', rowOpenUrl);
rowEl.setAttribute('tabindex', '-1');
} else {
rowEl.setAttribute(attr, String(value));
rowEl.removeAttribute('data-row-open-url');
if (rowEl.getAttribute('tabindex') === '-1') {
rowEl.removeAttribute('tabindex');
}
}
});
}
if (shouldApplyCustomDataset) {
const attrs = rowDataset(rowData, index) || {};
Object.keys(attrs).forEach((key) => {
const value = attrs[key];
const attr = `data-${key}`;
if (value === null || typeof value === 'undefined' || value === '') {
rowEl.removeAttribute(attr);
} else {
rowEl.setAttribute(attr, String(value));
}
});
}
});
};
@@ -530,7 +726,7 @@ export function createServerGrid(options) {
updateHeaderState();
};
if (typeof rowDataset === 'function') {
if (typeof rowDataset === 'function' || rowInteractionEnabled) {
const observer = new MutationObserver(() => applyRowDataset());
observer.observe(containerEl, { childList: true, subtree: true });
applyRowDataset();
@@ -547,25 +743,71 @@ export function createServerGrid(options) {
return { rowData, rowIndex };
};
if (rowDblClick) {
const resolveRowOpenUrlByElement = (rowEl) => {
if (!rowInteractionEnabled || !rowEl) {
return '';
}
const storedUrl = String(rowEl.getAttribute('data-row-open-url') || '').trim();
if (storedUrl !== '') {
return storedUrl;
}
const rowInfo = getRowDataByElement(rowEl);
if (!rowInfo) {
return '';
}
return resolveRowOpenUrl(rowInfo.rowData, rowInfo.rowIndex);
};
const navigateToRow = (rowEl) => {
const url = resolveRowOpenUrlByElement(rowEl);
if (url === '') {
return false;
}
window.location.href = url;
return true;
};
if (keepDblClick && rowDblClick) {
const excludeSelector = rowDblClick.exclude || '.grid-actions, button, a, input, select, textarea';
containerEl.addEventListener('dblclick', (event) => {
if (excludeSelector && event.target.closest(excludeSelector)) {
return;
}
const rowEl = event.target.closest('.gridjs-tr');
const rowInfo = getRowDataByElement(rowEl);
if (!rowInfo) {
navigateToRow(rowEl);
});
}
if (enableEnterOnRow) {
const interactiveSelector = '.grid-actions, button, a, input, select, textarea, [role="button"], [contenteditable="true"]';
containerEl.addEventListener('click', (event) => {
const target = event.target instanceof Element ? event.target : null;
if (!target || target.closest(interactiveSelector)) {
return;
}
const { rowData, rowIndex } = rowInfo;
const getUrl = rowDblClick.getUrl;
if (typeof getUrl !== 'function') {
const rowEl = target.closest('.gridjs-tr[data-row-open-url]');
if (!rowEl) {
return;
}
const url = getUrl(rowData, rowIndex);
if (url) {
window.location.href = url;
rowEl.focus({ preventScroll: true });
});
containerEl.addEventListener('keydown', (event) => {
if (event.key !== 'Enter' || event.defaultPrevented) {
return;
}
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
const target = event.target instanceof Element ? event.target : null;
if (!target || target.closest(interactiveSelector)) {
return;
}
const rowEl = target.closest('.gridjs-tr');
if (!rowEl) {
return;
}
if (navigateToRow(rowEl)) {
event.preventDefault();
}
});
}
@@ -581,13 +823,13 @@ export function createServerGrid(options) {
if (!rowInfo) {
return;
}
const { rowData } = rowInfo;
const { rowData, rowIndex } = rowInfo;
const actionKey = actionEl.getAttribute('data-grid-action') || '';
const actionItem = actionConfig.items.find((item) => item && item.key === actionKey);
if (!actionItem) {
return;
}
const id = actionConfig.getRowId ? actionConfig.getRowId(rowData) : null;
const id = actionConfig.getRowId ? actionConfig.getRowId(rowData, rowIndex) : null;
if (!id) {
return;
}
@@ -614,6 +856,91 @@ export function createServerGrid(options) {
});
}
let pageSizeSelect = null;
const syncPageSizeControl = () => {
if (!pageSizeSelect) {
return;
}
pageSizeSelect.value = String(currentLimit);
};
const resolvePageSizeToolbar = () => {
const selector = String(pageSizeConfig.toolbarSelector || '.app-list-toolbar').trim() || '.app-list-toolbar';
const allToolbars = Array.from(document.querySelectorAll(selector));
if (!allToolbars.length) {
return null;
}
const precedingToolbars = allToolbars.filter((toolbar) => (
Boolean(toolbar.compareDocumentPosition(containerEl) & Node.DOCUMENT_POSITION_FOLLOWING)
));
const pool = precedingToolbars.length ? precedingToolbars : allToolbars;
const withFilterButton = pool.filter((toolbar) => toolbar.querySelector('[data-filter-drawer-open]'));
const targetPool = withFilterButton.length ? withFilterButton : pool;
return targetPool[targetPool.length - 1] || null;
};
const mountPageSizeControl = () => {
if (!pageSizeEnabled) {
return;
}
const toolbar = resolvePageSizeToolbar();
if (!toolbar) {
warnOnce(
'UI_PAGE_SIZE_HOST_MISSING',
`Missing page-size toolbar near grid: ${containerKey || 'unknown'}`,
{ module: 'grid-factory' }
);
return;
}
const controlKey = encodeURIComponent(`${containerKey}:${dataUrlPath}`);
const existing = toolbar.querySelector(`.app-grid-page-size[data-grid-page-size-key="${controlKey}"]`);
if (existing) {
pageSizeSelect = existing.querySelector('select');
syncPageSizeControl();
return;
}
const field = document.createElement('label');
field.className = 'app-field app-grid-page-size';
field.dataset.gridPageSizeKey = controlKey;
const caption = document.createElement('span');
caption.textContent = pageSizeLabel;
const select = document.createElement('select');
pageSizeOptions.forEach((option) => {
const optionEl = document.createElement('option');
optionEl.value = String(option);
optionEl.textContent = String(option);
select.appendChild(optionEl);
});
select.value = String(currentLimit);
select.addEventListener('change', () => {
const nextLimit = parseAllowedLimit(select.value);
if (nextLimit === null || nextLimit === currentLimit) {
syncPageSizeControl();
return;
}
currentLimit = nextLimit;
currentPage = 1;
safeStorageSet(pageSizeStorageKey, String(currentLimit));
syncPageSizeControl();
updateGrid({ resetPage: true, urlHistoryMode: 'push' });
});
field.appendChild(caption);
field.appendChild(select);
const filterButton = toolbar.querySelector(':scope > [data-filter-drawer-open]');
if (filterButton) {
filterButton.insertAdjacentElement('afterend', field);
} else {
toolbar.appendChild(field);
}
pageSizeSelect = select;
syncPageSizeControl();
};
const updateGrid = (options = {}) => {
const resetPage = options && options.resetPage === true;
const urlHistoryMode = options?.urlHistoryMode || 'replace';
@@ -621,12 +948,18 @@ export function createServerGrid(options) {
currentPage = 1;
}
updateUrl({ historyMode: urlHistoryMode });
const nextConfig = {
server: { ...serverConfig, url: baseUrl() }
const paginationState = grid?.config?.pagination && typeof grid.config.pagination === 'object'
? grid.config.pagination
: paginationConfig;
const nextPagination = {
...paginationState,
limit: currentLimit,
page: resetPage ? 0 : Math.max(currentPage - 1, 0)
};
const nextConfig = {
server: { ...serverConfig, url: baseUrl() },
pagination: nextPagination
};
if (resetPage && grid?.config?.pagination && typeof grid.config.pagination === 'object') {
nextConfig.pagination = { ...grid.config.pagination, page: 0 };
}
// Use Grid.js reconfiguration API so server pipeline is refreshed reliably on filter/search changes.
if (typeof grid?.updateConfig === 'function') {
@@ -636,13 +969,13 @@ export function createServerGrid(options) {
if (grid?.config) {
grid.config.server = nextConfig.server;
if (nextConfig.pagination) {
grid.config.pagination = nextConfig.pagination;
}
grid.config.pagination = nextConfig.pagination;
}
grid.forceRender();
};
mountPageSizeControl();
const debounce = (fn, delay = 250) => {
let timer;
return (...args) => {
@@ -724,7 +1057,8 @@ export function createServerGrid(options) {
search: searchValue,
filters: filterValues,
sort: currentSort ? { ...currentSort } : null,
page: currentPage
page: currentPage,
limit: currentLimit
};
};
@@ -741,7 +1075,8 @@ export function createServerGrid(options) {
search: draftSearch,
filters: filterValues,
sort: currentSort ? { ...currentSort } : null,
page: currentPage
page: currentPage,
limit: currentLimit
};
};
@@ -783,6 +1118,12 @@ export function createServerGrid(options) {
const parsedPage = parsePage(state.page);
currentPage = parsedPage;
}
const nextLimit = parseAllowedLimit(state?.limit);
if (nextLimit !== null) {
currentLimit = nextLimit;
safeStorageSet(pageSizeStorageKey, String(currentLimit));
syncPageSizeControl();
}
};
const getSortParams = () => (currentSort ? { ...currentSort } : null);

View File

@@ -11,7 +11,17 @@ if (config) {
const labels = config.labels ?? {};
const initTenantsGrid = () => {
const tenantUuidIndex = 8;
const tenantDescriptionIndex = 1;
const formatBadge = (value) => badgeHtml(gridjs, value);
const initialsForRow = (row) => {
const descriptionCell = row?.cells?.[tenantDescriptionIndex]?.data;
const label = typeof descriptionCell === 'object' && descriptionCell !== null
? String(descriptionCell.label ?? '')
: String(descriptionCell ?? '');
const initial = label.trim().charAt(0).toUpperCase();
return initial || '?';
};
const formatStatus = (value) => {
if (!value || typeof value !== 'object') {
return gridjs.html('-');
@@ -50,15 +60,29 @@ if (config) {
dataUrl: 'admin/tenants/data',
appBase,
columns: [
{
name: labels.avatar || 'Avatar',
sort: false,
formatter: (cell, row) => {
if (!cell) {
const initials = escapeHtml(initialsForRow(row));
return gridjs.html(`<span class="grid-avatar grid-avatar-tenant grid-avatar-placeholder">${initials}</span>`);
}
const uuid = encodeURIComponent(String(cell));
const src = new URL(`admin/tenants/avatar-file?uuid=${uuid}&size=64`, appBase).toString();
const full = new URL(`admin/tenants/avatar-file?uuid=${uuid}&size=256`, appBase).toString();
return gridjs.html(`<a data-fslightbox="tenant-avatars" href="${full}"><img class="grid-avatar grid-avatar-tenant" src="${src}" alt="" loading="lazy"></a>`);
},
},
{
name: labels.description || 'Description',
sort: true,
formatter: (cell) => {
if (!cell || typeof cell !== 'object') {
return gridjs.html(String(cell ?? ''));
return gridjs.html(escapeHtml(String(cell ?? '')));
}
const label = cell.label ?? '';
return gridjs.html(`${label}`);
const label = escapeHtml(cell.label ?? '');
return gridjs.html(label);
},
},
{
@@ -96,10 +120,11 @@ if (config) {
hidden: true,
},
],
sortColumns: ['description', 'status', 'theme', 'sso', 'active_users', 'inactive_users', 'created', null],
sortColumns: [null, 'description', 'status', 'theme', 'sso', 'active_users', 'inactive_users', 'created', null],
paginationLimit: 10,
language: config.gridLang ?? {},
mapData: (data) => data.data.map((row) => [
row.has_avatar ? row.uuid : '',
{
label: row.description,
is_default: row.is_default ? 1 : 0,
@@ -117,11 +142,11 @@ if (config) {
filterSchema,
urlSync: true,
rowDataset: (row) => ({
uuid: row?.cells?.[7]?.data,
uuid: row?.cells?.[tenantUuidIndex]?.data,
}),
rowDblClick: {
getUrl: (rowData) => {
const uuid = rowData?.cells?.[7]?.data;
const uuid = rowData?.cells?.[tenantUuidIndex]?.data;
return uuid ? new URL(`admin/tenants/edit/${uuid}`, appBase).toString() : '';
},
},