diff --git a/src/pages/internal/changelog.tsx b/src/pages/internal/changelog.tsx index 2a333c8595146a889a7bb826633aac7434b021e0..eb59868d1cafce7670d4e091dcbc19f17933c084 100644 --- a/src/pages/internal/changelog.tsx +++ b/src/pages/internal/changelog.tsx @@ -7,7 +7,262 @@ import { useFilteredDataView, PagingNavigation, FilterInput } from "@/components import { DateTime } from "luxon"; import { showError, ErrorPage } from "@/misc/ErrorHandlers"; -import type { GetChangelogResponse, int } from "@/api/api_v1_types"; +import type { changelog_entry, GetChangelogResponse, int } from "@/api/api_v1_types"; +import { useEffect, useState } from "react"; + +function ValToStr({ val }: { val: any }) { + const [expand, setExpand] = useState(false); + + if (val === null) { + return <span className="fst-italic text-muted">null</span>; + } + if (val === undefined) { + return <span className="fst-italic text-muted">undefined</span>; + } + const stringifiedVal = JSON.stringify(val); + if (stringifiedVal.length > 100) { + if (!expand) { + return ( + <span> + {stringifiedVal.substring(0, 100)} + <button + type="button" + className="btn btn-link p-0 align-baseline text-decoration-underline" + onClick={(e) => { + setExpand(true); + }} + > + ... + </button> + </span> + ); + } + return ( + <span> + {stringifiedVal} <br /> + <button + type="button" + className="btn btn-secondary btn-sm" + onClick={(e) => { + setExpand(false); + }} + > + Hide + </button> + </span> + ); + } + return stringifiedVal; +} + +function Modification({ + i, + filterLevel, + formattedDate, + resolveUser, +}: { + i: changelog_entry; + filterLevel: (lvl: number) => () => void; + formattedDate: string; + resolveUser: (id: number) => string; +}) { + const reloadFunc = useReloadBoundary(); + const api = useBackendContext(); + + const undo = () => { + if (i.type !== "modification") { + alert("TODO: Not implemented"); + return; + } + if (!confirm("Wirklich zurücksetzen?")) { + return; + } + let updates: { [key: string]: any } = {}; + updates[i.field_description!.id] = i.old_value; + + api.updateOMObject(i.object_type!, i.object_id!, { + updates: updates, + expected_current_values: {}, //TODO have something here? + }) + .then(reloadFunc) + .catch((e) => { + showError(e, "Exception while reverting change"); + }); + }; + + return ( + <tr key={i.id}> + <td>{formattedDate}</td> + <td>{resolveUser(i.modifying_user_id)}</td> + <td>{i.type}</td> + <td> + <button + type="button" + className="btn btn-link p-0 align-baseline" + onClick={filterLevel(1)} + > + {`${i.object_type}`} + </button> + . + <button + type="button" + className="btn btn-link p-0 align-baseline" + onClick={filterLevel(2)} + > + {`${i.object_id}`} + </button> + . + <button + type="button" + className="btn btn-link p-0 align-baseline" + onClick={filterLevel(3)} + >{`${i.field_description!.id}`}</button> + {` (${i.field_description?.type})`} + </td> + <td> + <ValToStr val={i.old_value} /> + </td> + <td> + <ValToStr val={i.new_value} /> + </td> + <td> + <button className="btn btn-warning" onClick={undo}> + revert to old + </button> + </td> + </tr> + ); +} + +function DeletionChange({ + i, + filterLevel, + formattedDate, + resolveUser, +}: { + i: changelog_entry; + filterLevel: (lvl: number) => () => void; + formattedDate: string; + resolveUser: (id: number) => string; +}) { + const reloadFunc = useReloadBoundary(); + const api = useBackendContext(); + + const undelete = () => { + if (i.type !== "deletion_change" || i.is_now_deleted === false) { + alert("invalid action!"); + return; + } + if (!confirm("Wirklich wiederherstellen?")) { + return; + } + api.resurrectOMObject(i.object_type!, i.object_id!) + .then(reloadFunc) + .catch((e) => { + showError(e, "Exception while reverting change"); + }); + }; + + return ( + <tr key={i.id}> + <td>{formattedDate}</td> + <td>{resolveUser(i.modifying_user_id)}</td> + <td>{i.type}</td> + <td> + <button + type="button" + className="btn btn-link p-0 align-baseline" + onClick={filterLevel(1)} + > + {`${i.object_type}`} + </button> + . + <button + type="button" + className="btn btn-link p-0 align-baseline" + onClick={filterLevel(2)} + > + {`${i.object_id}`} + </button> + </td> + {i.is_now_deleted ? ( + <> + <td> + <span className="fst-italic text-muted">not deleted</span> + </td> + <td> + <span className="fst-italic text-danger">deleted</span> + </td> + </> + ) : ( + <> + <td> + <span className="fst-italic text-danger">deleted</span> + </td> + <td> + <span className="fst-italic text-muted">not deleted</span> + </td> + </> + )} + <td> + {i.is_now_deleted === true && ( + <button className="btn btn-warning" onClick={undelete}> + undelete + </button> + )} + </td> + </tr> + ); +} + +function Creation({ + i, + filterLevel, + formattedDate, + resolveUser, +}: { + i: changelog_entry; + filterLevel: (lvl: number) => () => void; + formattedDate: string; + resolveUser: (id: number) => string; +}) { + const reloadFunc = useReloadBoundary(); + const api = useBackendContext(); + + return ( + <tr key={i.id}> + <td>{formattedDate}</td> + <td>{resolveUser(i.modifying_user_id)}</td> + <td>{i.type}</td> + <td> + <button + type="button" + className="btn btn-link p-0 align-baseline" + onClick={filterLevel(1)} + > + {`${i.object_type}`} + </button> + . + <button + type="button" + className="btn btn-link p-0 align-baseline" + onClick={filterLevel(2)} + > + {`${i.object_id}`} + </button> + {typeof i.variant === "string" && ` (variant:${i.variant})`} + </td> + + <td> + <ValToStr val={i.old_value} /> + </td> + <td> + <ValToStr val={i.new_value} /> + </td> + <td></td> + </tr> + ); +} function ChangelogList({ changelog, @@ -21,8 +276,25 @@ function ChangelogList({ updateNow: boolean, ) => void; }) { - const reloadFunc = useReloadBoundary(); const api = useBackendContext(); + const [usersData, setUsersData] = useState<{ [key: number]: string }>({}); + + useEffect(() => { + api.getUsers().then((data) => { + let users: { [key: number]: string } = {}; + data.users.forEach((u) => { + users[u.id] = u.name; + }); + setUsersData(users); + }); + }, [api]); + + const resolveUser = (id: number) => { + if (usersData[id]) { + return `${usersData[id]} (${id})`; + } + return id + ""; + }; return ( <> @@ -76,79 +348,38 @@ function ChangelogList({ }; }; let pfad = <></>; + const formattedDate = DateTime.fromISO(i.change_time).toFormat( + "yyyy-MM-dd HH:mm:ss", + ); + switch (i.type) { case "modification": - pfad = ( - <> - <button - type="button" - className="btn btn-link p-0 align-baseline" - onClick={filterLevel(1)} - > - {`${i.object_type}`} - </button> - . - <button - type="button" - className="btn btn-link p-0 align-baseline" - onClick={filterLevel(2)} - > - {`${i.object_id}`} - </button> - . - <button - type="button" - className="btn btn-link p-0 align-baseline" - onClick={filterLevel(3)} - >{`${i.field_description!.id}`}</button> - {` (${i.field_description?.type})`} - </> + return ( + <Modification + i={i} + filterLevel={filterLevel} + formattedDate={formattedDate} + resolveUser={resolveUser} + /> ); - break; case "creation": - pfad = ( - <> - <button - type="button" - className="btn btn-link p-0 align-baseline" - onClick={filterLevel(1)} - > - {`${i.object_type}`} - </button> - . - <button - type="button" - className="btn btn-link p-0 align-baseline" - onClick={filterLevel(2)} - > - {`${i.object_id}`} - </button> - {typeof i.variant === "string" && - ` (variant:${i.variant})`} - </> + return ( + <Creation + i={i} + filterLevel={filterLevel} + formattedDate={formattedDate} + resolveUser={resolveUser} + /> ); - break; case "deletion_change": - pfad = ( - <> - <button - type="button" - className="btn btn-link p-0 align-baseline" - onClick={filterLevel(1)} - > - {`${i.object_type}`} - </button> - . - <button - type="button" - className="btn btn-link p-0 align-baseline" - onClick={filterLevel(2)} - > - {`${i.object_id}`} - </button> - </> + return ( + <DeletionChange + i={i} + filterLevel={filterLevel} + formattedDate={formattedDate} + resolveUser={resolveUser} + /> ); - break; case "unknown": pfad = ( <>{`${i.unknown_type}.${i.unknown_field}, id=${i.object_id}`}</> @@ -157,53 +388,20 @@ function ChangelogList({ default: pfad = <>{i.type}</>; } - const valToStr = (val: any) => { - if (val === null) { - return <span className="fst-italic text-muted">null</span>; - } - if (val === undefined) { - return <span className="fst-italic text-muted">undefined</span>; - } - return JSON.stringify(val); - }; - const formattedDate = DateTime.fromISO(i.change_time).toFormat( - "yyyy-MM-dd HH:mm:ss", - ); - const undo = () => { - if (i.type !== "modification") { - alert("TODO: Not implemented"); - return; - } - if (!confirm("Wirklich zurücksetzen?")) { - return; - } - let updates: { [key: string]: any } = {}; - updates[i.field_description!.id] = i.old_value; - api.updateOMObject(i.object_type!, i.object_id!, { - updates: updates, - expected_current_values: {}, //TODO have something here? - }) - .then(reloadFunc) - .catch((e) => { - showError(e, "Exception while reverting change"); - }); - }; return ( <tr key={i.id}> <td>{formattedDate}</td> - <td>{i.modifying_user_id}</td> + <td>{resolveUser(i.modifying_user_id)}</td> <td>{i.type}</td> <td>{pfad}</td> - <td>{valToStr(i.old_value)}</td> - <td>{valToStr(i.new_value)}</td> <td> - {i.type === "modification" && ( - <button className="btn btn-warning" onClick={undo}> - revert to old - </button> - )} + <ValToStr val={i.old_value} /> + </td> + <td> + <ValToStr val={i.new_value} /> </td> + <td></td> </tr> ); })}