Files
breadcrumb-the-shire/web/js/pages/app-users-list.js
fs 5ba086adee refactor(admin/users): migrate CSV export to core primitive
Replace the inline export implementation with the new generic primitive
(core/Service/Export + helpers/export + app-list-export.js). No more
hand-rolled fputcsv loop, header setup, or escape closure duplicated
here — one implementation, tested once, reused everywhere.

- export().php: uses exportRequireGetRequest, exportCapLimit,
  exportResolveFlavor, ExportColumn[], exportSendCsv. Phone/mobile/
  short_dial keep allowSignedNumeric=true so +49 numbers render
  untouched.
- export(none).phtml: deleted — headers + fputcsv now live in the
  core helper.
- index(default).phtml: inline <details class="dropdown"> "Export CSV"
  replaced by the reusable app-list-export-dropdown partial, exposing
  CSV and Excel flavors. exportUrl added to pageConfig.
- app-users-list.js: hand-rolled export-button binding removed in
  favor of initListExport({ gridConfig, exportUrl }).
- admin-users-index.js: forwards config.exportUrl into
  initUsersListPage.

Net effect: ~100 lines of duplicated export plumbing removed from the
users page; users gain the Excel flavor for free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:57:31 +02:00

146 lines
4.7 KiB
JavaScript

import { confirmDialog } from '../core/app-confirm-dialog.js';
import { warnOnce } from '../core/app-dom.js';
import { postForm } from '../core/app-http.js';
import { initListExport } from '../core/app-list-export.js';
export function initUsersListPage(options = {}) {
const {
gridConfig,
appBase,
csrf,
labels = {},
selectors = {},
bulkDownloadForms = {},
exportUrl,
listPath = 'admin/users',
resetOptions = {},
buildBulkUrl,
showFlash,
withLoading
} = options;
if (!gridConfig) {return;}
const resetSelector = selectors.resetButton || '[data-users-reset]';
const bulkSelector = selectors.bulkButtons || '[data-users-bulk]';
const resolveBulkUrl = buildBulkUrl
? buildBulkUrl
: (action) => new URL(`admin/users/bulk/${action}`, appBase).toString();
const postAction = async (url, data = {}) => {
const body = new URLSearchParams({ ...data, [csrf.key]: csrf.token });
return postForm(url, body);
};
if (exportUrl) {
initListExport({ gridConfig, exportUrl });
}
const resetButton = document.querySelector(resetSelector);
if (resetButton) {
resetButton.addEventListener('click', () => {
if (typeof gridConfig.resetFilters === 'function') {
gridConfig.resetFilters(resetOptions);
} else {
gridConfig.updateGrid(resetOptions);
}
document.dispatchEvent(new CustomEvent('app:filters-updated'));
});
}
const bulkButtons = Array.from(document.querySelectorAll(bulkSelector));
if (bulkButtons.length) {
bulkButtons.forEach((button) => {
button.addEventListener('click', async () => {
const action = button.getAttribute('data-users-bulk') || '';
const ids = gridConfig.selection?.getSelectedIds?.() || [];
const validActions = ['activate', 'deactivate', 'delete', 'send-access', 'access-pdf'];
if (!ids.length || !validActions.includes(action)) {
if (action && !validActions.includes(action)) {
warnOnce('UI_ACTION_UNSUPPORTED', `Unsupported users bulk action: ${action}`, {
module: 'users-list',
component: 'bulk-action',
});
}
return;
}
const confirmTexts = {
'activate': labels.bulkActivateConfirm,
'deactivate': labels.bulkDeactivateConfirm,
'delete': labels.bulkDeleteConfirm,
'send-access': labels.bulkSendAccessConfirm,
'access-pdf': labels.bulkAccessPdfConfirm
};
const confirmText = confirmTexts[action];
if (confirmText) {
const approved = await confirmDialog.confirm({
message: confirmText,
actionKind: action,
});
if (!approved) {
return;
}
}
const downloadFormSelector = bulkDownloadForms?.[action];
if (downloadFormSelector) {
const downloadForm = document.querySelector(downloadFormSelector);
if (!downloadForm) {
window.location.href = new URL(listPath, appBase).toString();
return;
}
const uuidsInput = downloadForm.querySelector('input[name="uuids"]');
if (uuidsInput) {
uuidsInput.value = ids.join(',');
}
downloadForm.submit();
return;
}
const executeBulk = async () => {
const url = resolveBulkUrl(action);
try {
const data = await postAction(url, { uuids: ids.join(',') });
if (!data || data.ok !== true) {
throw new Error('Bulk action failed');
}
gridConfig.selection?.clear?.();
gridConfig.grid?.forceRender();
if (showFlash && labels.bulkMessages) {
const msg = labels.bulkMessages[action];
if (msg) {
const count = data.count ?? 0;
const failed = data.failed ?? 0;
let text = msg.success.replace('%d', count);
if (failed > 0 && msg.partial) {
text = msg.partial.replace('%d', count).replace('%f', failed);
}
showFlash('success', text);
}
}
} catch {
if (showFlash && labels.bulkMessages) {
const msg = labels.bulkMessages[action];
if (msg?.error) {
showFlash('error', msg.error);
} else {
window.location.href = new URL(listPath, appBase).toString();
}
} else {
window.location.href = new URL(listPath, appBase).toString();
}
}
};
if (withLoading) {
await withLoading(executeBulk);
} else {
await executeBulk();
}
});
});
}
}