- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks - Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists - Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services - Add Microsoft OIDC SSO, API token management, and user lifecycle features - Add swagger-ui vendor integration and OpenAPI spec - Add production Docker setup and bin/ scripts - Update composer dependencies, config, templates, and frontend assets throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
709 lines
23 KiB
JavaScript
709 lines
23 KiB
JavaScript
import { warnOnce } from './app-dom.js';
|
|
|
|
export function createServerGrid(options) {
|
|
const {
|
|
container,
|
|
dataUrl,
|
|
appBase = '/',
|
|
columns,
|
|
mapData,
|
|
total = (data) => data.total,
|
|
sortColumns = [],
|
|
paginationLimit = 10,
|
|
language = {},
|
|
search = null,
|
|
filters = [],
|
|
urlSync = true,
|
|
urlState = null,
|
|
gridjs,
|
|
rowDblClick = null,
|
|
actions = null,
|
|
rowDataset = null,
|
|
selection = null
|
|
} = options || {};
|
|
|
|
const gridjsLib = gridjs || window.gridjs;
|
|
const containerEl = typeof container === 'string' ? document.querySelector(container) : container;
|
|
if (!containerEl || !gridjsLib) {
|
|
if (!containerEl) {
|
|
warnOnce('UI_EL_MISSING', `Missing grid container: ${container}`, { module: 'grid-factory' });
|
|
}
|
|
if (!gridjsLib) {
|
|
warnOnce('UI_LIB_MISSING', 'Grid.js not available', { module: 'grid-factory' });
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const urlStateConfig = {
|
|
pageParam: 'page',
|
|
sortOrderParam: 'order',
|
|
sortDirParam: 'dir',
|
|
cleanFirstPage: true,
|
|
syncPage: true,
|
|
syncSort: true,
|
|
...(urlState || {})
|
|
};
|
|
const pageParam = urlStateConfig.pageParam || 'page';
|
|
const sortOrderParam = urlStateConfig.sortOrderParam || 'order';
|
|
const sortDirParam = urlStateConfig.sortDirParam || 'dir';
|
|
const resolveUrl = (path) => new URL(path, appBase).toString();
|
|
const addParam = (url, key, value) => {
|
|
const nextUrl = new URL(url);
|
|
nextUrl.searchParams.set(key, value);
|
|
return nextUrl.toString();
|
|
};
|
|
const deleteParam = (url, key) => {
|
|
const nextUrl = new URL(url);
|
|
nextUrl.searchParams.delete(key);
|
|
return nextUrl.toString();
|
|
};
|
|
const clearSortParams = (url) => deleteParam(deleteParam(url, sortOrderParam), sortDirParam);
|
|
const parsePage = (rawValue) => {
|
|
const parsed = Number.parseInt(String(rawValue ?? ''), 10);
|
|
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
return 1;
|
|
}
|
|
return parsed;
|
|
};
|
|
const validSortKeys = new Set(
|
|
(sortColumns || [])
|
|
.filter((key) => typeof key === 'string')
|
|
.map((key) => key.trim())
|
|
.filter(Boolean)
|
|
);
|
|
const normalizeDir = (rawValue) => {
|
|
const normalized = String(rawValue ?? '').trim().toLowerCase();
|
|
return normalized === 'asc' || normalized === 'desc' ? normalized : '';
|
|
};
|
|
let currentPage = urlStateConfig.syncPage ? parsePage(params.get(pageParam)) : 1;
|
|
let currentSort = null;
|
|
if (urlStateConfig.syncSort) {
|
|
const initialOrder = String(params.get(sortOrderParam) ?? '').trim();
|
|
const initialDir = normalizeDir(params.get(sortDirParam));
|
|
if (initialOrder !== '' && initialDir !== '' && validSortKeys.has(initialOrder)) {
|
|
currentSort = { order: initialOrder, dir: initialDir };
|
|
}
|
|
}
|
|
|
|
const getInput = (input) => {
|
|
if (!input) {return null;}
|
|
return typeof input === 'string' ? document.querySelector(input) : input;
|
|
};
|
|
|
|
const getInputValue = (input) => {
|
|
if (!input) {return '';}
|
|
if (input.type === 'checkbox') {
|
|
return input.checked ? (input.value || '1') : '';
|
|
}
|
|
if (input.tagName === 'SELECT' && input.multiple) {
|
|
return Array.from(input.selectedOptions || []).map((option) => option.value).filter(Boolean);
|
|
}
|
|
return (input.value ?? '').toString();
|
|
};
|
|
|
|
const setInputValue = (input, value) => {
|
|
if (!input) {return;}
|
|
if (input.type === 'checkbox') {
|
|
input.checked = value === true || value === 'true' || value === '1';
|
|
} else if (input.tagName === 'SELECT' && input.multiple) {
|
|
const values = Array.isArray(value)
|
|
? value.map((item) => item.toString())
|
|
: value
|
|
? value.toString().split(',').map((item) => item.trim()).filter(Boolean)
|
|
: [];
|
|
Array.from(input.options || []).forEach((option) => {
|
|
option.selected = values.includes(option.value);
|
|
});
|
|
} else {
|
|
input.value = value;
|
|
}
|
|
if (input._multiSelectInstance && typeof input._multiSelectInstance.setValues === 'function') {
|
|
const values = Array.isArray(value)
|
|
? value.map((item) => item.toString())
|
|
: value
|
|
? value.toString().split(',').map((item) => item.trim()).filter(Boolean)
|
|
: [];
|
|
input._multiSelectInstance.setValues(values);
|
|
}
|
|
};
|
|
|
|
let searchValue = '';
|
|
let searchParam = 'search';
|
|
let searchInput = null;
|
|
let searchDebounce = 250;
|
|
if (search) {
|
|
searchInput = getInput(search.input);
|
|
searchParam = search.param || searchParam;
|
|
searchDebounce = search.debounce ?? searchDebounce;
|
|
const initial = params.get(searchParam) ?? search.default ?? '';
|
|
searchValue = initial || '';
|
|
if (searchInput) {
|
|
setInputValue(searchInput, initial);
|
|
}
|
|
}
|
|
|
|
const filterState = filters.map((filter) => {
|
|
const input = getInput(filter.input);
|
|
const initial = params.get(filter.param);
|
|
const value = initial !== null ? initial : (filter.default ?? '');
|
|
if (input) {
|
|
const parsed = typeof filter.parse === 'function' ? filter.parse(value) : value;
|
|
setInputValue(input, parsed);
|
|
}
|
|
return { ...filter, input, value };
|
|
});
|
|
|
|
const normalizeValue = (filter, value) => {
|
|
if (typeof filter.normalize === 'function') {
|
|
return filter.normalize(value);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
const buildDataParams = () => {
|
|
const query = new URLSearchParams();
|
|
if (searchValue) {
|
|
query.set(searchParam, searchValue);
|
|
}
|
|
filterState.forEach((filter) => {
|
|
const raw = getInputValue(filter.input) || filter.value || '';
|
|
const normalized = normalizeValue(filter, raw);
|
|
if (Array.isArray(normalized)) {
|
|
const list = normalized.map((item) => item.toString()).filter(Boolean);
|
|
if (list.length) {
|
|
query.set(filter.param, list.join(','));
|
|
}
|
|
return;
|
|
}
|
|
if (normalized !== '' && normalized !== null && typeof normalized !== 'undefined') {
|
|
query.set(filter.param, normalized);
|
|
}
|
|
});
|
|
if (currentSort?.order && currentSort?.dir) {
|
|
query.set(sortOrderParam, currentSort.order);
|
|
query.set(sortDirParam, currentSort.dir);
|
|
}
|
|
return query;
|
|
};
|
|
|
|
const buildUrlParams = () => {
|
|
const query = buildDataParams();
|
|
if (!urlStateConfig.syncSort) {
|
|
query.delete(sortOrderParam);
|
|
query.delete(sortDirParam);
|
|
}
|
|
if (urlStateConfig.syncPage) {
|
|
if (urlStateConfig.cleanFirstPage && currentPage <= 1) {
|
|
query.delete(pageParam);
|
|
} else {
|
|
query.set(pageParam, String(currentPage));
|
|
}
|
|
} else {
|
|
query.delete(pageParam);
|
|
}
|
|
return query;
|
|
};
|
|
|
|
const baseUrl = () => {
|
|
const url = new URL(dataUrl, appBase);
|
|
const query = buildDataParams();
|
|
query.forEach((value, key) => url.searchParams.set(key, value));
|
|
return url.toString();
|
|
};
|
|
|
|
const updateUrl = () => {
|
|
if (!urlSync) {return;}
|
|
const url = new URL(window.location.href);
|
|
if (searchParam) { url.searchParams.delete(searchParam); }
|
|
filterState.forEach((filter) => url.searchParams.delete(filter.param));
|
|
url.searchParams.delete(sortOrderParam);
|
|
url.searchParams.delete(sortDirParam);
|
|
url.searchParams.delete(pageParam);
|
|
const query = buildUrlParams();
|
|
query.forEach((value, key) => url.searchParams.set(key, value));
|
|
window.history.replaceState({}, '', url.toString());
|
|
};
|
|
|
|
const actionConfig = actions && actions.enabled ? {
|
|
label: actions.label || 'Actions',
|
|
index: Number.isInteger(actions.index) ? actions.index : null,
|
|
items: Array.isArray(actions.items) ? actions.items : [],
|
|
getRowId: typeof actions.getRowId === 'function' ? actions.getRowId : null,
|
|
wrapperClass: actions.wrapperClass || 'grid-actions',
|
|
onAction: typeof actions.onAction === 'function' ? actions.onAction : null
|
|
} : null;
|
|
const selectionConfig = selection && selection.enabled ? {
|
|
id: selection.id || 'selectRow',
|
|
index: Number.isInteger(selection.index) ? selection.index : 0,
|
|
component: selection.component || null,
|
|
props: selection.props || null,
|
|
onChange: typeof selection.onChange === 'function' ? selection.onChange : null,
|
|
getSelectedIds: typeof selection.getSelectedIds === 'function' ? selection.getSelectedIds : null,
|
|
selectAllLabel: selection.selectAllLabel || 'Select all',
|
|
selectAllEnabled: selection.selectAllEnabled !== false
|
|
} : null;
|
|
|
|
const insertActionColumn = (cols) => {
|
|
if (!actionConfig) {return cols;}
|
|
const actionColumn = {
|
|
name: actionConfig.label,
|
|
sort: false,
|
|
formatter: (_cell, row) => {
|
|
const id = actionConfig.getRowId ? actionConfig.getRowId(row) : null;
|
|
if (!id) {
|
|
return gridjsLib.html('');
|
|
}
|
|
const buttons = actionConfig.items.map((item) => {
|
|
if (!item || !item.key) {return '';}
|
|
const label = typeof item.label === 'function' ? item.label(row, id) : item.label;
|
|
if (!label) {return '';}
|
|
const className = item.className ? ` ${item.className}` : '';
|
|
const type = item.type || 'button';
|
|
if (type === 'link') {
|
|
const href = typeof item.href === 'function' ? item.href({ row, id }) : item.href;
|
|
const url = href || '#';
|
|
return `<a role="button" class="${className.trim()}" data-grid-action="${item.key}" href="${url}">${label}</a>`;
|
|
}
|
|
return `<button type="button" class="${className.trim()}" data-grid-action="${item.key}">${label}</button>`;
|
|
}).join('');
|
|
return gridjsLib.html(`<div class="${actionConfig.wrapperClass}" role="group">${buttons}</div>`);
|
|
}
|
|
};
|
|
|
|
const next = [...cols];
|
|
const index = actionConfig.index === null ? next.length : Math.min(Math.max(actionConfig.index, 0), next.length);
|
|
next.splice(index, 0, actionColumn);
|
|
return next;
|
|
};
|
|
|
|
const insertSelectionColumn = (cols) => {
|
|
if (!selectionConfig) {return cols;}
|
|
const headerLabel = selectionConfig.selectAllLabel || 'Select all';
|
|
const header = selectionConfig.selectAllEnabled
|
|
? gridjsLib.html(`<input type="checkbox" data-grid-select-all aria-label="${headerLabel}">`)
|
|
: '';
|
|
const selectionColumn = {
|
|
id: selectionConfig.id,
|
|
name: header,
|
|
sort: false,
|
|
plugin: {
|
|
id: selectionConfig.id
|
|
}
|
|
};
|
|
|
|
const next = [...cols];
|
|
const index = Math.min(Math.max(selectionConfig.index ?? 0, 0), next.length);
|
|
next.splice(index, 0, selectionColumn);
|
|
return next;
|
|
};
|
|
|
|
const insertActionSort = (cols) => {
|
|
if (!actionConfig || !cols.length) {return cols;}
|
|
const next = [...cols];
|
|
const index = actionConfig.index === null ? next.length : Math.min(Math.max(actionConfig.index, 0), next.length);
|
|
next.splice(index, 0, null);
|
|
return next;
|
|
};
|
|
|
|
const insertSelectionSort = (cols) => {
|
|
if (!selectionConfig || !cols.length) {return cols;}
|
|
const next = [...cols];
|
|
const index = Math.min(Math.max(selectionConfig.index ?? 0, 0), next.length);
|
|
next.splice(index, 0, null);
|
|
return next;
|
|
};
|
|
|
|
const mapDataWithActions = (data) => {
|
|
const rows = mapData ? mapData(data) : [];
|
|
if (!actionConfig || !Array.isArray(rows)) {
|
|
return rows;
|
|
}
|
|
const index = actionConfig.index === null ? null : Math.min(Math.max(actionConfig.index, 0), Infinity);
|
|
return rows.map((row) => {
|
|
if (!Array.isArray(row)) {return row;}
|
|
const next = [...row];
|
|
const insertAt = index === null ? next.length : Math.min(index, next.length);
|
|
next.splice(insertAt, 0, null);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const serverConfig = {
|
|
url: baseUrl(),
|
|
then: mapDataWithActions,
|
|
total
|
|
};
|
|
|
|
let finalSortColumns = sortColumns;
|
|
if (selectionConfig) {
|
|
finalSortColumns = insertSelectionSort(finalSortColumns);
|
|
}
|
|
if (actionConfig) {
|
|
finalSortColumns = insertActionSort(finalSortColumns);
|
|
}
|
|
const sortConfig = finalSortColumns.length ? {
|
|
enabled: true,
|
|
server: {
|
|
url: (prev, columns) => {
|
|
if (!columns.length) {
|
|
// Keep URL/bootstrap sort when grid has no active sort columns yet (e.g. initial load from deep-link).
|
|
if (currentSort?.order && currentSort?.dir) {
|
|
return addParam(addParam(clearSortParams(prev), sortOrderParam, currentSort.order), sortDirParam, currentSort.dir);
|
|
}
|
|
currentSort = null;
|
|
return clearSortParams(prev);
|
|
}
|
|
const column = columns[0];
|
|
const key = finalSortColumns[column.index];
|
|
if (!key) {
|
|
if (currentSort?.order && currentSort?.dir) {
|
|
return addParam(addParam(clearSortParams(prev), sortOrderParam, currentSort.order), sortDirParam, currentSort.dir);
|
|
}
|
|
currentSort = null;
|
|
return clearSortParams(prev);
|
|
}
|
|
const direction = column.direction === 1 ? 'asc' : 'desc';
|
|
currentSort = { order: key, dir: direction };
|
|
if (urlSync && urlStateConfig.syncSort) {
|
|
updateUrl();
|
|
}
|
|
return addParam(addParam(clearSortParams(prev), sortOrderParam, key), sortDirParam, direction);
|
|
}
|
|
}
|
|
} : { enabled: false };
|
|
|
|
const paginationConfig = {
|
|
limit: paginationLimit,
|
|
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 nextPage = Number.isFinite(parsedPage) && parsedPage >= 0 ? parsedPage : 0;
|
|
const nextLimit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : paginationLimit;
|
|
currentPage = nextPage + 1;
|
|
if (urlSync && urlStateConfig.syncPage) {
|
|
updateUrl();
|
|
}
|
|
const offset = nextPage * nextLimit;
|
|
return addParam(addParam(prev, 'limit', nextLimit), 'offset', offset);
|
|
}
|
|
}
|
|
};
|
|
|
|
const plugins = [];
|
|
if (selectionConfig?.component) {
|
|
plugins.push({
|
|
id: selectionConfig.id,
|
|
component: selectionConfig.component,
|
|
props: selectionConfig.props || {}
|
|
});
|
|
}
|
|
|
|
const grid = new gridjsLib.Grid({
|
|
columns: insertActionColumn(insertSelectionColumn(columns)),
|
|
server: serverConfig,
|
|
search: false,
|
|
sort: sortConfig,
|
|
pagination: paginationConfig,
|
|
language,
|
|
plugins
|
|
});
|
|
|
|
grid.render(containerEl);
|
|
setTimeout(() => {
|
|
initSelectAll();
|
|
}, 0);
|
|
|
|
// Prevent empty-state flash before data arrives.
|
|
let hasLoadedOnce = false;
|
|
const markLoadedOnce = () => {
|
|
if (hasLoadedOnce) {return;}
|
|
hasLoadedOnce = true;
|
|
containerEl.classList.add('gridjs-has-loaded');
|
|
};
|
|
const setUpdating = () => containerEl.classList.add('gridjs-is-updating');
|
|
const clearUpdating = () => containerEl.classList.remove('gridjs-is-updating');
|
|
|
|
const store = grid?.config?.store;
|
|
if (store?.subscribe) {
|
|
store.subscribe((state) => {
|
|
if (!state) {return;}
|
|
// Grid.js status: 0=Init, 1=Loading, 2=Loaded, 3=Rendered, 4=Error
|
|
if (state.status === 1) {
|
|
setUpdating();
|
|
} else {
|
|
clearUpdating();
|
|
}
|
|
if (state.status >= 2) {
|
|
markLoadedOnce();
|
|
}
|
|
if (state.status >= 3) {
|
|
initSelectAll();
|
|
}
|
|
});
|
|
}
|
|
|
|
const applyRowDataset = () => {
|
|
if (typeof rowDataset !== 'function') {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);
|
|
} else {
|
|
rowEl.setAttribute(attr, String(value));
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const initSelectAll = () => {
|
|
if (!selectionConfig?.component || !selectionConfig.selectAllEnabled) {return;}
|
|
const header = containerEl.querySelector(`th[data-column-id="${selectionConfig.id}"] input[data-grid-select-all]`);
|
|
if (!header) {return;}
|
|
if (header.dataset.bound === '1') {
|
|
return;
|
|
}
|
|
header.dataset.bound = '1';
|
|
|
|
const getRowCheckboxes = () => Array.from(
|
|
containerEl.querySelectorAll(`td[data-column-id="${selectionConfig.id}"] input[type="checkbox"]`)
|
|
);
|
|
|
|
const updateHeaderState = () => {
|
|
const boxes = getRowCheckboxes();
|
|
const total = boxes.length;
|
|
const checked = boxes.filter((box) => box.checked).length;
|
|
header.indeterminate = total > 0 && checked > 0 && checked < total;
|
|
header.checked = total > 0 && checked === total;
|
|
header.disabled = total === 0;
|
|
};
|
|
|
|
header.addEventListener('change', () => {
|
|
const boxes = getRowCheckboxes();
|
|
boxes.forEach((box) => {
|
|
if (box.checked !== header.checked) {
|
|
box.click();
|
|
}
|
|
});
|
|
updateHeaderState();
|
|
});
|
|
|
|
updateHeaderState();
|
|
};
|
|
|
|
if (typeof rowDataset === 'function') {
|
|
const observer = new MutationObserver(() => applyRowDataset());
|
|
observer.observe(containerEl, { childList: true, subtree: true });
|
|
applyRowDataset();
|
|
}
|
|
|
|
const getRowDataByElement = (rowEl) => {
|
|
if (!rowEl) {return null;}
|
|
const rows = Array.from(rowEl.parentElement?.children || []);
|
|
const rowIndex = rows.indexOf(rowEl);
|
|
if (rowIndex < 0) {return null;}
|
|
const dataRows = grid.config.store.getState().data?.rows || [];
|
|
const rowData = dataRows[rowIndex] || null;
|
|
if (!rowData) {return null;}
|
|
return { rowData, rowIndex };
|
|
};
|
|
|
|
if (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) {
|
|
return;
|
|
}
|
|
const { rowData, rowIndex } = rowInfo;
|
|
const getUrl = rowDblClick.getUrl;
|
|
if (typeof getUrl !== 'function') {
|
|
return;
|
|
}
|
|
const url = getUrl(rowData, rowIndex);
|
|
if (url) {
|
|
window.location.href = url;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (actionConfig) {
|
|
containerEl.addEventListener('click', async (event) => {
|
|
const actionEl = event.target.closest('[data-grid-action]');
|
|
if (!actionEl) {
|
|
return;
|
|
}
|
|
const rowEl = actionEl.closest('.gridjs-tr');
|
|
const rowInfo = getRowDataByElement(rowEl);
|
|
if (!rowInfo) {
|
|
return;
|
|
}
|
|
const { rowData } = 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;
|
|
if (!id) {
|
|
return;
|
|
}
|
|
const confirmText = typeof actionItem.confirm === 'function'
|
|
? actionItem.confirm({ row: rowData, id })
|
|
: actionItem.confirm;
|
|
if (confirmText && !window.confirm(confirmText)) {
|
|
return;
|
|
}
|
|
if (actionItem.type === 'link') {
|
|
const href = typeof actionItem.href === 'function'
|
|
? actionItem.href({ row: rowData, id })
|
|
: actionItem.href;
|
|
if (href) {
|
|
event.preventDefault();
|
|
window.location.href = href;
|
|
}
|
|
return;
|
|
}
|
|
if (actionConfig.onAction) {
|
|
event.preventDefault();
|
|
await actionConfig.onAction({ action: actionKey, id, row: rowData, event, grid, container: containerEl });
|
|
}
|
|
});
|
|
}
|
|
|
|
const updateGrid = (options = {}) => {
|
|
const resetPage = options && options.resetPage === true;
|
|
if (resetPage) {
|
|
currentPage = 1;
|
|
}
|
|
updateUrl();
|
|
const nextConfig = {
|
|
server: { ...serverConfig, url: baseUrl() }
|
|
};
|
|
if (resetPage) {
|
|
nextConfig.pagination = { ...paginationConfig, page: 0 };
|
|
}
|
|
grid.updateConfig(nextConfig).forceRender();
|
|
};
|
|
|
|
const debounce = (fn, delay = 250) => {
|
|
let timer;
|
|
return (...args) => {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => fn(...args), delay);
|
|
};
|
|
};
|
|
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', debounce((event) => {
|
|
searchValue = event.target.value.trim();
|
|
updateGrid({ resetPage: true });
|
|
}, searchDebounce));
|
|
}
|
|
|
|
filterState.forEach((filter) => {
|
|
if (!filter.input) {return;}
|
|
const eventName = filter.event || 'change';
|
|
filter.input.addEventListener(eventName, () => {
|
|
filter.value = getInputValue(filter.input);
|
|
updateGrid({ resetPage: true });
|
|
});
|
|
});
|
|
|
|
const resetFilters = () => {
|
|
if (search) {
|
|
const defaultValue = search.default ?? '';
|
|
searchValue = defaultValue;
|
|
if (searchInput) {
|
|
setInputValue(searchInput, defaultValue);
|
|
}
|
|
}
|
|
filterState.forEach((filter) => {
|
|
const defaultValue = filter.default ?? '';
|
|
filter.value = defaultValue;
|
|
if (filter.input) {
|
|
setInputValue(filter.input, defaultValue);
|
|
}
|
|
});
|
|
updateGrid({ resetPage: true });
|
|
};
|
|
|
|
const getSortParams = () => (currentSort ? { ...currentSort } : null);
|
|
const selectionApi = selectionConfig && selectionConfig.component ? {
|
|
getSelectedIds: () => {
|
|
if (selectionConfig.getSelectedIds) {
|
|
return selectionConfig.getSelectedIds({ grid, container: containerEl });
|
|
}
|
|
const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
|
|
const store = plugin?.props?.store;
|
|
const state = store?.state ?? store?.getState?.();
|
|
if (!state) {return [];}
|
|
if (Array.isArray(state.selected)) {return state.selected;}
|
|
if (Array.isArray(state.ids)) {return state.ids;}
|
|
if (state.rows && typeof state.rows === 'object') {
|
|
return Object.keys(state.rows).filter((key) => state.rows[key]);
|
|
}
|
|
if (state instanceof Set) {return Array.from(state);}
|
|
if (Array.isArray(state)) {return state;}
|
|
if (typeof state === 'object') {
|
|
return Object.keys(state).filter((key) => state[key]);
|
|
}
|
|
return [];
|
|
},
|
|
clear: () => {
|
|
const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
|
|
const store = plugin?.props?.store;
|
|
if (store?.dispatch) {
|
|
store.dispatch('reset');
|
|
} else if (store?.reset) {
|
|
store.reset();
|
|
}
|
|
}
|
|
} : null;
|
|
|
|
if (selectionConfig?.onChange && selectionConfig.component) {
|
|
grid.on('ready', () => {
|
|
const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
|
|
const store = plugin?.props?.store;
|
|
const sync = () => {
|
|
selectionConfig.onChange(selectionApi?.getSelectedIds?.() || []);
|
|
};
|
|
if (store?.on) {store.on('updated', sync);}
|
|
if (store?.subscribe) {store.subscribe(sync);}
|
|
sync();
|
|
});
|
|
}
|
|
|
|
grid.on('ready', () => {
|
|
initSelectAll();
|
|
setTimeout(() => {
|
|
if (typeof window.requestFsLightboxRefresh === 'function') {
|
|
window.requestFsLightboxRefresh();
|
|
return;
|
|
}
|
|
if (typeof window.refreshFsLightbox === 'function') {
|
|
window.refreshFsLightbox();
|
|
} else {
|
|
window.__fsLightboxRefreshPending = true;
|
|
}
|
|
}, 0);
|
|
});
|
|
|
|
return { grid, container: containerEl, updateGrid, updateUrl, baseUrl, resolveUrl, getSortParams, resetFilters, selection: selectionApi };
|
|
}
|