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; top: 0px;
left:0px; left:0px;
} }
.dialog-table { .dialog-person-table {
display: grid; display: grid;
gap: 5px; gap: 5px;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
.dialog-party-table {
display: grid;
gap: 5px;
grid-template-columns: 1fr auto 1fr auto;
}
.add-guest-button { .add-guest-button {
padding: 5px; padding: 5px;
border-radius: 4px; border-radius: 4px;
@ -171,7 +176,16 @@ dialog > div label {
width: 100%; width: 100%;
justify-content: space-between; 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; align-self: flex-end;
font-size: 8pt; font-size: 8pt;
color: gray; color: gray;

View File

@ -1,21 +1,22 @@
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import './AdminUI.css'; 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'; import { GrammaticalGender, Person, RequestCreateGuest, ResponseCreateParty, ResponseListGuests, ResponseListParties } from './partyAdminApiTypes';
export const AdminUI: React.FC = () => { export const AdminUI: React.FC = () => {
const [editingParty, setEditingParty] = useState(false);
const [partyList, setPartyList] = useState<ResponseListParties>([]); const [partyList, setPartyList] = useState<ResponseListParties>([]);
const [selectedParty, setSelectedParty] = useState<number | undefined>(undefined); const [selectedParty, setSelectedParty] = useState<number | undefined>(undefined);
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
const adminToken = useMemo(() => parseToken(location.href) ?? "", []); const adminToken = useMemo(() => parseToken(location.href) ?? "", []);
const loadPartyList = useCallback(async () => { const loadPartyList = useCallback(async () => {
const response = await listPartyRequest(adminToken); console.log("load party list");
const listPartyResponse = await listPartyRequest(adminToken);
if (selectedParty === undefined) if (selectedParty === undefined)
setSelectedParty(response.length > 0 ? 0 : undefined); setSelectedParty(listPartyResponse.length > 0 ? 0 : undefined);
setPartyList(response); setPartyList([...listPartyResponse]);
}, [adminToken, selectedParty]); }, [adminToken, selectedParty]);
const deleteUser = async () => { const deleteUser = async () => {
@ -33,11 +34,46 @@ export const AdminUI: React.FC = () => {
dialog.show(); dialog.show();
}; };
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 [editUserData, setEditUserData] = useState<RequestCreateGuest | Person>();
const dismissBackdrop = () => {
const dialog = document.getElementById("dialog-edit-user") as HTMLDialogElement; const createParty = () => {
setEditUserData(undefined); 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(); dialog.close();
})
setEditUserData(undefined);
setEditingParty(false);
}; };
const saveEditUser = () => { const saveEditUser = () => {
@ -58,15 +94,34 @@ export const AdminUI: React.FC = () => {
}, [adminToken, loadPartyList]); }, [adminToken, loadPartyList]);
return <> return <>
{editUserData ? {editUserData || editingParty ?
<div id="backdrop" onClick={dismissBackdrop} /> <div id="backdrop" onClick={dismissBackdrop} />
: null} : 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"> <dialog id="dialog-edit-user">
<div> <div>
{editUserData ? {editUserData ?
<> <>
<span className="lg">{"_id" in editUserData ? `Editing ${editUserData.name}` : "New Invitation"}</span> <span className="lg">{"_id" in editUserData ? `Editing ${editUserData.name}` : "New Invitation"}</span>
<div className="dialog-table"> <div className="dialog-person-table">
<label htmlFor="edit-token">Token</label> <label htmlFor="edit-token">Token</label>
<input type="text" id="edit-token" value={editUserData.token} onChange={(e) => { setEditUserData({ ...editUserData, token: e.target.value }) }} /> <input type="text" id="edit-token" value={editUserData.token} onChange={(e) => { setEditUserData({ ...editUserData, token: e.target.value }) }} />
<label htmlFor="edit-name">Name</label> <label htmlFor="edit-name">Name</label>
@ -108,12 +163,12 @@ export const AdminUI: React.FC = () => {
</div> </div>
<div className="select-party-box"> <div className="select-party-box">
{partyList.map((p, index) => {partyList.map((p, index) =>
<div className={`select-party ${index === selectedParty ? "selected" : ""}`}> <div key={index} onClick={() => setSelectedParty(index)} className={`select-party ${index === selectedParty ? "selected" : ""}`}>
<span onClick={() => setSelectedParty(index)} key={index}>{p.name}</span> <span>{p.name}</span>
</div> </div>
)} )}
</div> </div>
<div title="Create New Party" className="create-party-label"> <div title="Create New Party" className="create-party-label" onClick={() => editParty()}>
<span> <span>
🎉 🎉
</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 => { export const parseToken = (uri: string): string | undefined => {
const x = uri.match(/https?:\/\/party\.leafbla\.de\/(?<token>.+)/); 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> => { 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"); if (!result.ok) throw new Error("Error sending createPartyRequest");
const data = await result.json(); const data = await result.json();
return data as ResponseCreateParty; return data as ResponseCreateParty;
@ -47,9 +47,29 @@ export const modifyGuestRequest = async (adminToken: string, partyName: string,
return data as Person; 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" } }); 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"); if (!result.ok) throw new Error("Error sending createGuestRequest");
const data = await result.json();
return data as Person;
}; };
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 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 ResponseListParties = ResponseCreateParty[];
export type ResponseCreateParty = { export type ResponseCreateParty = {
"_id": string, _id: string,
name: string, name: string,
created: string, created: string,
allowedExtra: AllowedExtraLengths, allowed_extra: AllowedExtraLengths,
} }
export type AllowedExtraLengths = { [key: string]: number }; export type AllowedExtraLengths = { [key: string]: number };
export type RequestCreateParty = { export type RequestCreateParty = {
name: "string", name: string,
"allowed_extra": AllowedExtraLengths, allowed_extra: AllowedExtraLengths,
} }
export type ResponseListGuests = Person[]; export type ResponseListGuests = Person[];
@ -20,11 +20,11 @@ export type GrammaticalGender = "m" | "f" | "d";
export type ComingStatus = "yes" | "no" | "maybe" | null; export type ComingStatus = "yes" | "no" | "maybe" | null;
export type Person = { export type Person = {
"_id": string, _id: string,
token: string, token: string,
name: string, name: string,
coming: ComingStatus, coming: ComingStatus,
"grammatical_gender": GrammaticalGender, grammatical_gender: GrammaticalGender,
extra: Record<string, string>, extra: Record<string, string>,
}; };
@ -32,6 +32,12 @@ export type RequestCreateGuest = {
token: string, token: string,
name: string, name: string,
coming: ComingStatus, coming: ComingStatus,
"grammatical_gender": GrammaticalGender, grammatical_gender: GrammaticalGender,
extra: Record<string, string>, extra: Record<string, string>,
}; };
export type CreatePartyRequest = {
name: string,
allowed_extra: AllowedExtraLengths,
}