v0.1.4-pre.009
This commit is contained in:
279
crates/ksp-app-config-desk/frontend/ts/documents.ts
Normal file
279
crates/ksp-app-config-desk/frontend/ts/documents.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/documents.ts
|
||||
// version: 1
|
||||
|
||||
//! Documents panel backed exclusively by ksp-config-lib inventory, validation and management APIs.
|
||||
|
||||
import DataTable from "datatables.net-bs5";
|
||||
import "datatables.net-select-bs5";
|
||||
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
|
||||
import "datatables.net-select-bs5/css/select.bootstrap5.css";
|
||||
import type { ConfigDocumentDetailDto } from "./bindings/ksp_app_config_desk/documents/ConfigDocumentDetailDto";
|
||||
import type { ConfigDocumentErrorDto } from "./bindings/ksp_app_config_desk/documents/ConfigDocumentErrorDto";
|
||||
import type { ConfigDocumentSaveResultDto } from "./bindings/ksp_app_config_desk/documents/ConfigDocumentSaveResultDto";
|
||||
import type { ConfigDocumentSummaryDto } from "./bindings/ksp_app_config_desk/documents/ConfigDocumentSummaryDto";
|
||||
import { frontendDebug, frontendTrace, frontendWarn } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
interface DocumentsDataTable {
|
||||
destroy(): unknown;
|
||||
on(event: string, callback: (event: unknown, api: unknown, type: string, indexes: number[]) => void): unknown;
|
||||
row(selector: number): { node(): Node | null };
|
||||
}
|
||||
|
||||
let documentsTable: DocumentsDataTable | null = null;
|
||||
let activeDocument: ConfigDocumentDetailDto | null = null;
|
||||
|
||||
const stageLabels: Record<string, string> = {
|
||||
valid: "Valide",
|
||||
read: "Lecture",
|
||||
json: "JSON",
|
||||
schema: "Schema",
|
||||
semantic: "Sémantique",
|
||||
effective: "Effective",
|
||||
other: "Autre",
|
||||
};
|
||||
|
||||
function stageLabel(stage: string): string {
|
||||
return stageLabels[stage] ?? stage;
|
||||
}
|
||||
|
||||
function setText(selector: string, value: string): void {
|
||||
const element = document.querySelector<HTMLElement>(selector);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setDocumentsStatus(message: string, tone: "primary" | "success" | "warning" | "danger" = "primary"): void {
|
||||
const status = document.querySelector<HTMLElement>("#documentsStatus");
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
status.className = `alert alert-${tone} mb-0`;
|
||||
status.textContent = message;
|
||||
frontendTrace("main", "Documents panel status replaced", { tone });
|
||||
}
|
||||
|
||||
function diagnosticMessage(summary: ConfigDocumentSummaryDto): string {
|
||||
if (!summary.diagnostic) {
|
||||
return "Aucun diagnostic backend.";
|
||||
}
|
||||
return `${summary.diagnostic.domain}.${summary.diagnostic.code} — ${summary.diagnostic.message}`;
|
||||
}
|
||||
|
||||
function renderDetail(detail: ConfigDocumentDetailDto): void {
|
||||
activeDocument = detail;
|
||||
setText("#documentDetailFileId", detail.summary.fileId);
|
||||
setText("#documentDetailFilename", detail.summary.filename);
|
||||
setText("#documentDetailSchema", detail.summary.schemaFileId ?? "—");
|
||||
setText("#documentDetailPath", detail.summary.path);
|
||||
setText("#documentDetailStage", stageLabel(detail.summary.diagnosticStage));
|
||||
setText("#documentDetailDiagnostic", diagnosticMessage(detail.summary));
|
||||
const badge = document.querySelector<HTMLElement>("#documentDetailStatus");
|
||||
if (badge) {
|
||||
const valid = detail.summary.validationStatus === "valid";
|
||||
badge.className = `badge ${valid ? "text-bg-success" : "text-bg-danger"}`;
|
||||
badge.textContent = valid ? "Valide" : "Invalide";
|
||||
}
|
||||
const editor = document.querySelector<HTMLTextAreaElement>("#documentSourceEditor");
|
||||
if (editor) {
|
||||
editor.value = detail.source ?? "";
|
||||
editor.disabled = detail.source === null;
|
||||
}
|
||||
const saveButton = document.querySelector<HTMLButtonElement>("#saveDocumentSource");
|
||||
if (saveButton) {
|
||||
saveButton.disabled = detail.source === null;
|
||||
}
|
||||
const detailPanel = document.querySelector<HTMLElement>("#documentDetailPanel");
|
||||
if (detailPanel) {
|
||||
detailPanel.hidden = false;
|
||||
}
|
||||
frontendTrace("main", "Config document detail rendered", {
|
||||
fileId: detail.summary.fileId,
|
||||
validationStatus: detail.summary.validationStatus,
|
||||
diagnosticStage: detail.summary.diagnosticStage,
|
||||
sourceAvailable: detail.source !== null,
|
||||
});
|
||||
}
|
||||
|
||||
function clearDetail(): void {
|
||||
activeDocument = null;
|
||||
const detailPanel = document.querySelector<HTMLElement>("#documentDetailPanel");
|
||||
if (detailPanel) {
|
||||
detailPanel.hidden = true;
|
||||
}
|
||||
frontendTrace("main", "Config document detail cleared");
|
||||
}
|
||||
|
||||
async function loadDetail(fileId: string, source: "selection" | "reload"): Promise<void> {
|
||||
frontendDebug("main", "Config document detail requested", { fileId, source });
|
||||
try {
|
||||
const detail = await invokeKsp<ConfigDocumentDetailDto>("main", "get_config_document_detail", { fileId });
|
||||
renderDetail(detail);
|
||||
setDocumentsStatus(`Document ${fileId} chargé depuis Config.`, "success");
|
||||
} catch (caughtError) {
|
||||
const error = asDocumentError(caughtError);
|
||||
const message = error
|
||||
? `${stageLabel(error.diagnosticStage)} : ${error.error.domain}.${error.error.code} — ${error.error.message}`
|
||||
: "Le détail du document n'a pas pu être chargé.";
|
||||
setDocumentsStatus(message, "danger");
|
||||
}
|
||||
}
|
||||
|
||||
function populateTableRows(documents: ConfigDocumentSummaryDto[]): void {
|
||||
const body = document.querySelector<HTMLTableSectionElement>("#configDocumentsTable tbody");
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
body.replaceChildren();
|
||||
for (const summary of documents) {
|
||||
const row = document.createElement("tr");
|
||||
row.dataset.fileId = summary.fileId;
|
||||
appendCell(row, summary.fileId);
|
||||
appendCell(row, summary.filename);
|
||||
appendCell(row, summary.schemaFileId ?? "—");
|
||||
appendCell(row, summary.validationStatus === "valid" ? "Valide" : "Invalide");
|
||||
appendCell(row, stageLabel(summary.diagnosticStage));
|
||||
body.append(row);
|
||||
}
|
||||
frontendTrace("main", "Config document inventory table DOM replaced", { documentCount: documents.length });
|
||||
}
|
||||
|
||||
function appendCell(row: HTMLTableRowElement, text: string): void {
|
||||
const cell = document.createElement("td");
|
||||
cell.textContent = text;
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
function initializeDataTable(): void {
|
||||
if (documentsTable) {
|
||||
documentsTable.destroy();
|
||||
documentsTable = null;
|
||||
}
|
||||
const table = new DataTable("#configDocumentsTable", {
|
||||
order: [[0, "asc"]],
|
||||
pageLength: 10,
|
||||
select: { style: "single" },
|
||||
language: {
|
||||
emptyTable: "Aucun document Config enregistré.",
|
||||
info: "_START_ à _END_ sur _TOTAL_ document(s)",
|
||||
infoEmpty: "0 document",
|
||||
lengthMenu: "Afficher _MENU_",
|
||||
search: "Filtrer :",
|
||||
zeroRecords: "Aucun document correspondant.",
|
||||
},
|
||||
}) as unknown as DocumentsDataTable;
|
||||
table.on("select", (event, api, type, indexes) => {
|
||||
void event;
|
||||
void api;
|
||||
if (type !== "row" || indexes.length === 0) {
|
||||
return;
|
||||
}
|
||||
const node = table.row(indexes[0]).node();
|
||||
if (!(node instanceof HTMLTableRowElement)) {
|
||||
return;
|
||||
}
|
||||
const fileId = node.dataset.fileId;
|
||||
frontendTrace("main", "Config document table row selected", { fileId: fileId ?? null });
|
||||
if (fileId) {
|
||||
void loadDetail(fileId, "selection");
|
||||
}
|
||||
});
|
||||
documentsTable = table;
|
||||
frontendTrace("main", "DataTables Documents initialized", { selection: "single" });
|
||||
}
|
||||
|
||||
async function refreshInventory(): Promise<void> {
|
||||
frontendDebug("main", "Config document inventory refresh requested");
|
||||
clearDetail();
|
||||
try {
|
||||
const documents = await invokeKsp<ConfigDocumentSummaryDto[]>("main", "get_config_documents");
|
||||
populateTableRows(documents);
|
||||
initializeDataTable();
|
||||
setDocumentsStatus(`${documents.length} document(s) Config évalué(s) par le backend.`, "success");
|
||||
} catch {
|
||||
setDocumentsStatus("L'inventaire Config n'a pas pu être chargé.", "danger");
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadActiveDocument(): Promise<void> {
|
||||
if (!activeDocument) {
|
||||
return;
|
||||
}
|
||||
frontendDebug("main", "Config document reload clicked", { fileId: activeDocument.summary.fileId });
|
||||
await loadDetail(activeDocument.summary.fileId, "reload");
|
||||
}
|
||||
|
||||
async function saveActiveDocument(): Promise<void> {
|
||||
if (!activeDocument) {
|
||||
return;
|
||||
}
|
||||
const editor = document.querySelector<HTMLTextAreaElement>("#documentSourceEditor");
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const fileId = activeDocument.summary.fileId;
|
||||
frontendDebug("main", "Config document validated save clicked", { fileId });
|
||||
setDocumentsStatus(`Validation et sauvegarde de ${fileId} en cours...`, "primary");
|
||||
try {
|
||||
const result = await invokeKsp<ConfigDocumentSaveResultDto>("main", "save_config_document_source", {
|
||||
fileId,
|
||||
source: editor.value,
|
||||
});
|
||||
renderDetail(result.document);
|
||||
const outcome = result.sourceChanged ? "source modifiée" : "source inchangée";
|
||||
setDocumentsStatus(`Sauvegarde validée par Config : ${outcome}; reload_required=${result.reloadRequired}.`, "success");
|
||||
frontendDebug("main", "Config document validated save completed", {
|
||||
fileId,
|
||||
sourceChanged: result.sourceChanged,
|
||||
reloadRequired: result.reloadRequired,
|
||||
});
|
||||
} catch (caughtError) {
|
||||
const error = asDocumentError(caughtError);
|
||||
if (error) {
|
||||
setDocumentsStatus(
|
||||
`${stageLabel(error.diagnosticStage)} : ${error.error.domain}.${error.error.code} — ${error.error.message}`,
|
||||
"danger",
|
||||
);
|
||||
frontendWarn("main", "Config document candidate rejected by backend", { fileId, diagnosticStage: error.diagnosticStage });
|
||||
return;
|
||||
}
|
||||
setDocumentsStatus("Le candidat Config a été rejeté sans diagnostic exploitable.", "danger");
|
||||
}
|
||||
}
|
||||
|
||||
function asDocumentError(value: unknown): ConfigDocumentErrorDto | null {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return null;
|
||||
}
|
||||
const candidate = value as Record<string, unknown>;
|
||||
if (typeof candidate.diagnosticStage !== "string" || typeof candidate.error !== "object" || candidate.error === null) {
|
||||
return null;
|
||||
}
|
||||
const error = candidate.error as Record<string, unknown>;
|
||||
if (typeof error.domain !== "string" || typeof error.code !== "string" || typeof error.message !== "string") {
|
||||
return null;
|
||||
}
|
||||
return value as ConfigDocumentErrorDto;
|
||||
}
|
||||
|
||||
function bindDocumentActions(): void {
|
||||
document.querySelector<HTMLButtonElement>("#refreshDocuments")?.addEventListener("click", () => {
|
||||
frontendTrace("main", "Documents refresh button clicked");
|
||||
void refreshInventory();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#reloadDocumentSource")?.addEventListener("click", () => {
|
||||
frontendTrace("main", "Document reload button clicked");
|
||||
void reloadActiveDocument();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#saveDocumentSource")?.addEventListener("click", () => {
|
||||
frontendTrace("main", "Document save button clicked");
|
||||
void saveActiveDocument();
|
||||
});
|
||||
frontendTrace("main", "Documents panel handlers installed");
|
||||
}
|
||||
|
||||
export function initializeDocumentsPanel(): void {
|
||||
bindDocumentActions();
|
||||
void refreshInventory();
|
||||
}
|
||||
Reference in New Issue
Block a user