forked from fa/breadcrumb-the-shire
refactor(js): migrate core list pages to list module factory
This commit is contained in:
@@ -37,6 +37,14 @@ else
|
||||
ok "Frontend JS uses centralized app-http (except app-http/app-telemetry infrastructure)"
|
||||
fi
|
||||
|
||||
list_module_hits="$(rg -n "initStandardListPage\\(" web/js/pages modules/*/web/js/pages -g '*.js' || true)"
|
||||
if [[ -n "${list_module_hits}" ]]; then
|
||||
fail "Legacy initStandardListPage(...) usage found in page modules (use createListPageModule):"
|
||||
echo "${list_module_hits}" >&2
|
||||
else
|
||||
ok "List page modules use createListPageModule (no direct initStandardListPage calls)"
|
||||
fi
|
||||
|
||||
runtime_files=(
|
||||
"modules/helpdesk/web/js/helpdesk-detail.js"
|
||||
"modules/helpdesk/web/js/helpdesk-domain-detail.js"
|
||||
|
||||
@@ -20,10 +20,10 @@ class ListTemplateContractTest extends TestCase
|
||||
$file . ' does not render toolbar directly and does not include shared list-filters partial.'
|
||||
);
|
||||
$this->assertTrue(
|
||||
str_contains($moduleContent, 'initStandardListPage(')
|
||||
|| str_contains($moduleContent, 'createListPageModule('),
|
||||
$file . ' does not initialize list module via initStandardListPage/createListPageModule.'
|
||||
str_contains($moduleContent, 'createListPageModule('),
|
||||
$file . ' does not initialize list module via createListPageModule.'
|
||||
);
|
||||
$this->assertStringNotContainsString('initStandardListPage(', $moduleContent, $file);
|
||||
$this->assertStringNotContainsString('gridFiltersFromSchema(', $content, $file);
|
||||
$this->assertSame(0, preg_match('/<select id=\"[^\"]*-filter\"/', $content), $file);
|
||||
$this->assertSame(0, preg_match('/<input[^>]*id=\"[^\"]*-filter\"/', $content), $file);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { initStandardListPage } from '../core/app-grid-factory.js';
|
||||
import { readPageConfig } from '../core/app-page-config.js';
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
import { badgeHtml, getAppBase } from './app-list-utils.js';
|
||||
import { createListPageModule } from '../core/app-list-page-module.js';
|
||||
import { badgeHtml } from './app-list-utils.js';
|
||||
|
||||
const config = readPageConfig('admin-departments-index');
|
||||
if (config) {
|
||||
const appBase = getAppBase();
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
createListPageModule({
|
||||
configId: 'admin-departments-index',
|
||||
moduleId: 'admin-departments-index',
|
||||
missingGridMessage: 'Departments grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Departments grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
|
||||
const initDepartmentsGrid = () => {
|
||||
const formatBadge = (value) => badgeHtml(gridjs, value);
|
||||
const formatCount = (value) => {
|
||||
const number = Number(value ?? 0);
|
||||
@@ -28,76 +28,75 @@ if (config) {
|
||||
const label = isInactive ? statusLabels.inactive : statusLabels.active;
|
||||
return gridjs.html(`<span class="badge" data-variant="${variant}">${label}</span>`);
|
||||
};
|
||||
const gridOptions = {
|
||||
gridjs: window.gridjs,
|
||||
container: '#departments-grid',
|
||||
dataUrl: 'admin/departments/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{ name: labels.description || 'Description', sort: true },
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: false,
|
||||
formatter: (cell) => formatStatus(cell),
|
||||
},
|
||||
{ name: labels.code || 'Code', sort: true },
|
||||
{ name: labels.costCenter || 'Cost center', sort: true },
|
||||
{
|
||||
name: labels.activeUsers || 'Active users',
|
||||
sort: false,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.inactiveUsers || 'Inactive users',
|
||||
sort: false,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.created || 'Created',
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell),
|
||||
},
|
||||
{
|
||||
name: labels.modified || 'Modified',
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell),
|
||||
},
|
||||
{
|
||||
name: 'UUID',
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
sortColumns: ['description', null, 'code', 'cost_center', null, null, 'created', 'modified', null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.description,
|
||||
row.active,
|
||||
row.code,
|
||||
row.cost_center,
|
||||
row.active_users ?? 0,
|
||||
row.inactive_users ?? 0,
|
||||
row.created,
|
||||
row.modified,
|
||||
row.uuid,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[8]?.data,
|
||||
}),
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const uuid = rowData?.cells?.[8]?.data;
|
||||
return uuid ? new URL(`admin/departments/edit/${uuid}`, appBase).toString() : '';
|
||||
|
||||
const { gridConfig } = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#departments-grid',
|
||||
dataUrl: 'admin/departments/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{ name: labels.description || 'Description', sort: true },
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: false,
|
||||
formatter: (cell) => formatStatus(cell),
|
||||
},
|
||||
{ name: labels.code || 'Code', sort: true },
|
||||
{ name: labels.costCenter || 'Cost center', sort: true },
|
||||
{
|
||||
name: labels.activeUsers || 'Active users',
|
||||
sort: false,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.inactiveUsers || 'Inactive users',
|
||||
sort: false,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.created || 'Created',
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell),
|
||||
},
|
||||
{
|
||||
name: labels.modified || 'Modified',
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell),
|
||||
},
|
||||
{
|
||||
name: 'UUID',
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
sortColumns: ['description', null, 'code', 'cost_center', null, null, 'created', 'modified', null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.description,
|
||||
row.active,
|
||||
row.code,
|
||||
row.cost_center,
|
||||
row.active_users ?? 0,
|
||||
row.inactive_users ?? 0,
|
||||
row.created,
|
||||
row.modified,
|
||||
row.uuid,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[8]?.data,
|
||||
}),
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const uuid = rowData?.cells?.[8]?.data;
|
||||
return uuid ? new URL(`admin/departments/edit/${uuid}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { gridConfig } = initStandardListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: filterChipMeta,
|
||||
@@ -110,13 +109,8 @@ if (config) {
|
||||
preserveFilterParams: ['tenant'],
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Departments grid init failed', { module: 'admin-departments-index', component: 'grid' });
|
||||
return null;
|
||||
}
|
||||
return gridConfig;
|
||||
};
|
||||
}) || {};
|
||||
|
||||
initDepartmentsGrid();
|
||||
}
|
||||
return gridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,101 +1,94 @@
|
||||
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 { createListPageModule } from '../core/app-list-page-module.js';
|
||||
|
||||
const config = readPageConfig('admin-mail-log-index');
|
||||
if (config) {
|
||||
const appBase = getAppBase();
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
createListPageModule({
|
||||
configId: 'admin-mail-log-index',
|
||||
moduleId: 'admin-mail-log-index',
|
||||
missingGridMessage: 'Mail log grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Mail log grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
|
||||
const initMailLogGrid = () => {
|
||||
const gridOptions = {
|
||||
gridjs: window.gridjs,
|
||||
container: '#mail-log-grid',
|
||||
dataUrl: 'admin/mail-log/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.created || 'Created',
|
||||
sort: true,
|
||||
},
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html('');
|
||||
}
|
||||
const variant = cell.variant || 'neutral';
|
||||
const label = cell.label || '';
|
||||
return gridjs.html(`<span class="badge" data-variant="${variant}">${label}</span>`);
|
||||
const { gridConfig } = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#mail-log-grid',
|
||||
dataUrl: 'admin/mail-log/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.created || 'Created',
|
||||
sort: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.recipient || 'Recipient',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html('');
|
||||
}
|
||||
const variant = cell.variant || 'neutral';
|
||||
const label = cell.label || '';
|
||||
return gridjs.html(`<span class="badge" data-variant="${variant}">${label}</span>`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.recipient || 'Recipient',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html(String(cell ?? ''));
|
||||
}
|
||||
return gridjs.html(String(cell ?? ''));
|
||||
}
|
||||
return gridjs.html(String(cell ?? ''));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.subject || 'Subject',
|
||||
sort: true,
|
||||
},
|
||||
{
|
||||
name: labels.template || 'Template',
|
||||
sort: false,
|
||||
},
|
||||
{
|
||||
name: 'ID',
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
sortColumns: ['created_at', 'status', 'to_email', 'subject', null, null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.created_at,
|
||||
{
|
||||
variant: row.status_badge,
|
||||
label: row.status_label,
|
||||
},
|
||||
row.to_email,
|
||||
row.subject,
|
||||
row.template || '-',
|
||||
row.id,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 3 },
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const id = rowData?.cells?.[5]?.data;
|
||||
return id ? new URL(`admin/mail-log/view/${id}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.subject || 'Subject',
|
||||
sort: true,
|
||||
},
|
||||
{
|
||||
name: labels.template || 'Template',
|
||||
sort: false,
|
||||
},
|
||||
{
|
||||
name: 'ID',
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
sortColumns: ['created_at', 'status', 'to_email', 'subject', null, null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.created_at,
|
||||
{
|
||||
variant: row.status_badge,
|
||||
label: row.status_label,
|
||||
},
|
||||
row.to_email,
|
||||
row.subject,
|
||||
row.template || '-',
|
||||
row.id,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 3 },
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const id = rowData?.cells?.[5]?.data;
|
||||
return id ? new URL(`admin/mail-log/view/${id}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
const { gridConfig } = initStandardListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: filterChipMeta,
|
||||
watchInputs: ['#mail-log-search'],
|
||||
},
|
||||
});
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Mail log grid init failed', { module: 'admin-mail-log-index', component: 'grid' });
|
||||
return null;
|
||||
}
|
||||
return gridConfig;
|
||||
};
|
||||
}) || {};
|
||||
|
||||
initMailLogGrid();
|
||||
}
|
||||
return gridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,79 +1,72 @@
|
||||
import { initStandardListPage } from '../core/app-grid-factory.js';
|
||||
import { readPageConfig } from '../core/app-page-config.js';
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
import { badgeHtml, getAppBase } from './app-list-utils.js';
|
||||
import { createListPageModule } from '../core/app-list-page-module.js';
|
||||
import { badgeHtml } from './app-list-utils.js';
|
||||
|
||||
const config = readPageConfig('admin-permissions-index');
|
||||
if (config) {
|
||||
const appBase = getAppBase();
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
createListPageModule({
|
||||
configId: 'admin-permissions-index',
|
||||
moduleId: 'admin-permissions-index',
|
||||
missingGridMessage: 'Permissions grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Permissions grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
|
||||
const initPermissionsGrid = () => {
|
||||
const gridOptions = {
|
||||
gridjs: window.gridjs,
|
||||
container: '#permissions-grid',
|
||||
dataUrl: 'admin/permissions/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{ name: labels.description || 'Description', sort: true },
|
||||
{ name: labels.permissionKey || 'Permission key', sort: true },
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: true,
|
||||
formatter: (cell) => cell
|
||||
? gridjs.html(`<span class="badge" data-variant="success">${labels.active || 'Active'}</span>`)
|
||||
: gridjs.html(`<span class="badge" data-variant="danger">${labels.inactive || 'Inactive'}</span>`),
|
||||
},
|
||||
{
|
||||
name: labels.system || 'System',
|
||||
sort: false,
|
||||
formatter: (cell) => cell
|
||||
? gridjs.html(`<span class="badge" data-variant="neutral">${labels.system || 'System'}</span>`)
|
||||
: gridjs.html(''),
|
||||
},
|
||||
{ name: labels.created || 'Created', sort: true, formatter: (cell) => badgeHtml(gridjs, cell) },
|
||||
{ name: 'ID', hidden: true },
|
||||
],
|
||||
sortColumns: ['description', 'key', 'active', null, 'created', null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.description,
|
||||
row.key,
|
||||
row.active,
|
||||
row.is_system,
|
||||
row.created,
|
||||
row.id,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
rowDblClick: {
|
||||
getUrl: (row) => {
|
||||
const id = row?.cells?.[5]?.data;
|
||||
return id ? new URL(`admin/permissions/edit/${id}`, appBase).toString() : '';
|
||||
const { gridConfig } = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#permissions-grid',
|
||||
dataUrl: 'admin/permissions/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{ name: labels.description || 'Description', sort: true },
|
||||
{ name: labels.permissionKey || 'Permission key', sort: true },
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: true,
|
||||
formatter: (cell) => cell
|
||||
? gridjs.html(`<span class="badge" data-variant="success">${labels.active || 'Active'}</span>`)
|
||||
: gridjs.html(`<span class="badge" data-variant="danger">${labels.inactive || 'Inactive'}</span>`),
|
||||
},
|
||||
{
|
||||
name: labels.system || 'System',
|
||||
sort: false,
|
||||
formatter: (cell) => cell
|
||||
? gridjs.html(`<span class="badge" data-variant="neutral">${labels.system || 'System'}</span>`)
|
||||
: gridjs.html(''),
|
||||
},
|
||||
{ name: labels.created || 'Created', sort: true, formatter: (cell) => badgeHtml(gridjs, cell) },
|
||||
{ name: 'ID', hidden: true },
|
||||
],
|
||||
sortColumns: ['description', 'key', 'active', null, 'created', null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.description,
|
||||
row.key,
|
||||
row.active,
|
||||
row.is_system,
|
||||
row.created,
|
||||
row.id,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
rowDblClick: {
|
||||
getUrl: (row) => {
|
||||
const id = row?.cells?.[5]?.data;
|
||||
return id ? new URL(`admin/permissions/edit/${id}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
},
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
};
|
||||
|
||||
const { gridConfig } = initStandardListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: filterChipMeta,
|
||||
watchInputs: ['#permission-search'],
|
||||
},
|
||||
});
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Permissions grid init failed', { module: 'admin-permissions-index', component: 'grid' });
|
||||
return null;
|
||||
}
|
||||
return gridConfig;
|
||||
};
|
||||
}) || {};
|
||||
|
||||
initPermissionsGrid();
|
||||
}
|
||||
return gridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,127 +1,121 @@
|
||||
import { initStandardListPage } from '../core/app-grid-factory.js';
|
||||
import { readPageConfig } from '../core/app-page-config.js';
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
import { badgeHtml, getAppBase } from './app-list-utils.js';
|
||||
import { createListPageModule } from '../core/app-list-page-module.js';
|
||||
import { badgeHtml } from './app-list-utils.js';
|
||||
|
||||
const config = readPageConfig('admin-roles-index');
|
||||
if (config) {
|
||||
const appBase = getAppBase();
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
createListPageModule({
|
||||
configId: 'admin-roles-index',
|
||||
moduleId: 'admin-roles-index',
|
||||
missingGridMessage: 'Roles grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Roles grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
|
||||
const initRolesGrid = () => {
|
||||
const formatBadge = (value) => badgeHtml(gridjs, value);
|
||||
const formatCount = (value) => {
|
||||
const number = Number(value ?? 0);
|
||||
return Number.isFinite(number) ? number : 0;
|
||||
};
|
||||
const gridOptions = {
|
||||
gridjs: window.gridjs,
|
||||
container: '#roles-grid',
|
||||
dataUrl: 'admin/roles/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.description || 'Description',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html(String(cell ?? ''));
|
||||
}
|
||||
const label = cell.label ?? '';
|
||||
return gridjs.html(`${label}`);
|
||||
|
||||
const { gridConfig } = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#roles-grid',
|
||||
dataUrl: 'admin/roles/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.description || 'Description',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html(String(cell ?? ''));
|
||||
}
|
||||
const label = cell.label ?? '';
|
||||
return gridjs.html(`${label}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: true,
|
||||
formatter: (cell) => cell
|
||||
? gridjs.html(`<span class="badge" data-variant="success">${labels.active || 'Active'}</span>`)
|
||||
: gridjs.html(`<span class="badge" data-variant="danger">${labels.inactive || 'Inactive'}</span>`),
|
||||
},
|
||||
{
|
||||
name: labels.code || 'Code',
|
||||
sort: true,
|
||||
formatter: (cell) => gridjs.html(String(cell ?? '')),
|
||||
},
|
||||
{
|
||||
name: labels.activeUsers || 'Active users',
|
||||
sort: false,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.inactiveUsers || 'Inactive users',
|
||||
sort: false,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.permissions || 'Permissions',
|
||||
sort: false,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.created || 'Created',
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell),
|
||||
},
|
||||
{
|
||||
name: labels.modified || 'Modified',
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell),
|
||||
},
|
||||
{
|
||||
name: 'UUID',
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
sortColumns: ['description', 'active', 'code', null, null, null, 'created', 'modified', null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
{
|
||||
label: row.description,
|
||||
is_default: row.is_default ? 1 : 0,
|
||||
},
|
||||
row.active,
|
||||
row.code,
|
||||
row.active_users ?? 0,
|
||||
row.inactive_users ?? 0,
|
||||
row.permission_count ?? 0,
|
||||
row.created,
|
||||
row.modified,
|
||||
row.uuid,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[8]?.data,
|
||||
}),
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const uuid = rowData?.cells?.[8]?.data;
|
||||
return uuid ? new URL(`admin/roles/edit/${uuid}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: true,
|
||||
formatter: (cell) => cell
|
||||
? gridjs.html(`<span class="badge" data-variant="success">${labels.active || 'Active'}</span>`)
|
||||
: gridjs.html(`<span class="badge" data-variant="danger">${labels.inactive || 'Inactive'}</span>`),
|
||||
},
|
||||
{
|
||||
name: labels.code || 'Code',
|
||||
sort: true,
|
||||
formatter: (cell) => gridjs.html(String(cell ?? '')),
|
||||
},
|
||||
{
|
||||
name: labels.activeUsers || 'Active users',
|
||||
sort: false,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.inactiveUsers || 'Inactive users',
|
||||
sort: false,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.permissions || 'Permissions',
|
||||
sort: false,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.created || 'Created',
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell),
|
||||
},
|
||||
{
|
||||
name: labels.modified || 'Modified',
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell),
|
||||
},
|
||||
{
|
||||
name: 'UUID',
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
sortColumns: ['description', 'active', 'code', null, null, null, 'created', 'modified', null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
{
|
||||
label: row.description,
|
||||
is_default: row.is_default ? 1 : 0,
|
||||
},
|
||||
row.active,
|
||||
row.code,
|
||||
row.active_users ?? 0,
|
||||
row.inactive_users ?? 0,
|
||||
row.permission_count ?? 0,
|
||||
row.created,
|
||||
row.modified,
|
||||
row.uuid,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[8]?.data,
|
||||
}),
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const uuid = rowData?.cells?.[8]?.data;
|
||||
return uuid ? new URL(`admin/roles/edit/${uuid}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { gridConfig: standardGridConfig } = initStandardListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: filterChipMeta,
|
||||
watchInputs: ['#role-search'],
|
||||
},
|
||||
});
|
||||
if (!standardGridConfig || !standardGridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Roles grid init failed', { module: 'admin-roles-index', component: 'grid' });
|
||||
return null;
|
||||
}
|
||||
return standardGridConfig;
|
||||
};
|
||||
}) || {};
|
||||
|
||||
initRolesGrid();
|
||||
}
|
||||
return gridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,97 +1,94 @@
|
||||
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 { createListPageModule } from '../core/app-list-page-module.js';
|
||||
|
||||
const config = readPageConfig('admin-scheduled-jobs-index');
|
||||
if (config) {
|
||||
const appBase = getAppBase();
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
createListPageModule({
|
||||
configId: 'admin-scheduled-jobs-index',
|
||||
moduleId: 'admin-scheduled-jobs-index',
|
||||
missingGridMessage: 'Scheduled jobs grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Scheduled jobs grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
|
||||
const gridOptions = {
|
||||
gridjs: window.gridjs,
|
||||
container: '#scheduled-jobs-grid',
|
||||
dataUrl: 'admin/scheduled-jobs/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.job || 'Job',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html('-');
|
||||
}
|
||||
const fallbackLabel = String(cell.job_key || '')
|
||||
.toLowerCase()
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/^./, (char) => char.toUpperCase());
|
||||
return gridjs.html(cell.label || fallbackLabel || '-');
|
||||
const { gridConfig } = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#scheduled-jobs-grid',
|
||||
dataUrl: 'admin/scheduled-jobs/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.job || 'Job',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html('-');
|
||||
}
|
||||
const fallbackLabel = String(cell.job_key || '')
|
||||
.toLowerCase()
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/^./, (char) => char.toUpperCase());
|
||||
return gridjs.html(cell.label || fallbackLabel || '-');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.enabled || 'Enabled',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
const enabled = Number(cell) === 1;
|
||||
const variant = enabled ? 'success' : 'neutral';
|
||||
const label = enabled ? (labels.enabled || 'Enabled') : (labels.disabled || 'Disabled');
|
||||
return gridjs.html(`<span class="badge" data-variant="${variant}">${label}</span>`);
|
||||
},
|
||||
},
|
||||
{ name: labels.nextRun || 'Next run', sort: true },
|
||||
{ name: labels.schedule || 'Schedule', sort: false },
|
||||
{ name: labels.lastRun || 'Last run', sort: true },
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html('-');
|
||||
}
|
||||
return gridjs.html(`<span class="badge" data-variant="${cell.variant || 'neutral'}">${cell.label || '-'}</span>`);
|
||||
},
|
||||
},
|
||||
{ name: labels.lastError || 'Last error', sort: false },
|
||||
{ name: 'ID', hidden: true },
|
||||
],
|
||||
sortColumns: ['job_key', 'enabled', 'next_run_at', null, 'last_run_finished_at', 'last_run_status', null, null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
{ id: row.id, label: row.label || row.job_key, job_key: row.job_key },
|
||||
row.enabled,
|
||||
row.next_run_at || '-',
|
||||
row.schedule_summary || '-',
|
||||
row.last_run_finished_at || '-',
|
||||
{ variant: row.last_run_status_badge, label: row.last_run_status_label || row.last_run_status || '' },
|
||||
row.last_error || '-',
|
||||
row.id,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const id = rowData?.cells?.[7]?.data;
|
||||
return id ? new URL(`admin/scheduled-jobs/edit/${id}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.enabled || 'Enabled',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
const enabled = Number(cell) === 1;
|
||||
const variant = enabled ? 'success' : 'neutral';
|
||||
const label = enabled ? (labels.enabled || 'Enabled') : (labels.disabled || 'Disabled');
|
||||
return gridjs.html(`<span class="badge" data-variant="${variant}">${label}</span>`);
|
||||
},
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: filterChipMeta,
|
||||
watchInputs: ['#scheduled-jobs-search'],
|
||||
},
|
||||
{ name: labels.nextRun || 'Next run', sort: true },
|
||||
{ name: labels.schedule || 'Schedule', sort: false },
|
||||
{ name: labels.lastRun || 'Last run', sort: true },
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html('-');
|
||||
}
|
||||
return gridjs.html(`<span class="badge" data-variant="${cell.variant || 'neutral'}">${cell.label || '-'}</span>`);
|
||||
},
|
||||
},
|
||||
{ name: labels.lastError || 'Last error', sort: false },
|
||||
{ name: 'ID', hidden: true },
|
||||
],
|
||||
sortColumns: ['job_key', 'enabled', 'next_run_at', null, 'last_run_finished_at', 'last_run_status', null, null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
{ id: row.id, label: row.label || row.job_key, job_key: row.job_key },
|
||||
row.enabled,
|
||||
row.next_run_at || '-',
|
||||
row.schedule_summary || '-',
|
||||
row.last_run_finished_at || '-',
|
||||
{ variant: row.last_run_status_badge, label: row.last_run_status_label || row.last_run_status || '' },
|
||||
row.last_error || '-',
|
||||
row.id,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const id = rowData?.cells?.[7]?.data;
|
||||
return id ? new URL(`admin/scheduled-jobs/edit/${id}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
}) || {};
|
||||
|
||||
const { gridConfig } = initStandardListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: filterChipMeta,
|
||||
watchInputs: ['#scheduled-jobs-search'],
|
||||
},
|
||||
});
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Scheduled jobs grid init failed', { module: 'admin-scheduled-jobs-index', component: 'grid' });
|
||||
}
|
||||
}
|
||||
return gridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { initStandardListPage } from '../core/app-grid-factory.js';
|
||||
import { readPageConfig } from '../core/app-page-config.js';
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
import { badgeHtml, escapeHtml, getAppBase } from './app-list-utils.js';
|
||||
import { createListPageModule } from '../core/app-list-page-module.js';
|
||||
import { badgeHtml, escapeHtml } from './app-list-utils.js';
|
||||
|
||||
const config = readPageConfig('admin-tenants-index');
|
||||
if (config) {
|
||||
const appBase = getAppBase();
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
createListPageModule({
|
||||
configId: 'admin-tenants-index',
|
||||
moduleId: 'admin-tenants-index',
|
||||
missingGridMessage: 'Tenants grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Tenants grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const filterChipMeta = config.filterChipMeta ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
|
||||
const initTenantsGrid = () => {
|
||||
const tenantUuidIndex = 8;
|
||||
const tenantDescriptionIndex = 1;
|
||||
const formatBadge = (value) => badgeHtml(gridjs, value);
|
||||
@@ -55,117 +55,112 @@ if (config) {
|
||||
const number = Number(value ?? 0);
|
||||
return Number.isFinite(number) ? number : 0;
|
||||
};
|
||||
const gridOptions = {
|
||||
gridjs: window.gridjs,
|
||||
container: '#tenants-grid',
|
||||
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>`);
|
||||
|
||||
const { gridConfig } = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#tenants-grid',
|
||||
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(escapeHtml(String(cell ?? '')));
|
||||
}
|
||||
const label = escapeHtml(cell.label ?? '');
|
||||
return gridjs.html(label);
|
||||
{
|
||||
name: labels.description || 'Description',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html(escapeHtml(String(cell ?? '')));
|
||||
}
|
||||
const label = escapeHtml(cell.label ?? '');
|
||||
return gridjs.html(label);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: false,
|
||||
formatter: (cell) => formatStatus(cell),
|
||||
},
|
||||
{
|
||||
name: labels.theme || 'Theme',
|
||||
sort: true,
|
||||
formatter: (cell) => formatTheme(cell),
|
||||
},
|
||||
{
|
||||
name: labels.sso || 'SSO',
|
||||
sort: true,
|
||||
formatter: (cell) => formatSso(cell),
|
||||
},
|
||||
{
|
||||
name: labels.activeUsers || 'Active users',
|
||||
sort: true,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.inactiveUsers || 'Inactive users',
|
||||
sort: true,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.created || 'Created',
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell),
|
||||
},
|
||||
{
|
||||
name: 'UUID',
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
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,
|
||||
},
|
||||
{ variant: row.status_badge || 'neutral', label: row.status_label || row.status || '-' },
|
||||
row.theme ?? null,
|
||||
row.sso ?? null,
|
||||
row.active_users ?? 0,
|
||||
row.inactive_users ?? 0,
|
||||
row.created,
|
||||
row.uuid,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 1 },
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[tenantUuidIndex]?.data,
|
||||
}),
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const uuid = rowData?.cells?.[tenantUuidIndex]?.data;
|
||||
return uuid ? new URL(`admin/tenants/edit/${uuid}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: false,
|
||||
formatter: (cell) => formatStatus(cell),
|
||||
},
|
||||
{
|
||||
name: labels.theme || 'Theme',
|
||||
sort: true,
|
||||
formatter: (cell) => formatTheme(cell),
|
||||
},
|
||||
{
|
||||
name: labels.sso || 'SSO',
|
||||
sort: true,
|
||||
formatter: (cell) => formatSso(cell),
|
||||
},
|
||||
{
|
||||
name: labels.activeUsers || 'Active users',
|
||||
sort: true,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.inactiveUsers || 'Inactive users',
|
||||
sort: true,
|
||||
formatter: (cell) => formatCount(cell),
|
||||
},
|
||||
{
|
||||
name: labels.created || 'Created',
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell),
|
||||
},
|
||||
{
|
||||
name: 'UUID',
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
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,
|
||||
},
|
||||
{ variant: row.status_badge || 'neutral', label: row.status_label || row.status || '-' },
|
||||
row.theme ?? null,
|
||||
row.sso ?? null,
|
||||
row.active_users ?? 0,
|
||||
row.inactive_users ?? 0,
|
||||
row.created,
|
||||
row.uuid,
|
||||
]),
|
||||
rowInteraction: { linkColumn: 1 },
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[tenantUuidIndex]?.data,
|
||||
}),
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const uuid = rowData?.cells?.[tenantUuidIndex]?.data;
|
||||
return uuid ? new URL(`admin/tenants/edit/${uuid}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
const { gridConfig } = initStandardListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: filterChipMeta,
|
||||
watchInputs: ['#tenant-search'],
|
||||
},
|
||||
});
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Tenants grid init failed', { module: 'admin-tenants-index', component: 'grid' });
|
||||
return null;
|
||||
}
|
||||
return gridConfig;
|
||||
};
|
||||
}) || {};
|
||||
|
||||
initTenantsGrid();
|
||||
}
|
||||
return gridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { initStandardListPage } from '../core/app-grid-factory.js';
|
||||
import { readPageConfig } from '../core/app-page-config.js';
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
import { createListPageModule } from '../core/app-list-page-module.js';
|
||||
import { initUsersListPage } from './app-users-list.js';
|
||||
import { bindBulkVisibility } from '../components/app-bulk-selection.js';
|
||||
import { buildUrl, badgeHtml, getAppBase, escapeHtml, buildBadgeList } from './app-list-utils.js';
|
||||
import { buildUrl, badgeHtml, escapeHtml, buildBadgeList } from './app-list-utils.js';
|
||||
import { showAsyncFlash, withLoading } from '../components/app-async-flash.js';
|
||||
|
||||
const config = readPageConfig('admin-users-index');
|
||||
if (config) {
|
||||
const appBase = getAppBase();
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const usersFilterChipMeta = config.filterChipMeta ?? [];
|
||||
const currentUserUuid = String(config.currentUserUuid || '');
|
||||
const canUpdateUsers = config.canUpdateUsers === true;
|
||||
const canUpdateSelf = config.canUpdateSelf === true;
|
||||
const csrf = config.csrf ?? null;
|
||||
const labels = config.labels ?? {};
|
||||
createListPageModule({
|
||||
configId: 'admin-users-index',
|
||||
moduleId: 'admin-users-index',
|
||||
missingGridMessage: 'Users grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Users grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const usersFilterChipMeta = config.filterChipMeta ?? [];
|
||||
const currentUserUuid = String(config.currentUserUuid || '');
|
||||
const canUpdateUsers = config.canUpdateUsers === true;
|
||||
const canUpdateSelf = config.canUpdateSelf === true;
|
||||
const csrf = config.csrf ?? null;
|
||||
const labels = config.labels ?? {};
|
||||
|
||||
const buildBulkUrl = (action) => buildUrl(appBase, `admin/users/bulk/${action}`);
|
||||
const buildBulkUrl = (action) => buildUrl(appBase, `admin/users/bulk/${action}`);
|
||||
|
||||
const initUsersGrid = () => {
|
||||
const rowSelection = window.gridjs?.plugins?.selection?.RowSelection;
|
||||
const rowSelection = gridjs?.plugins?.selection?.RowSelection;
|
||||
if (!rowSelection) {
|
||||
throw new Error('Grid.js RowSelection plugin is not available.');
|
||||
}
|
||||
@@ -29,7 +29,7 @@ if (config) {
|
||||
const uuidIndex = 2;
|
||||
const canEditRow = (row) => {
|
||||
const uuid = row?.cells?.[uuidIndex]?.data;
|
||||
if (!uuid) {return false;}
|
||||
if (!uuid) { return false; }
|
||||
if (uuid === currentUserUuid) {
|
||||
return canUpdateUsers || canUpdateSelf;
|
||||
}
|
||||
@@ -91,68 +91,66 @@ if (config) {
|
||||
{ name: 'ID', hidden: true },
|
||||
];
|
||||
|
||||
const gridOptions = {
|
||||
gridjs: window.gridjs,
|
||||
container: '#users-grid',
|
||||
dataUrl: 'admin/users/data',
|
||||
appBase,
|
||||
columns,
|
||||
sortColumns: [null, null, 'first_name', 'last_name', 'email', 'active', null, null, null, null, null, null, 'created', 'modified', 'last_login_at', null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.has_avatar ? row.uuid : '',
|
||||
row.uuid,
|
||||
row.first_name,
|
||||
row.last_name,
|
||||
row.email,
|
||||
row.active,
|
||||
row.tenants,
|
||||
row.departments,
|
||||
row.roles,
|
||||
row.phone,
|
||||
row.mobile,
|
||||
row.short_dial,
|
||||
row.created,
|
||||
row.modified,
|
||||
row.last_login,
|
||||
row.id,
|
||||
]),
|
||||
selection: {
|
||||
enabled: true,
|
||||
id: 'selectRow',
|
||||
component: rowSelection,
|
||||
selectAllLabel: labels.selectAll || 'Select all',
|
||||
props: {
|
||||
id: (row) => row.cell(uuidIndex).data,
|
||||
const { gridConfig } = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#users-grid',
|
||||
dataUrl: 'admin/users/data',
|
||||
appBase,
|
||||
columns,
|
||||
sortColumns: [null, null, 'first_name', 'last_name', 'email', 'active', null, null, null, null, null, null, 'created', 'modified', 'last_login_at', null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.has_avatar ? row.uuid : '',
|
||||
row.uuid,
|
||||
row.first_name,
|
||||
row.last_name,
|
||||
row.email,
|
||||
row.active,
|
||||
row.tenants,
|
||||
row.departments,
|
||||
row.roles,
|
||||
row.phone,
|
||||
row.mobile,
|
||||
row.short_dial,
|
||||
row.created,
|
||||
row.modified,
|
||||
row.last_login,
|
||||
row.id,
|
||||
]),
|
||||
selection: {
|
||||
enabled: true,
|
||||
id: 'selectRow',
|
||||
component: rowSelection,
|
||||
selectAllLabel: labels.selectAll || 'Select all',
|
||||
props: {
|
||||
id: (row) => row.cell(uuidIndex).data,
|
||||
},
|
||||
getSelectedIds: ({ container }) => {
|
||||
if (!container) { return []; }
|
||||
return Array.from(container.querySelectorAll('.gridjs-tr-selected'))
|
||||
.map((row) => row.getAttribute('data-uuid'))
|
||||
.filter(Boolean);
|
||||
},
|
||||
},
|
||||
getSelectedIds: ({ container }) => {
|
||||
if (!container) {return [];}
|
||||
return Array.from(container.querySelectorAll('.gridjs-tr-selected'))
|
||||
.map((row) => row.getAttribute('data-uuid'))
|
||||
.filter(Boolean);
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[uuidIndex]?.data,
|
||||
}),
|
||||
rowInteraction: { linkColumn: 2 },
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
if (!canEditRow(rowData)) {
|
||||
return '';
|
||||
}
|
||||
const uuid = rowData?.cells?.[uuidIndex]?.data;
|
||||
return uuid ? new URL(`admin/users/edit/${uuid}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
},
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[uuidIndex]?.data,
|
||||
}),
|
||||
rowInteraction: { linkColumn: 2 },
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
if (!canEditRow(rowData)) {
|
||||
return '';
|
||||
}
|
||||
const uuid = rowData?.cells?.[uuidIndex]?.data;
|
||||
return uuid ? new URL(`admin/users/edit/${uuid}`, appBase).toString() : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { gridConfig } = initStandardListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: usersFilterChipMeta,
|
||||
@@ -165,37 +163,32 @@ if (config) {
|
||||
preserveFilterParams: ['tenant'],
|
||||
},
|
||||
},
|
||||
}) || {};
|
||||
|
||||
const usersGridConfig = gridConfig ? { ...gridConfig, indices: { activeIndex, uuidIndex } } : null;
|
||||
initUsersListPage({
|
||||
gridConfig: usersGridConfig,
|
||||
appBase,
|
||||
csrf,
|
||||
labels,
|
||||
buildBulkUrl,
|
||||
bulkDownloadForms: {
|
||||
'access-pdf': '#users-access-pdf-bulk-form',
|
||||
},
|
||||
resetOptions: {
|
||||
resetPage: true,
|
||||
urlHistoryMode: 'push',
|
||||
preserveFilterParams: ['tenant'],
|
||||
},
|
||||
showFlash: showAsyncFlash,
|
||||
withLoading,
|
||||
});
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Users grid init failed', { module: 'admin-users-index', component: 'grid' });
|
||||
return null;
|
||||
}
|
||||
bindBulkVisibility({
|
||||
containerSelector: '#users-grid',
|
||||
buttonSelector: '[data-users-bulk]',
|
||||
});
|
||||
|
||||
return { ...gridConfig, indices: { activeIndex, uuidIndex } };
|
||||
};
|
||||
|
||||
const gridConfig = initUsersGrid();
|
||||
initUsersListPage({
|
||||
gridConfig,
|
||||
appBase,
|
||||
csrf,
|
||||
labels,
|
||||
buildBulkUrl,
|
||||
bulkDownloadForms: {
|
||||
'access-pdf': '#users-access-pdf-bulk-form',
|
||||
},
|
||||
resetOptions: {
|
||||
resetPage: true,
|
||||
urlHistoryMode: 'push',
|
||||
preserveFilterParams: ['tenant'],
|
||||
},
|
||||
showFlash: showAsyncFlash,
|
||||
withLoading,
|
||||
});
|
||||
|
||||
bindBulkVisibility({
|
||||
containerSelector: '#users-grid',
|
||||
buttonSelector: '[data-users-bulk]',
|
||||
});
|
||||
}
|
||||
return usersGridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
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, escapeHtml } from './app-list-utils.js';
|
||||
import { createListPageModule } from '../core/app-list-page-module.js';
|
||||
import { escapeHtml } from './app-list-utils.js';
|
||||
|
||||
const config = readPageConfig('search-index');
|
||||
if (config) {
|
||||
const appBase = getAppBase();
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
createListPageModule({
|
||||
configId: 'search-index',
|
||||
moduleId: 'search-index',
|
||||
missingGridMessage: 'Search results grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Search results grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const gridSearch = config.gridSearch ?? null;
|
||||
const filterSchema = config.filterSchema ?? [];
|
||||
const labels = config.labels ?? {};
|
||||
|
||||
const renderResult = (cell) => {
|
||||
const title = escapeHtml(cell?.title ?? '');
|
||||
const description = escapeHtml(cell?.description ?? '');
|
||||
const url = cell?.url ?? '#';
|
||||
const image = cell?.image ?? '';
|
||||
const icon = escapeHtml(cell?.icon ?? 'bi-search');
|
||||
const avatar = image
|
||||
? `<img class="search-result-avatar" src="${image}" alt="" loading="lazy">`
|
||||
: `<span class="search-result-avatar-placeholder"><i class="bi ${icon}"></i></span>`;
|
||||
const renderResult = (cell) => {
|
||||
const title = escapeHtml(cell?.title ?? '');
|
||||
const description = escapeHtml(cell?.description ?? '');
|
||||
const url = cell?.url ?? '#';
|
||||
const image = cell?.image ?? '';
|
||||
const icon = escapeHtml(cell?.icon ?? 'bi-search');
|
||||
const avatar = image
|
||||
? `<img class="search-result-avatar" src="${image}" alt="" loading="lazy">`
|
||||
: `<span class="search-result-avatar-placeholder"><i class="bi ${icon}"></i></span>`;
|
||||
|
||||
return gridjs.html(`
|
||||
return gridjs.html(`
|
||||
<div class="search-result-row">
|
||||
${avatar}
|
||||
<div class="search-result-content">
|
||||
@@ -29,42 +30,41 @@ if (config) {
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
};
|
||||
};
|
||||
|
||||
const renderType = (cell) => gridjs.html(`<span class="search-result-type">${escapeHtml(cell ?? '')}</span>`);
|
||||
const renderType = (cell) => gridjs.html(`<span class="search-result-type">${escapeHtml(cell ?? '')}</span>`);
|
||||
|
||||
const { gridConfig } = initStandardListPage({
|
||||
grid: {
|
||||
gridjs: window.gridjs,
|
||||
container: '#search-results-grid',
|
||||
dataUrl: 'search/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.result || 'Result',
|
||||
sort: false,
|
||||
formatter: (cell) => renderResult(cell),
|
||||
},
|
||||
{
|
||||
name: labels.type || 'Type',
|
||||
sort: false,
|
||||
formatter: (cell) => renderType(cell),
|
||||
},
|
||||
],
|
||||
sortColumns: [null, null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [row, row.type]),
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
},
|
||||
filters: {
|
||||
mode: 'search-only',
|
||||
},
|
||||
});
|
||||
const { gridConfig } = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#search-results-grid',
|
||||
dataUrl: 'search/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.result || 'Result',
|
||||
sort: false,
|
||||
formatter: (cell) => renderResult(cell),
|
||||
},
|
||||
{
|
||||
name: labels.type || 'Type',
|
||||
sort: false,
|
||||
formatter: (cell) => renderType(cell),
|
||||
},
|
||||
],
|
||||
sortColumns: [null, null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang ?? {},
|
||||
mapData: (data) => data.data.map((row) => [row, row.type]),
|
||||
search: gridSearch,
|
||||
filterSchema,
|
||||
urlSync: true,
|
||||
},
|
||||
filters: {
|
||||
mode: 'search-only',
|
||||
},
|
||||
}) || {};
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Search results grid init failed', { module: 'search-index', component: 'grid' });
|
||||
}
|
||||
}
|
||||
return gridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user