@underpostnet/underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
1,016 lines (1,015 loc) • 45.9 kB
JavaScript
import { AgGrid } from '../../components/core/AgGrid.js';
import { BtnIcon } from '../../components/core/BtnIcon.js';
import { getId, getValueFromJoinString, timer } from '../../components/core/CommonJs.js';
import { darkTheme } from '../../components/core/Css.js';
import { EventsUI } from '../../components/core/EventsUI.js';
import { loggerFactory } from '../../components/core/Logger.js';
import { Modal } from '../../components/core/Modal.js';
import { NotificationManager } from '../../components/core/NotificationManager.js';
import { Translate } from '../../components/core/Translate.js';
import { getQueryParams, listenQueryParamsChange, RouterEvents, setQueryParams } from '../../components/core/Router.js';
import { s } from '../../components/core/VanillaJs.js';
import { DefaultService } from './default.service.js';
const logger = loggerFactory(import.meta);
class DefaultOptions {
static idModal = 'modal-default-management';
static serviceId = 'default-management';
static entity = 'default';
static columnDefs = [
{ field: '0', headerName: '0', cellClassRules: { 'row-new-highlight': (params) => true } },
{ field: '1', headerName: '1' },
{ field: '2', headerName: '2' },
{ field: 'createdAt', headerName: 'createdAt', cellDataType: 'date', editable: false },
{ field: 'updatedAt', headerName: 'updatedAt', cellDataType: 'date', editable: false },
];
static defaultColKeyFocus = '0';
static ServiceProvider = DefaultService;
static permissions = {
add: true,
remove: true,
reload: true,
};
static paginationOptions = {
limitOptions: [10, 20, 50, 100],
};
}
const columnDefFormatter = (obj, columnDefs, customFormat) => {
for (const colDef of columnDefs)
switch (colDef.cellDataType) {
case 'date': {
const value = obj[colDef.field];
// Do NOT default missing/blank dates to "now" — render as empty instead.
if (value === null || value === undefined || value === '') {
obj[colDef.field] = null;
break;
}
const date = new Date(value);
obj[colDef.field] = isNaN(date.getTime()) ? null : date;
break;
}
case 'boolean':
if (obj[colDef.field] !== true && obj[colDef.field] !== false) obj[colDef.field] = false;
default:
break;
}
return customFormat ? customFormat(obj) : obj;
};
class DefaultManagement {
static Tokens = {};
// Helper functions for managing serviceOptions ID filter
static setIdFilter(id, itemId) {
if (!DefaultManagement.Tokens[id]) {
DefaultManagement.Tokens[id] = {};
}
if (!DefaultManagement.Tokens[id].serviceOptions) {
DefaultManagement.Tokens[id].serviceOptions = {};
}
if (!DefaultManagement.Tokens[id].serviceOptions.get) {
DefaultManagement.Tokens[id].serviceOptions.get = {};
}
DefaultManagement.Tokens[id].serviceOptions.get.id = itemId;
}
static clearIdFilter(id) {
if (DefaultManagement.Tokens[id]?.serviceOptions?.get?.id) {
delete DefaultManagement.Tokens[id].serviceOptions.get.id;
}
}
static getIdFilter(id) {
return DefaultManagement.Tokens[id]?.serviceOptions?.get?.id ?? undefined;
}
static waitGridReady(id) {
return new Promise((resolve) => {
if (DefaultManagement.Tokens[id]?.gridApi) {
return resolve(DefaultManagement.Tokens[id].gridApi);
}
if (!DefaultManagement.Tokens[id].readyGridEvent) DefaultManagement.Tokens[id].readyGridEvent = {};
DefaultManagement.Tokens[id].readyGridEvent['waitGridReady'] = (params) => {
delete DefaultManagement.Tokens[id].readyGridEvent['waitGridReady'];
resolve(params.api);
};
});
}
static async runIsolated(id, callback) {
if (!DefaultManagement.Tokens[id]) return await callback();
DefaultManagement.Tokens[id].isProcessingQueryChange = true;
try {
return await callback();
} finally {
DefaultManagement.Tokens[id].isProcessingQueryChange = false;
}
}
static async loadTable(id, options = {}) {
options = { reload: true, force: true, createHistory: false, skipUrlUpdate: false, ...options };
try {
if (!DefaultManagement.Tokens[id]) {
logger.warn(`DefaultManagement loadTable - Token not found for id: ${id}`);
return;
}
const { serviceId, columnDefs, customFormat, gridId } = DefaultManagement.Tokens[id];
let _page = DefaultManagement.Tokens[id].page;
let _limit = DefaultManagement.Tokens[id].limit;
let _id = DefaultManagement.getIdFilter(id);
let filterModel = DefaultManagement.Tokens[id].filterModel || {};
let sortModel = DefaultManagement.Tokens[id].sortModel || [];
const gridApi = DefaultManagement.Tokens[id].gridApi || AgGrid.grids[gridId];
if (gridApi) {
filterModel = gridApi.getFilterModel() || {};
const columnState = gridApi.getColumnState();
if (columnState) {
sortModel = columnState
.filter((col) => col.sort)
.map((col) => ({ colId: col.colId, sort: col.sort, sortIndex: col.sortIndex }))
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0));
}
}
// Clean up filterModel and sortModel for URL params
const filterModelStr = Object.keys(filterModel).length > 0 ? JSON.stringify(filterModel) : null;
const sortModelStr = sortModel.length > 0 ? JSON.stringify(sortModel) : null;
// Update URL parameters to reflect current grid state
// Use pushState (createHistory) for filter/sort changes to enable browser back/forward
// Skip URL update when handling browser navigation to avoid interfering with history
if (!options.skipUrlUpdate) {
const urlParams = {
page: _page,
limit: _limit,
filterModel: filterModelStr,
sortModel: sortModelStr,
id: _id ? _id : '',
};
setQueryParams(urlParams, { replace: !options.createHistory });
}
if (!options.force && DefaultManagement.Tokens[id].lastOptions) {
const last = DefaultManagement.Tokens[id].lastOptions;
if (
_page === last.page &&
_limit === last.limit &&
_id === last.id &&
JSON.stringify(filterModel) === JSON.stringify(last.filterModel) &&
JSON.stringify(sortModel) === JSON.stringify(last.sortModel)
) {
logger.warn(`DefaultManagement loadTable ${serviceId} - Skipping load, options unchanged`);
return;
}
}
DefaultManagement.Tokens[id].lastOptions = {
page: _page,
limit: _limit,
id: _id,
filterModel,
sortModel,
};
// Update tokens with current state
DefaultManagement.Tokens[id].filterModel = filterModel;
DefaultManagement.Tokens[id].sortModel = sortModel;
const queryOptions = {
page: _page,
limit: _limit,
};
if (_id) {
queryOptions.id = _id;
}
if (filterModel && Object.keys(filterModel).length > 0) {
queryOptions.filterModel = filterModel;
}
if (sortModel && sortModel.length > 0) {
queryOptions.sortModel = sortModel;
// Legacy simple sort support
if (sortModel.length === 1) {
queryOptions.sort = sortModel[0].colId;
queryOptions.asc = sortModel[0].sort === 'asc' ? '1' : '0';
}
}
logger.info(`Loading table ${serviceId}`, {
id,
idFilter: _id,
page: _page,
limit: _limit,
hasFilters: Object.keys(filterModel).length > 0,
});
if (!DefaultManagement.Tokens[id] || !DefaultManagement.Tokens[id].ServiceProvider) {
logger.warn(`DefaultManagement loadTable ${serviceId} - ServiceProvider not found for token ${id}`);
return;
}
const result = await DefaultManagement.Tokens[id].ServiceProvider.get(queryOptions);
if (result.status === 'success') {
let data, total, page, totalPages;
// Handle both single object (when querying by ID) and paginated array responses
if (queryOptions.id && result.data && !Array.isArray(result.data.data)) {
// Single object response when filtering by ID
data = [result.data];
total = 1;
page = 1;
totalPages = 1;
} else {
// Paginated array response
({ data = [], total = 0, page = 1, totalPages = 1 } = result.data || {});
}
DefaultManagement.Tokens[id].total = total;
DefaultManagement.Tokens[id].page = page;
DefaultManagement.Tokens[id].totalPages = totalPages;
const rowDataScope = data.map((row) => columnDefFormatter(row, columnDefs, customFormat));
if (options.reload) {
const grid = AgGrid.grids[DefaultManagement.Tokens[id].gridId];
if (grid && grid.setGridOption) {
grid.setGridOption('rowData', rowDataScope);
} else {
logger.warn(`Grid ${gridId} not found or not ready for setGridOption`);
}
}
const paginationComp = s(`#ag-pagination-${DefaultManagement.Tokens[id].gridId}`);
if (paginationComp) {
paginationComp.setAttribute('current-page', DefaultManagement.Tokens[id].page);
paginationComp.setAttribute('total-pages', DefaultManagement.Tokens[id].totalPages);
paginationComp.setAttribute('total-items', DefaultManagement.Tokens[id].total);
} else {
logger.warn(`Pagination component not found for grid ${gridId}`);
}
setTimeout(async () => {
if (DefaultManagement.Tokens[id].readyRowDataEvent)
for (const event of Object.keys(DefaultManagement.Tokens[id].readyRowDataEvent))
await DefaultManagement.Tokens[id].readyRowDataEvent[event](rowDataScope);
}, 1);
// Update clear filter button visibility
DefaultManagement.updateClearFilterButtonVisibility(id);
} else {
logger.error(`Failed to load table ${serviceId}:`, result);
}
} catch (error) {
logger.error(`Error in loadTable for ${id}:`, error);
throw error;
}
}
static hasActiveFilters(id) {
const gridId = DefaultManagement.Tokens[id]?.gridId;
if (!gridId) return false;
const gridApi = AgGrid.grids[gridId];
const filterModel = gridApi ? gridApi.getFilterModel() : {};
const idFilter = DefaultManagement.getIdFilter(id);
const sortModel = DefaultManagement.Tokens[id]?.sortModel || [];
return Object.keys(filterModel).length > 0 || !!idFilter || sortModel.length > 0;
}
static updateClearFilterButtonVisibility(id) {
const clearFilterBtn = s(`.management-table-btn-clear-filter-${id}`);
if (!clearFilterBtn) return;
if (DefaultManagement.hasActiveFilters(id)) {
clearFilterBtn.classList.remove('hide');
} else {
clearFilterBtn.classList.add('hide');
}
}
static async refreshTable(id) {
const gridApi = AgGrid.grids[DefaultManagement.Tokens[id].gridId];
if (gridApi) {
// Use refreshCells with change detection for optimal performance
// This is preferred over redrawRows() as it only updates changed cells
gridApi.refreshCells({
force: false, // Use change detection - only refresh cells whose values have changed
suppressFlash: false, // Show flash animation for changed cells (requires enableCellChangeFlash)
});
}
}
static async instance(options = DefaultOptions) {
if (!options) options = DefaultOptions;
const { serviceId, columnDefs, entity, defaultColKeyFocus, ServiceProvider, permissions, paginationOptions } =
options;
logger.info('DefaultManagement instance', options);
const id = options?.idModal ? options.idModal : getId(DefaultManagement.Tokens, `${serviceId}-`);
const gridId = `${serviceId}-grid-${id}`;
const queryParamsListenerId = `default-management-${id}`;
const queryParams = getQueryParams();
const page = parseInt(queryParams.page) || 1;
const defaultLimit = paginationOptions?.limitOptions?.[0] || 10;
const limit = parseInt(queryParams.limit) || defaultLimit;
const urlId = queryParams.id || undefined;
let filterModel = {};
let sortModel = [];
try {
if (queryParams.filterModel) filterModel = JSON.parse(queryParams.filterModel);
if (queryParams.sortModel) sortModel = JSON.parse(queryParams.sortModel);
} catch (e) {
logger.warn('Error parsing filter/sort model from URL', e);
}
// Enhance column definitions for Date filtering and ensure colId
const enhancedColumnDefs = columnDefs.map((col) => {
const enhancedCol = {
...col,
colId: col.field, // Ensure colId matches field
};
if (enhancedCol.cellDataType === 'date' || enhancedCol.filter === 'agDateColumnFilter') {
enhancedCol.filter = 'agDateColumnFilter';
// Value getter to ensure date is properly parsed
if (!enhancedCol.valueGetter) {
enhancedCol.valueGetter = (params) => {
const value = params.data?.[enhancedCol.field];
if (!value) return null;
const date = new Date(value);
return isNaN(date.getTime()) ? null : date;
};
}
// Value formatter for display
if (!enhancedCol.valueFormatter) {
enhancedCol.valueFormatter = (params) => {
if (!params.value) return '';
const date = new Date(params.value);
if (isNaN(date.getTime())) return '';
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
};
}
enhancedCol.filterParams = {
comparator: (filterLocalDateAtMidnight, cellValue) => {
if (cellValue == null) return -1;
const cellDate = new Date(cellValue);
if (isNaN(cellDate.getTime())) return -1;
// Compare dates (ignoring time)
const cellTime = new Date(cellDate).setHours(0, 0, 0, 0);
const filterTime = filterLocalDateAtMidnight.getTime();
if (filterTime === cellTime) return 0;
if (cellTime < filterTime) return -1;
if (cellTime > filterTime) return 1;
return 0;
},
browserDatePicker: true,
minValidYear: 2000,
maxValidYear: 2100,
inRangeInclusive: true,
debounceMs: 500,
...enhancedCol.filterParams,
};
}
return enhancedCol;
});
class RemoveActionGridRenderer {
eGui;
tokens;
async init(params) {
this.eGui = document.createElement('div');
this.tokens = {};
const { rowIndex } = params;
const { createdAt, updatedAt } = params.data;
const cellRenderId = getId(this.tokens, `${serviceId}-`);
this.tokens[cellRenderId] = {};
this.eGui.innerHTML = html` ${await BtnIcon.instance({
label: html`<div class="abs center">
<i class="fas fa-times"></i>
</div> `,
class: `in fll section-mp management-table-btn-mini management-table-btn-remove-${id}-${cellRenderId} ${!params.data._id ? 'hide' : ''}`,
})}`;
setTimeout(() => {
EventsUI.onClick(
`.management-table-btn-remove-${id}-${cellRenderId}`,
async () => {
const confirmResult = await Modal.RenderConfirm({
html: async () => {
return html`
<div class="in section-mp" style="text-align: center">
${Translate.instance('confirm-delete-item')}
${Object.keys(params.data).length > 0
? html`<br />
"${options.defaultColKeyFocus
? getValueFromJoinString(params.data, options.defaultColKeyFocus)
: params.data[Object.keys(params.data)[0]]}"`
: ''}
</div>
`;
},
id: `delete-${params.data._id}`,
});
if (confirmResult.status !== 'confirm') return;
let result;
if (params.data._id) result = await ServiceProvider.delete({ id: params.data._id });
else result = { status: 'success' };
NotificationManager.Push({
html: result.status === 'error' ? result.message : Translate.instance('item-success-delete'),
status: result.status,
});
if (result.status === 'success') {
AgGrid.grids[gridId].applyTransaction({ remove: [params.data] });
const token = DefaultManagement.Tokens[id];
// if we are on the last page and we delete the last item, go to the previous page
const newTotal = token.total - 1;
const newTotalPages = Math.ceil(newTotal / token.limit);
if (token.page > newTotalPages && newTotalPages > 0) {
token.page = newTotalPages;
}
// reload the current page
await DefaultManagement.loadTable(id, { reload: false });
}
},
{ context: 'modal' },
);
});
}
getGui() {
return this.eGui;
}
refresh(params) {
return true;
}
}
const finalColumnDefs = (enhancedColumnDefs || []).concat(
permissions.remove
? [
{
field: 'remove-action',
headerName: '',
width: 100,
cellRenderer: RemoveActionGridRenderer,
editable: false,
},
]
: [],
);
DefaultManagement.Tokens[id] = {
...DefaultManagement.Tokens[id],
...options,
columnDefs: finalColumnDefs, // Use enhanced definitions including actions
gridId,
page,
limit,
total: 0,
totalPages: 1,
filterModel,
sortModel,
isInitializing: true, // Flag to prevent double loading during grid ready
isProcessingQueryChange: false, // Flag to prevent listener recursion
};
// Initialize ID filter from query params if present
if (urlId) {
DefaultManagement.setIdFilter(id, urlId);
}
setQueryParams({
page,
limit,
id: urlId ? urlId : '',
filterModel: Object.keys(filterModel).length > 0 ? JSON.stringify(filterModel) : null,
sortModel: sortModel.length > 0 ? JSON.stringify(sortModel) : null,
});
setTimeout(async () => {
// https://www.ag-grid.com/javascript-data-grid/data-update-transactions/
// Initial loadTable is now called in onGridReady after grid is fully initialized
// {
// const result = await ServiceProvider.get();
// if (result.status === 'success') {
// rowDataScope = result.data.map((row) => columnDefFormatter(row, columnDefs, options.customFormat));
// AgGrid.grids[gridId].setGridOption('rowData', rowDataScope);
// }
// }
s(`.management-table-btn-save-${id}`).onclick = () => {
s(`.management-table-btn-save-${id}`).classList.add('hide');
// s(`.management-table-btn-stop-${id}`).classList.add('hide');
if (permissions.add) s(`.management-table-btn-add-${id}`).classList.remove('hide');
if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.remove('hide');
if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.remove('hide');
AgGrid.grids[gridId].stopEditing();
};
EventsUI.onClick(`.management-table-btn-add-${id}`, async () => {
if (options.customEvent && options.customEvent.add) return await options.customEvent.add();
const rowObj = {};
for (const def of columnDefs) {
rowObj[def.field] = '';
}
// const result = await ServiceProvider.post({ body: rowObj });
const result = {
status: 'success',
data: rowObj,
};
// NotificationManager.Push({
// html: result.status === 'error' ? result.message : `${Translate.instance('success-create-item')}`,
// status: result.status,
// });
if (result.status === 'success') {
AgGrid.grids[gridId].applyTransaction({
add: [columnDefFormatter(rowObj, columnDefs, options.customFormat)],
addIndex: 0,
});
// AgGrid.grids[gridId].applyTransaction({
// add: [columnDefFormatter(result.data, columnDefs, options.customFormat)],
// addIndex: 0,
// });
// AgGrid.grids[gridId].applyColumnState({
// state: [
// // { colId: 'country', sort: 'asc', sortIndex: 1 },
// { colId: 'updatedAt', sort: 'desc', sortIndex: 0 },
// ],
// defaultState: { sort: null },
// });
} else {
// AgGrid.grids[gridId].applyColumnState({
// defaultState: { sort: null },
// });
}
// https://www.ag-grid.com/javascript-data-grid/cell-editing-start-stop/
const pinned = undefined;
const key = undefined;
// setFocusedCell = (
// rowIndex: number,
// colKey: string | Column,
// rowPinned?: RowPinnedType
// ) => void;
// type RowPinnedType =
// 'top'
// | 'bottom'
// | null
// | undefined
AgGrid.grids[gridId].setFocusedCell(0, '0', pinned);
// interface StartEditingCellParams {
// // The row index of the row to start editing
// rowIndex: number;
// // The column key of the row to start editing
// colKey: string | Column;
// // Set to `'top'` or `'bottom'` to start editing a pinned row
// rowPinned?: RowPinnedType;
// // The key to pass to the cell editor
// key?: string;
// }
setTimeout(() => {
s(`.management-table-btn-save-${id}`).classList.remove('hide');
// s(`.management-table-btn-stop-${id}`).classList.remove('hide');
if (permissions.add) s(`.management-table-btn-add-${id}`).classList.add('hide');
if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.add('hide');
if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.add('hide');
AgGrid.grids[gridId].startEditingCell({
rowIndex: 0,
colKey: defaultColKeyFocus,
rowPinned: pinned,
key: key,
});
});
});
EventsUI.onClick(`.management-table-btn-stop-${id}`, async () => {
s(`.management-table-btn-save-${id}`).classList.add('hide');
// s(`.management-table-btn-stop-${id}`).classList.add('hide');
if (permissions.add) s(`.management-table-btn-add-${id}`).classList.remove('hide');
if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.remove('hide');
if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.remove('hide');
AgGrid.grids[gridId].stopEditing();
});
EventsUI.onClick(`.management-table-btn-clean-${id}`, async () => {
const confirmResult = await Modal.RenderConfirm(
{
html: async () => {
return html`
<div class="in section-mp" style="text-align: center;">
<strong>${Translate.instance('confirm-delete-all-data')}</strong>
</div>
`;
},
id: `clean-table-${id}`,
},
{ context: 'modal' },
);
if (confirmResult.status === 'cancelled') return;
const result = await ServiceProvider.delete();
NotificationManager.Push({
html: result.status === 'error' ? result.message : Translate.instance('success-delete-all-items'),
status: result.status,
});
if (result.status === 'success') {
DefaultManagement.loadTable(id);
}
});
// Listen to query parameter changes for browser back/forward navigation
listenQueryParamsChange({
id: queryParamsListenerId,
event: (queryParams) => {
// Prevent recursion - if we're already processing a query change, skip
if (DefaultManagement.Tokens[id].isProcessingQueryChange) {
return;
}
const newPage = parseInt(queryParams.page, 10) || 1;
const newLimit = parseInt(queryParams.limit, 10) || DefaultManagement.Tokens[id].limit || 10;
const newFilterModel = queryParams.filterModel;
const newSortModel = queryParams.sortModel;
const newId = queryParams.id || undefined;
let shouldReload = false;
// Check if id parameter changed
const currentId = DefaultManagement.getIdFilter(id);
if (newId !== currentId) {
if (newId) {
DefaultManagement.setIdFilter(id, newId);
} else {
DefaultManagement.clearIdFilter(id);
}
shouldReload = true;
}
// Check if page or limit changed
if (newPage !== DefaultManagement.Tokens[id].page || newLimit !== DefaultManagement.Tokens[id].limit) {
DefaultManagement.Tokens[id].page = newPage;
DefaultManagement.Tokens[id].limit = newLimit;
shouldReload = true;
}
// Check if filter or sort changed by comparing with actual grid state
const gridApi = AgGrid.grids[gridId];
let filterChanged = false;
let sortChanged = false;
if (gridApi) {
// Get current grid filter state
const currentGridFilterModel = gridApi.getFilterModel() || {};
const currentGridFilterStr = JSON.stringify(currentGridFilterModel);
const newFilterStr = newFilterModel || '{}';
// Get current grid sort state
const currentColumnState = gridApi.getColumnState() || [];
const currentGridSortModel = currentColumnState
.filter((col) => col.sort)
.map((col) => ({ colId: col.colId, sort: col.sort, sortIndex: col.sortIndex }))
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0));
const currentGridSortStr = JSON.stringify(currentGridSortModel);
const newSortStr = newSortModel || '[]';
filterChanged = currentGridFilterStr !== newFilterStr;
sortChanged = currentGridSortStr !== newSortStr;
}
if (filterChanged || sortChanged) {
// Parse and apply the new filter/sort models
try {
DefaultManagement.Tokens[id].filterModel = newFilterModel ? JSON.parse(newFilterModel) : {};
} catch (e) {
DefaultManagement.Tokens[id].filterModel = {};
}
try {
DefaultManagement.Tokens[id].sortModel = newSortModel ? JSON.parse(newSortModel) : [];
} catch (e) {
DefaultManagement.Tokens[id].sortModel = [];
}
// Apply filters and sorts to the grid
if (gridApi) {
// Temporarily disable filter/sort change handlers to prevent recursion
DefaultManagement.Tokens[id].isProcessingQueryChange = true;
if (filterChanged) {
gridApi.setFilterModel(DefaultManagement.Tokens[id].filterModel);
}
if (sortChanged) {
// Apply sort model
const columnState = DefaultManagement.Tokens[id].sortModel.map((sortItem) => ({
colId: sortItem.colId,
sort: sortItem.sort,
sortIndex: sortItem.sortIndex,
}));
if (columnState.length > 0) {
gridApi.applyColumnState({
state: columnState,
defaultState: { sort: null },
});
} else {
gridApi.applyColumnState({
defaultState: { sort: null },
});
}
}
// Re-enable handlers after a short delay
setTimeout(() => {
DefaultManagement.Tokens[id].isProcessingQueryChange = false;
}, 100);
}
shouldReload = true;
}
if (shouldReload) {
// Skip URL update since browser already changed it (back/forward navigation)
DefaultManagement.loadTable(id, { reload: true, force: true, createHistory: false, skipUrlUpdate: true });
}
},
});
EventsUI.onClick(`.management-table-btn-clear-filter-${id}`, async () => {
try {
const gridApi = AgGrid.grids[gridId];
await DefaultManagement.runIsolated(id, async () => {
// Clear all filters without letting grid/query listeners trigger their own reloads.
DefaultManagement.clearIdFilter(id);
if (gridApi) {
gridApi.setFilterModel({});
gridApi.applyColumnState({ defaultState: { sort: null } });
}
if (DefaultManagement.Tokens[id]) {
DefaultManagement.Tokens[id].filterModel = {};
DefaultManagement.Tokens[id].sortModel = [];
}
const queryParams = getQueryParams();
setQueryParams({
page: queryParams.page || 1,
limit: queryParams.limit || DefaultManagement.Tokens[id]?.limit || 10,
filterModel: null,
sortModel: null,
id: null,
});
await DefaultManagement.loadTable(id, { force: true, reload: true, skipUrlUpdate: true });
});
NotificationManager.Push({
html: Translate.instance('success-clear-filter') || 'Filters cleared',
status: 'success',
});
} catch (error) {
NotificationManager.Push({
html: error.message || 'Error clearing filters',
status: 'error',
});
}
});
EventsUI.onClick(`.management-table-btn-reload-${id}`, async () => {
try {
// Reload data from server
await DefaultManagement.loadTable(id, { force: true, reload: true });
// Other option: Refresh cells to update UI
// DefaultManagement.refreshTable(id);
NotificationManager.Push({
html: Translate.instance('success-reload-data') || 'Data reloaded successfully',
status: 'success',
});
} catch (error) {
NotificationManager.Push({
html: error.message || 'Error reloading data',
status: 'error',
});
} finally {
}
});
s(`#ag-pagination-${gridId}`).addEventListener('page-change', async (event) => {
const token = DefaultManagement.Tokens[id];
token.page = event.detail.page;
});
s(`#ag-pagination-${gridId}`).addEventListener('limit-change', async (event) => {
const token = DefaultManagement.Tokens[id];
token.limit = event.detail.limit;
token.page = 1; // Reset to first page
});
RouterEvents[id] = async (...args) => {
const queryParams = getQueryParams();
const page = parseInt(queryParams.page) || 1;
const limit = parseInt(queryParams.limit) || 10;
const urlId = queryParams.id || undefined;
let filterModel = {};
let sortModel = [];
try {
if (queryParams.filterModel) filterModel = JSON.parse(queryParams.filterModel);
if (queryParams.sortModel) sortModel = JSON.parse(queryParams.sortModel);
} catch (e) {}
const token = DefaultManagement.Tokens[id];
if (!token) {
// Token doesn't exist yet, table hasn't been initialized
return;
}
// Check if state in URL is different from current state
const currentId = DefaultManagement.getIdFilter(id);
const isIdChanged = currentId !== urlId;
const isPaginationChanged = token.page !== page || token.limit !== limit;
const isFilterChanged = JSON.stringify(token.filterModel || {}) !== JSON.stringify(filterModel);
const isSortChanged = JSON.stringify(token.sortModel || []) !== JSON.stringify(sortModel);
if (isPaginationChanged || isFilterChanged || isSortChanged || isIdChanged) {
// Update ID filter from query params
if (urlId) {
DefaultManagement.setIdFilter(id, urlId);
} else {
DefaultManagement.clearIdFilter(id);
}
token.page = page;
token.limit = limit;
token.filterModel = filterModel;
token.sortModel = sortModel;
// If grid is active, we should update its state to match URL
const gridApi = AgGrid.grids[gridId];
if (gridApi && gridApi.setFilterModel && gridApi.applyColumnState) {
// Updating filter/sort on grid will trigger onFilterChanged/onSortChanged -> loadTable
// But we must ensure it happens.
if (isFilterChanged) {
try {
gridApi.setFilterModel(filterModel);
} catch (e) {
console.warn('Failed to set filter model:', e);
}
}
if (isSortChanged) {
try {
gridApi.applyColumnState({
state: sortModel,
defaultState: { sort: null },
});
} catch (e) {
console.warn('Failed to apply column state:', e);
}
}
// If pagination or ID changed, and no grid-level filter/sort changed, we must load manually
if ((isPaginationChanged || isIdChanged) && !isFilterChanged && !isSortChanged) {
await DefaultManagement.loadTable(id);
}
} else if (!gridApi) {
// Grid doesn't exist yet, just load the table data when it's ready
// The onGridReady event will handle applying the initial state
logger.warn(`Grid not ready for ${gridId}, state will be applied on grid ready`);
}
}
};
}, 1);
return html`<div class="fl management-table-toolbar">
${await BtnIcon.instance({
class: `in fll section-mp management-table-btn-mini management-table-btn-add-${id} ${permissions.add ? '' : 'hide'}`,
label: html`<div class="abs center btn-add-${id}-label"><i class="fa-solid fa-circle-plus"></i></div> `,
type: 'button',
})}
${await BtnIcon.instance({
class: `in fll section-mp management-table-btn-mini management-table-btn-save-${id} hide`,
label: html`<div class="abs center btn-save-${id}-label"><i class="fas fa-save"></i></div> `,
type: 'button',
})}
${await BtnIcon.instance({
class: `in fll section-mp management-table-btn-mini management-table-btn-stop-${id} hide`,
label: html`<div class="abs center btn-save-${id}-label"><i class="fa-solid fa-rectangle-xmark"></i></div> `,
type: 'button',
})}
${await BtnIcon.instance({
class: `in fll section-mp management-table-btn-mini management-table-btn-clean-${id} ${permissions.remove ? '' : 'hide'}`,
label: html`<div class="abs center btn-clean-${id}-label"><i class="fas fa-broom"></i></div> `,
type: 'button',
})}
${await BtnIcon.instance({
class: `in fll section-mp management-table-btn-mini management-table-btn-clear-filter-${id} ${Object.keys(filterModel).length > 0 || sortModel.length > 0 || urlId ? '' : 'hide'}`,
label: html`<div class="abs center btn-clear-filter-${id}-label">
<i class="fa-solid fa-filter-circle-xmark"></i>
</div> `,
type: 'button',
})}
${await BtnIcon.instance({
class: `in fll section-mp management-table-btn-mini management-table-btn-reload-${id} ${permissions.reload ? '' : 'hide'}`,
label: html`<div class="abs center btn-reload-${id}-label"><i class="fas fa-sync-alt"></i></div> `,
type: 'button',
})}
</div>
<div class="in section-mp">
${await AgGrid.instance({
id: gridId,
parentModal: options.idModal,
usePagination: true,
paginationOptions,
customHeightOffset:
options.customHeightOffset !== undefined
? options.customHeightOffset
: !permissions.add && !permissions.remove && !permissions.reload
? 50
: 0,
darkTheme: typeof darkTheme !== 'undefined' ? darkTheme : false,
gridOptions: {
columnDefs: finalColumnDefs,
getRowClass: (params) => {
const idFilter = DefaultManagement.getIdFilter(id);
if (idFilter && params.data && params.data._id === idFilter) {
return 'row-new-highlight';
}
},
defaultColDef: {
flex: 1,
editable: true,
cellDataType: false,
minWidth: 100,
filter: true,
autoHeight: true,
},
onGridReady: async (params) => {
DefaultManagement.Tokens[id].gridApi = params.api;
if (DefaultManagement.Tokens[id].readyGridEvent) {
for (const key of Object.keys(DefaultManagement.Tokens[id].readyGridEvent)) {
await DefaultManagement.Tokens[id].readyGridEvent[key](params);
}
}
params.api.setGridOption('columnDefs', finalColumnDefs);
// Apply initial state from URL
const { filterModel, sortModel } = DefaultManagement.Tokens[id];
if (filterModel && Object.keys(filterModel).length > 0) {
params.api.setFilterModel(filterModel);
}
if (sortModel && sortModel.length > 0) {
params.api.applyColumnState({
state: sortModel,
defaultState: { sort: null },
});
}
// Load initial data now that grid is ready
// Filter/sort state has been applied, this will fetch data from server
DefaultManagement.loadTable(id).finally(() => {
// Mark initialization complete to allow future filter/sort events
DefaultManagement.Tokens[id].isInitializing = false;
});
},
onFilterChanged: () => {
// Skip if still initializing (state being applied in onGridReady)
if (DefaultManagement.Tokens[id].isInitializing) return;
// Skip if we're processing a query change from browser navigation
if (DefaultManagement.Tokens[id].isProcessingQueryChange) return;
// Reset to page 1 on filter change
DefaultManagement.Tokens[id].page = 1;
// Update clear filter button visibility
DefaultManagement.updateClearFilterButtonVisibility(id);
// Create history entry for filter changes
DefaultManagement.loadTable(id, { reload: true, force: true, createHistory: true });
},
onSortChanged: () => {
// Skip if still initializing (state being applied in onGridReady)
if (DefaultManagement.Tokens[id].isInitializing) return;
// Skip if we're processing a query change from browser navigation
if (DefaultManagement.Tokens[id].isProcessingQueryChange) return;
// Update clear filter button visibility
DefaultManagement.updateClearFilterButtonVisibility(id);
// Create history entry for sort changes
DefaultManagement.loadTable(id, { reload: true, force: true, createHistory: true });
},
editType: 'fullRow', // Keep fullRow for add new row, but cells will auto-save
// rowData: [],
onCellValueChanged: async (...args) => {
const [event] = args;
// Only auto-save for existing rows (with _id), not new rows
if (event.data && event.data._id) {
logger.info('onCellValueChanged - auto-saving', args);
const body = event.data ? event.data : {};
const result = await ServiceProvider.put({ id: event.data._id, body });
NotificationManager.Push({
html: result.status === 'error' ? result.message : `${Translate.instance('success-update-item')}`,
status: result.status,
});
if (result.status === 'success') {
AgGrid.grids[gridId].applyTransaction({
update: [event.data],
});
// Restore default buttons after save
s(`.management-table-btn-save-${id}`).classList.add('hide');
if (permissions.add) s(`.management-table-btn-add-${id}`).classList.remove('hide');
if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.remove('hide');
if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.remove('hide');
DefaultManagement.loadTable(id, { reload: false });
}
}
},
rowSelection: 'single',
onSelectionChanged: async (...args) => {
console.log('onSelectionChanged', args);
const [event] = args;
const selectedRows = AgGrid.grids[gridId].getSelectedRows();
logger.info('selectedRows', selectedRows);
},
onCellEditingStarted: async (...args) => {
const [event] = args;
// Only show save button for new rows (without _id)
if (event.data && !event.data._id) {
s(`.management-table-btn-save-${id}`).classList.remove('hide');
if (permissions.add) s(`.management-table-btn-add-${id}`).classList.add('hide');
if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.add('hide');
if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.add('hide');
}
},
onRowEditingStarted: async (...args) => {
// Show only save button when editing starts (for new rows)
s(`.management-table-btn-save-${id}`).classList.remove('hide');
if (permissions.add) s(`.management-table-btn-add-${id}`).classList.add('hide');
if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.add('hide');
if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.add('hide');
},
onRowValueChanged: async (...args) => {
const [event] = args;
logger.info('onRowValueChanged', args);
let result;
if (options.onRowValueChanged) {
const { status, data, message } = await options.onRowValueChanged(...args);
if (status === 'success' && data) event.data = data;
else {
NotificationManager.Push({
html: message,
status,
});
return;
}
}
if (!event.data._id) {
result = await ServiceProvider.post({ body: event.data });
NotificationManager.Push({
html: result.status === 'error' ? result.message : `${Translate.instance('success-create-item')}`,
status: result.status,
});
if (result.status === 'success') {
// Filter by the newly created row's ID
const newItemId = result.data._id;
// Update UI buttons first
s(`.management-table-btn-save-${id}`).classList.add('hide');
if (permissions.add) s(`.management-table-btn-add-${id}`).classList.remove('hide');
if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.remove('hide');
if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.remove('hide');
// Stop editing to avoid triggering other events
AgGrid.grids[gridId].stopEditing();
// Set ID filter and reload
DefaultManagement.Tokens[id].page = 1;
DefaultManagement.setIdFilter(id, newItemId);
setTimeout(async () => {
await DefaultManagement.loadTable(id, { force: true, createHistory: true });
});
}
} else {
// Skip update here - onCellValueChanged already handles auto-save for existing rows
// This prevents duplicate notifications and API calls
return;
}
},
...(options.gridOptions ? options.gridOptions : undefined),
},
})}
</div>`;
}
}
export { DefaultManagement };