251 lines
9.9 KiB
TypeScript

import { Alert, AppLayout, BreadcrumbGroup, Button, Container, Flashbar, FlashbarProps, Form, FormField, Header, Input, Link, SpaceBetween } from "@cloudscape-design/components";
import { useLoaderData, useSearchParams } from "react-router-dom";
import { DoorResponse } from "../types/DoorResponse";
import { useForm } from "react-hook-form";
import { InputTokenGroup } from "../components/InputTokenGroup";
import { ReactNode, useState } from "react";
import CInput from "react-hook-form-cloudscape/components/input";
import CTextArea from "react-hook-form-cloudscape/components/textarea";
import { fetchUrlEncoded } from "../helpers/FetchHelper";
export type DoorEditForm = DoorResponse & { pin: string, fallbackNumber: string, discordUser: string, isConfirmed: boolean };
const EDIT_ROUTE = '/api/door/edit';
const ONBOARD_ROUTE = '/api/door/onboard';
export interface EditPageProps {
isOnboarding?: boolean;
}
export const EditPage = ({ isOnboarding }: EditPageProps) => {
const doorResponse = useLoaderData() as DoorResponse;
const door = doorResponse.id;
const { setValue, setError, getValues, watch, formState, clearErrors, control, register } = useForm<DoorEditForm>({
defaultValues: {
...doorResponse,
isConfirmed: !isOnboarding,
},
mode: "all",
});
const [ alerts, setAlerts ] = useState<FlashbarProps.MessageDefinition[]>([]);
const dismissAlert = (id: string) => {
setAlerts(alerts => alerts.filter(alert => alert.id !== id));
}
const createAlert = (type: FlashbarProps.Type, content: ReactNode): FlashbarProps.MessageDefinition => {
const id = `${Math.random()}`;
return {
id,
type,
content,
dismissible: type !== 'in-progress',
onDismiss: () => dismissAlert(id),
}
}
const addAlert = (type: FlashbarProps.Type, content: ReactNode): string => {
const newAlert = createAlert(type, content);
setAlerts(alerts => [
newAlert,
...alerts,
]);
return newAlert.id as string;
}
const fallbackNumbers = watch("fallbackNumbers") || [];
const discordUsers = watch("discordUsers") || [];
const fallbackNumbersError = formState.errors.fallbackNumbers?.message;
const discordUsersError = formState.errors.discordUsers?.message;
const backUrl = isOnboarding? '/': `?door=${door}`;
const apiRoute = isOnboarding ? ONBOARD_ROUTE: EDIT_ROUTE;
return (
<AppLayout
contentType="form"
navigationHide
toolsHide
breadcrumbs={
<BreadcrumbGroup
items={[
{ text: 'Door', href: backUrl },
isOnboarding ? { text: "Onboard", href: "?onboard" }: { text: door, href: `?edit&door=${door}` },
]}
/>
}
notifications={
<Flashbar
stackItems={true}
items={alerts}
/>
}
content={
<Form
actions={
<SpaceBetween direction="horizontal" size="xs">
<Button formAction="none" variant="link" href={backUrl}>
Cancel
</Button>
<Button
variant="primary"
onClick={() => {
if (isOnboarding && !getValues("isConfirmed")) {
setError("isConfirmed", {
type: "manual",
message: "Must confirm that Doorman number is configured",
});
return;
}
const form = {
...getValues(),
id: undefined,
name: getValues("id"),
timeout: parseInt("" + getValues("timeout"))
};
const body = {
door,
newConfig: JSON.stringify(form),
};
fetchUrlEncoded(apiRoute, body)
.then(res => res.json())
.then(res => {
if (res.msg) {
addAlert("error", res.msg);
return;
}
if (!isOnboarding) {
addAlert("success", `Created approval, check Discord notifcation from Doorman to confirm and approve the changes`);
} else if (res.redirect) {
// redirect for discord login
addAlert("in-progress", `Created Door, you will now be redirected to Discord login to complete the onboarding`);
// redirect in 2 seconds
setTimeout(() => {
window.location = res.redirect;
}, 2_000);
}
});
}}
>
Submit
</Button>
</SpaceBetween>
}
header={
<Header variant="h1">
{isOnboarding? "Onboard Door": "Edit Door"}
</Header>
}
>
<Container>
<SpaceBetween direction="vertical" size="l">
<FormField label="Door Name" constraintText="Unique name for this Door, will be used in your Doorman URL">
<CInput readOnly={!isOnboarding} disabled={!isOnboarding} name="id" control={control} />
</FormField>
<FormField label="Buzzer Number" constraintText="Phone number that calls you when your buzzer is called">
<CInput readOnly={!isOnboarding} disabled={!isOnboarding} name="buzzer" control={control} />
</FormField>
<FormField label="PIN" constraintText={"Code to unlock this Door in Doorman"}>
<CInput type="password" name="pin" control={control} />
</FormField>
<FormField label="Buzzer Code" constraintText={"Extension that is used to reach your unit on your buzzer"}>
<CInput name="buzzerCode" control={control} />
</FormField>
<FormField label="Timeout" constraintText="Time in seconds for the door to remain unlocked from Doorman">
<CInput type="number" name="timeout" control={control} />
</FormField>
<FormField label="Unlock Key" constraintText="Phone digit pressed to grant access">
<CInput name="pressKey" control={control} />
</FormField>
<FormField
errorText={fallbackNumbersError}
label="Fallback numbers"
constraintText="Phone numbers to dial through in case Door is not unlocked by Doorman"
>
<InputTokenGroup
name={"fallbackNumber"}
control={control}
tokenGroupProps={{ items: fallbackNumbers.map(n => ({ label: n }))}}
onAdd={() => {
const newValue = getValues().fallbackNumber;
if (newValue.length === 0) {
return;
}
clearErrors("fallbackNumbers");
setValue("fallbackNumbers", [...fallbackNumbers, newValue]);
setValue("fallbackNumber", "");
}}
onDismiss={(i) => {
clearErrors("fallbackNumbers");
if (fallbackNumbers.length === 1) {
setError("fallbackNumbers", { message: "Can't remove last entry", type: "value" })
return;
}
fallbackNumbers.splice(i, 1);
setValue("fallbackNumbers", [...fallbackNumbers]);
}}
/>
</FormField>
{!isOnboarding &&
<FormField
label="Discord Users"
errorText={discordUsersError}
constraintText={
<>
<Link fontSize={"body-s"} href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID#h_01HRSTXPS5H5D7JBY2QKKPVKNA">Discord users</Link> to receive notifications
</>
}
>
<InputTokenGroup
name={"discordUser"}
control={control}
tokenGroupProps={{ items: discordUsers.map(n => ({ label: n }))}}
onAdd={() => {
const newValue = getValues().discordUser;
if (newValue.length === 0) {
return;
}
clearErrors("discordUsers");
setValue("discordUsers", [...discordUsers, newValue]);
setValue("discordUser", "");
}}
onDismiss={(i) => {
clearErrors("discordUsers");
if (discordUsers.length === 1) {
setError("discordUsers", { message: "Can't remove last entry" })
return;
}
discordUsers.splice(i, 1);
setValue("discordUsers", [...discordUsers]);
}}
/>
</FormField>
}
<FormField label="Welcome Message" constraintText="Message to display after a successful unlock">
<CTextArea name="greeting" control={control} />
</FormField>
{isOnboarding &&
<FormField label="Register Doorman in Building" constraintText="Confirm that you added our number to your buzzer system: 604-757-1824" errorText={formState.errors.isConfirmed?.message}>
<label>
I have added Doorman's number to my buzzer
<input type="checkbox" {...register("isConfirmed")} />
</label>
</FormField>
}
</SpaceBetween>
</Container>
</Form>
}
/>
);
};