Implement party editing, Creation and Deletion
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dominic Zimmer 2022-10-16 18:18:49 +02:00
parent 74661bdb42
commit fd5d6fa71b
4 changed files with 126 additions and 31 deletions

View File

@ -155,11 +155,16 @@ dialog > div label {
top: 0px;
left:0px;
}
.dialog-table {
.dialog-person-table {
display: grid;
gap: 5px;
grid-template-columns: 1fr 1fr;
}
.dialog-party-table {
display: grid;
gap: 5px;
grid-template-columns: 1fr auto 1fr auto;
}
.add-guest-button {
padding: 5px;
border-radius: 4px;
@ -171,7 +176,16 @@ dialog > div label {
width: 100%;
justify-content: space-between;
}
.add-guest > .tip {
.danger-hint {
align-self: flex-end;
font-size: 8pt;
color: red;
flex-wrap:nowrap;
}
.danger-hint > pre {
display: inline-block;
}
.tip {
align-self: flex-end;
font-size: 8pt;
color: gray;

View File

@ -1,21 +1,22 @@
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import './AdminUI.css';
import { createGuestRequest, deleteGuestRequest, listGuestsRequest, listPartyRequest, modifyGuestRequest, parseToken } from './partyAdminApi';
import { createGuestRequest, createPartyRequest, deleteGuestRequest, deletePartyRequest, listGuestsRequest, listPartyRequest, modifyGuestRequest, parseToken, setAllowedExtras } from './partyAdminApi';
import { GrammaticalGender, Person, RequestCreateGuest, ResponseCreateParty, ResponseListGuests, ResponseListParties } from './partyAdminApiTypes';
export const AdminUI: React.FC = () => {
const [editingParty, setEditingParty] = useState(false);
const [partyList, setPartyList] = useState<ResponseListParties>([]);
const [selectedParty, setSelectedParty] = useState<number | undefined>(undefined);
// eslint-disable-next-line no-restricted-globals
const adminToken = useMemo(() => parseToken(location.href) ?? "", []);
const loadPartyList = useCallback(async () => {
const response = await listPartyRequest(adminToken);
console.log("load party list");
const listPartyResponse = await listPartyRequest(adminToken);
if (selectedParty === undefined)
setSelectedParty(response.length > 0 ? 0 : undefined);
setPartyList(response);
setSelectedParty(listPartyResponse.length > 0 ? 0 : undefined);
setPartyList([...listPartyResponse]);
}, [adminToken, selectedParty]);
const deleteUser = async () => {
@ -33,11 +34,46 @@ export const AdminUI: React.FC = () => {
dialog.show();
};
const [editUserData, setEditUserData] = useState<RequestCreateGuest|Person>();
const dismissBackdrop = () => {
const dialog = document.getElementById("dialog-edit-user") as HTMLDialogElement;
const editParty = () => {
const dialog = document.getElementById("dialog-edit-party") as HTMLDialogElement;
if (!dialog) return;
setEditingParty(true);
dialog.show();
};
const [editUserData, setEditUserData] = useState<RequestCreateGuest | Person>();
const createParty = () => {
const element = document.getElementById("create-party") as HTMLInputElement;
if (!element) return;
const partyName = element.value;
if (!partyName) return;
createPartyRequest(adminToken, { allowed_extra: {}, name: partyName });
loadPartyList();
dismissBackdrop();
};
const updateExtras = (partyName: string) => {
const element = document.getElementById(`payload-${partyName}`) as HTMLInputElement;
if (!element) return;
const newExtras = element.value;
if (newExtras === "deletethis") {
deletePartyRequest(adminToken, partyName);
} else {
const payload = JSON.parse(newExtras);
setAllowedExtras(adminToken, partyName, payload);
}
loadPartyList();
dismissBackdrop();
};
const dismissBackdrop = (e?: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e?.stopPropagation();
["dialog-edit-user", "dialog-edit-party"].forEach(elementId => {
const dialog = document.getElementById(elementId) as HTMLDialogElement;
dialog.close();
})
setEditUserData(undefined);
dialog.close();
setEditingParty(false);
};
const saveEditUser = () => {
@ -56,17 +92,36 @@ export const AdminUI: React.FC = () => {
if (adminToken === "") return;
loadPartyList();
}, [adminToken, loadPartyList]);
return <>
{editUserData ?
{editUserData || editingParty ?
<div id="backdrop" onClick={dismissBackdrop} />
: null}
<dialog id="dialog-edit-party">
<div>
<span className="lg">Edit Parties</span>
<div className="dialog-party-table">
{partyList.map((p, i)=> <>
<b>{p.name}</b>
<label htmlFor={`edit-keys-${p.name}`}>ExtraData:</label>
<input type="text" id={`payload-${p.name}`} defaultValue={JSON.stringify(p.allowed_extra)} />
<button onClick={() => updateExtras(p.name)}>update</button>
</>)}
</div>
<span className="danger-hint">To delete a party, enter <pre>deletethis</pre> and press update!</span>
<div className="dialog-bottom">
<span>Create a party:</span>
<input type="text" id={"create-party"} />
<button onClick={() => { createParty() }}>Create</button>
</div>
</div>
</dialog>
<dialog id="dialog-edit-user">
<div>
{editUserData ?
<>
<span className="lg">{"_id" in editUserData ? `Editing ${editUserData.name}`: "New Invitation"}</span>
<div className="dialog-table">
<span className="lg">{"_id" in editUserData ? `Editing ${editUserData.name}` : "New Invitation"}</span>
<div className="dialog-person-table">
<label htmlFor="edit-token">Token</label>
<input type="text" id="edit-token" value={editUserData.token} onChange={(e) => { setEditUserData({ ...editUserData, token: e.target.value }) }} />
<label htmlFor="edit-name">Name</label>
@ -108,12 +163,12 @@ export const AdminUI: React.FC = () => {
</div>
<div className="select-party-box">
{partyList.map((p, index) =>
<div className={`select-party ${index === selectedParty ? "selected" : ""}`}>
<span onClick={() => setSelectedParty(index)} key={index}>{p.name}</span>
<div key={index} onClick={() => setSelectedParty(index)} className={`select-party ${index === selectedParty ? "selected" : ""}`}>
<span>{p.name}</span>
</div>
)}
</div>
<div title="Create New Party" className="create-party-label">
<div title="Create New Party" className="create-party-label" onClick={() => editParty()}>
<span>
🎉
</span>

View File

@ -1,4 +1,4 @@
import { Person, RequestCreateGuest, RequestCreateParty, ResponseCreateParty, ResponseListGuests, ResponseListParties } from "./partyAdminApiTypes";
import { AllowedExtraLengths, Person, RequestCreateGuest, RequestCreateParty, ResponseCreateParty, ResponseListGuests, ResponseListParties } from "./partyAdminApiTypes";
export const parseToken = (uri: string): string | undefined => {
const x = uri.match(/https?:\/\/party\.leafbla\.de\/(?<token>.+)/);
@ -20,7 +20,7 @@ export const listPartyRequest = async (adminToken: string): Promise<ResponseList
};
export const createPartyRequest = async (adminToken: string, payload: RequestCreateParty): Promise<ResponseCreateParty> => {
const result = await fetch(`${apiUrl(adminToken)}`, { method: "post", body: JSON.stringify(payload) });
const result = await fetch(`${apiUrl(adminToken)}`, { method: "post", body: JSON.stringify(payload), headers: { "Content-Type": "application/json" } });
if (!result.ok) throw new Error("Error sending createPartyRequest");
const data = await result.json();
return data as ResponseCreateParty;
@ -47,9 +47,29 @@ export const modifyGuestRequest = async (adminToken: string, partyName: string,
return data as Person;
};
export const deleteGuestRequest = async (adminToken: string, partyName: string, payload: Person): Promise<Person> => {
export const deleteGuestRequest = async (adminToken: string, partyName: string, payload: Person): Promise<void> => {
const result = await fetch(`${apiUrl(adminToken)}/${partyName}/${payload._id}`, { method: "DELETE", headers: { "Content-Type": "application/json" } });
if (!result.ok) throw new Error("Error sending createGuestRequest");
};
export const deletePartyRequest = async (adminToken: string, partyName: string): Promise<void> => {
const result = await fetch(`${apiUrl(adminToken)}/${partyName}`, { method: "DELETE" });
if (!result.ok) throw new Error("Error sending deletePartyRequest");
};
/**
* @deprecated This info can already be found in the listPartiesResponse
*/
export const getAllowedExtras = async (adminToken: string, partyName: string): Promise<AllowedExtraLengths> => {
const result = await fetch(`${apiUrl(adminToken)}/${partyName}/userAllowedExtra`, { method: "get" });
if (!result.ok) throw new Error("Error getting allowed extras");
const data = await result.json();
return data as Person;
};
return data as AllowedExtraLengths;
};
export const setAllowedExtras = async (adminToken: string, partyName: string, payload: AllowedExtraLengths): Promise<void> => {
const result = await fetch(`${apiUrl(adminToken)}/${partyName}/userAllowedExtra`, { headers: { "Content-Type": "application/json" }, method: "PATCH", body: JSON.stringify(payload) });
if (!result.ok) throw new Error("Error setting allowed extras");
};

View File

@ -1,16 +1,16 @@
export type ResponseListParties = ResponseCreateParty[];
export type ResponseCreateParty = {
"_id": string,
_id: string,
name: string,
created: string,
allowedExtra: AllowedExtraLengths,
allowed_extra: AllowedExtraLengths,
}
export type AllowedExtraLengths = { [key: string]: number };
export type RequestCreateParty = {
name: "string",
"allowed_extra": AllowedExtraLengths,
name: string,
allowed_extra: AllowedExtraLengths,
}
export type ResponseListGuests = Person[];
@ -20,11 +20,11 @@ export type GrammaticalGender = "m" | "f" | "d";
export type ComingStatus = "yes" | "no" | "maybe" | null;
export type Person = {
"_id": string,
_id: string,
token: string,
name: string,
coming: ComingStatus,
"grammatical_gender": GrammaticalGender,
grammatical_gender: GrammaticalGender,
extra: Record<string, string>,
};
@ -32,6 +32,12 @@ export type RequestCreateGuest = {
token: string,
name: string,
coming: ComingStatus,
"grammatical_gender": GrammaticalGender,
grammatical_gender: GrammaticalGender,
extra: Record<string, string>,
};
};
export type CreatePartyRequest = {
name: string,
allowed_extra: AllowedExtraLengths,
}