Skip to content

Commit

Permalink
improve forms and revalidation on delete (#159)
Browse files Browse the repository at this point in the history
* improve forms and revalidation on delete

* resolve chrome issues

* limit options for sum bits

* clarify that sum bits is the range for each measurement
  • Loading branch information
jbr committed Jun 2, 2023
1 parent b76844d commit adaa787
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 45 deletions.
37 changes: 15 additions & 22 deletions app/src/AccountForm.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,35 @@
import { useState, useCallback, ChangeEvent } from "react";
import Button from "react-bootstrap/Button";
import Form from "react-bootstrap/Form";
import ApiClient from "./ApiClient";
import { useNavigate, Form as RRForm } from "react-router-dom";
import FormGroup from "react-bootstrap/FormGroup";
import FormLabel from "react-bootstrap/FormLabel";
import FormControl from "react-bootstrap/FormControl";
import { Form } from "react-router-dom";
import { BuildingAdd } from "react-bootstrap-icons";

export default function AccountForm({ apiClient }: { apiClient: ApiClient }) {
export default function AccountForm() {
let [name, setName] = useState<string>("");
let navigate = useNavigate();
let create = useCallback(() => {
if (name) {
apiClient
.createAccount({ name })
.then((account) => navigate(`/accounts/${account.id}`));
}
}, [name, apiClient, navigate]);

let updateName = useCallback(
(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setName(event.target.value as string);
const updateName = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
},
[setName]
);

return (
<RRForm onSubmit={create}>
<Form.Group className="mb-3" controlId="Account">
<Form.Label>Account Name</Form.Label>
<Form.Control
<Form action="." method="POST">
<FormGroup className="mb-3" controlId="Account">
<FormLabel>Account Name</FormLabel>
<FormControl
name="name"
type="text"
placeholder="Account Name"
value={name}
onChange={updateName}
/>
</Form.Group>
</FormGroup>
<Button variant="primary" type="submit">
<BuildingAdd /> Create Account
</Button>
</RRForm>
</Form>
);
}
49 changes: 40 additions & 9 deletions app/src/Memberships.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import {
Form,
useSubmit,
} from "react-router-dom";
import React, { Suspense } from "react";
import React, { Suspense, useState } from "react";
import { Account, Membership, User } from "./ApiClient";
import { LinkContainer } from "react-router-bootstrap";
import { Button, FormControl, Spinner } from "react-bootstrap";
import { PersonSlash, PersonAdd, People } from "react-bootstrap-icons";
import Modal from "react-bootstrap/Modal";

export default function Memberships() {
let { account } = useRouteLoaderData("account") as {
Expand Down Expand Up @@ -73,29 +74,59 @@ function MembershipsFull() {

function DeleteMembershipButton({ membership }: { membership: Membership }) {
let submit = useSubmit();
let callback = React.useCallback(() => {
if (window.confirm(`Really remove ${membership.user_email}?`)) {
submit({ membershipId: membership.id }, { method: "delete" });
}
const [show, setShow] = useState(false);
const close = React.useCallback(() => setShow(false), []);
const open = React.useCallback(() => setShow(true), []);

let deleteMembership = React.useCallback(() => {
submit({ membershipId: membership.id }, { method: "delete" });
}, [membership, submit]);

return (
<Button variant="outline-danger" className="ml-auto" onClick={callback}>
<PersonSlash />
</Button>
<>
<Button variant="outline-danger" className="ml-auto" onClick={open}>
<PersonSlash />
</Button>
<Modal show={show} onHide={close}>
<Modal.Header closeButton>
<Modal.Title>Confirm Membership Removal</Modal.Title>
</Modal.Header>
<Modal.Body>
This user will no longer be able to view or create tasks on this
account
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={close}>
Close
</Button>
<Button variant="primary" onClick={deleteMembership}>
Remove {membership.user_email}
</Button>
</Modal.Footer>
</Modal>
</>
);
}

function AddMembershipForm() {
let [email, setEmail] = React.useState("");

return (
<Form action="." method="post">
<Form
action="."
method="post"
onSubmit={React.useCallback(() => {
setEmail("");
}, [setEmail])}
>
<Row className="my-3">
<Col xs="11">
<FormControl
type="email"
name="user_email"
id="user_email"
value={email}
autoComplete="off"
onChange={React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) =>
setEmail(event.target.value),
Expand Down
1 change: 0 additions & 1 deletion app/src/TaskDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ export function VdafIcon({
task: Task;
fill?: boolean;
}) {
console.log(task.vdaf.type);
switch (task.vdaf.type.toLowerCase()) {
case "sum":
return fill ? <FileEarmarkPlusFill /> : <FileEarmarkPlus />;
Expand Down
21 changes: 15 additions & 6 deletions app/src/TaskForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ function IsLeader({ handleChange, values }: FormikProps<NewTask>) {
checked={values.is_leader}
onChange={handleChange}
name="is_leader"
id="is_leader"
label="Leader"
/>
);
Expand Down Expand Up @@ -150,6 +151,7 @@ function QueryType(props: FormikProps<NewTask>) {
<FormCheck
type="radio"
name="query-type"
id="query=type-time"
checked={timeInterval}
onChange={checkboxChange}
label="Time Interval"
Expand All @@ -158,6 +160,7 @@ function QueryType(props: FormikProps<NewTask>) {
<FormCheck
type="radio"
name="query-type"
id="query-type-fixed"
checked={!timeInterval}
onChange={checkboxChange}
label="Fixed Size"
Expand Down Expand Up @@ -223,7 +226,7 @@ function HpkeConfig({ setFieldValue, errors }: FormikProps<NewTask>) {
);

return (
<FormGroup className="mb-3">
<FormGroup className="mb-3" controlId="hpke_config">
<FormLabel>DAP-encoded HPKE file</FormLabel>
<FormControl
type="file"
Expand All @@ -244,6 +247,7 @@ function TaskName(props: FormikProps<NewTask>) {
<FormControl
type="text"
name="name"
autoComplete="off"
placeholder="Task Name"
onChange={props.handleChange}
onBlur={props.handleBlur}
Expand Down Expand Up @@ -312,13 +316,16 @@ function TimePrecisionSeconds(props: FormikProps<NewTask>) {
);

return (
<FormGroup className="mb-3" controlId="time_precision_seconds">
<FormLabel column>Time Precision</FormLabel>
<FormGroup className="mb-3">
<FormLabel column htmlFor="time-precision-number">
Time Precision
</FormLabel>
<Row>
<Col xs="2">
<FormControl
type="number"
value={count || ""}
id="time-precision-number"
onChange={changeCount}
isInvalid={!!props.errors.time_precision_seconds}
/>
Expand All @@ -328,6 +335,7 @@ function TimePrecisionSeconds(props: FormikProps<NewTask>) {
value={unit}
onChange={changeUnit}
isInvalid={!!props.errors.time_precision_seconds}
id="time-precision-unit"
>
{Object.keys(seconds).map((unit) => (
<option key={unit} value={unit}>
Expand Down Expand Up @@ -456,16 +464,17 @@ function SumBits(props: FormikProps<NewTask>) {

return (
<FormGroup className="mb-3" controlId="vdaf.bits">
<FormLabel>Maximum Sum Value</FormLabel>
<FormLabel>Measurement Range</FormLabel>
<FormSelect
value={props.values.vdaf?.bits}
name="vdaf.bits"
onChange={handleChange}
onBlur={props.handleBlur}
>
{[...new Array(128)].map((_, i) => (
{[8, 16, 32, 64].map((i) => (
<option value={i} key={i}>
{Math.pow(2, i)}
Unsigned {i}-bit integer (0 to{" "}
{(Math.pow(2, i) - 1).toLocaleString()})
</option>
))}
</FormSelect>
Expand Down
46 changes: 39 additions & 7 deletions app/src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import React from "react";
import { createBrowserRouter, RouterProvider, defer } from "react-router-dom";
import {
createBrowserRouter,
RouterProvider,
defer,
redirect,
} from "react-router-dom";
import { ApiClientContext } from "./ApiClientContext";
import { ApiClient, PartialAccount } from "./ApiClient";
import AccountForm from "./AccountForm";
Expand Down Expand Up @@ -41,6 +46,9 @@ function buildRouter(apiClient: ApiClient) {
}
}),
}),
shouldRevalidate(_) {
return false;
},

errorElement: <ErrorPage apiClient={apiClient} />,

Expand Down Expand Up @@ -109,15 +117,25 @@ function buildRouter(apiClient: ApiClient) {
let data = Object.fromEntries(await request.formData());
switch (request.method) {
case "PATCH":
return await apiClient.updateAccount(
params.account_id as string,
data as unknown as PartialAccount
);
return {
account: await apiClient.updateAccount(
params.account_id as string,
data as unknown as PartialAccount
),
};
default:
throw new Error(`unexpected method ${request.method}`);
}
},

shouldRevalidate(args) {
return (
typeof args.actionResult === "object" &&
args.actionResult !== null &&
"account" in args.actionResult
);
},

children: [
{
path: "",
Expand All @@ -133,13 +151,15 @@ function buildRouter(apiClient: ApiClient) {
),
});
},

async action({ params, request }) {
let data = Object.fromEntries(await request.formData());
switch (request.method) {
case "DELETE":
return await apiClient.deleteMembership(
await apiClient.deleteMembership(
data.membershipId as string
);
return { deleted: data.membershipId };
case "POST":
return await apiClient.createMembership(
params.account_id as string,
Expand Down Expand Up @@ -192,7 +212,19 @@ function buildRouter(apiClient: ApiClient) {
},
{
path: "new",
element: <AccountForm apiClient={apiClient} />,
element: <AccountForm />,
async action({ request }) {
let data = Object.fromEntries(await request.formData());
switch (request.method) {
case "POST":
const account = await apiClient.createAccount(
data as unknown as PartialAccount
);
return redirect(`/accounts/${account.id}`);
default:
throw new Error(`unexpected method ${request.method}`);
}
},
},
],
},
Expand Down

0 comments on commit adaa787

Please sign in to comment.