Can create a family from the GUI
This commit is contained in:
parent
378761f6d5
commit
5b06886c71
@ -12,6 +12,7 @@ import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute";
|
|||||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
||||||
import { BaseLoginPage } from "./widgets/BaseLoginpage";
|
import { BaseLoginPage } from "./widgets/BaseLoginpage";
|
||||||
import { DeleteAccountRoute } from "./routes/DeleteAccountRoute";
|
import { DeleteAccountRoute } from "./routes/DeleteAccountRoute";
|
||||||
|
import { FamiliesListRoute } from "./routes/FamiliesListRoute";
|
||||||
|
|
||||||
interface AuthContext {
|
interface AuthContext {
|
||||||
signedIn: boolean;
|
signedIn: boolean;
|
||||||
@ -38,6 +39,7 @@ export function App(): React.ReactElement {
|
|||||||
|
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
<Route path="*" element={<BaseAuthenticatedPage />}>
|
<Route path="*" element={<BaseAuthenticatedPage />}>
|
||||||
|
<Route path="" element={<FamiliesListRoute />} />
|
||||||
<Route path="profile" element={<ProfileRoute />} />
|
<Route path="profile" element={<ProfileRoute />} />
|
||||||
<Route path="*" element={<NotFoundRoute />} />
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
16
geneit_app/src/api/FamilyApi.ts
Normal file
16
geneit_app/src/api/FamilyApi.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { APIClient } from "./ApiClient";
|
||||||
|
|
||||||
|
export interface Family {}
|
||||||
|
|
||||||
|
export class FamilyApi {
|
||||||
|
/**
|
||||||
|
* Create a new family
|
||||||
|
*/
|
||||||
|
static async CreateFamily(name: string): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "POST",
|
||||||
|
uri: "/family/create",
|
||||||
|
jsonData: { name: name },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ interface Constraints {
|
|||||||
mail_len: LenConstraint;
|
mail_len: LenConstraint;
|
||||||
user_name_len: LenConstraint;
|
user_name_len: LenConstraint;
|
||||||
password_len: LenConstraint;
|
password_len: LenConstraint;
|
||||||
|
family_name_len: LenConstraint;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OIDCProvider {
|
interface OIDCProvider {
|
||||||
|
55
geneit_app/src/dialogs/CreateFamilyDialog.tsx
Normal file
55
geneit_app/src/dialogs/CreateFamilyDialog.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { TextInputDialog } from "./TextInputDialog";
|
||||||
|
import { ServerApi } from "../api/ServerApi";
|
||||||
|
import { FamilyApi } from "../api/FamilyApi";
|
||||||
|
|
||||||
|
export function CreateFamilyDialog(p: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onCreated: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [name, setName] = React.useState("");
|
||||||
|
const [creating, setCreating] = React.useState(false);
|
||||||
|
const [error, setError] = React.useState(false);
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
setName("");
|
||||||
|
setCreating(false);
|
||||||
|
setError(false);
|
||||||
|
p.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFamily = async () => {
|
||||||
|
setCreating(true);
|
||||||
|
try {
|
||||||
|
await FamilyApi.CreateFamily(name);
|
||||||
|
setName("");
|
||||||
|
p.onCreated();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
setCreating(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextInputDialog
|
||||||
|
open={p.open}
|
||||||
|
title="Creation d'une famille"
|
||||||
|
text="Veuillez définir le nom que vous souhaitez donner à la famille créée :"
|
||||||
|
label="Nom de la famille"
|
||||||
|
minLen={ServerApi.Config.constraints.family_name_len.min}
|
||||||
|
maxLen={ServerApi.Config.constraints.family_name_len.max}
|
||||||
|
onClose={cancel}
|
||||||
|
onCancel={cancel}
|
||||||
|
submitButton="Créer la famille"
|
||||||
|
value={name}
|
||||||
|
onValueChange={setName}
|
||||||
|
isChecking={creating}
|
||||||
|
checkingMessage="Création en cours..."
|
||||||
|
errorIsBlocking={false}
|
||||||
|
error={error ? "Echec de la création de la famille !" : undefined}
|
||||||
|
onSubmit={createFamily}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
147
geneit_app/src/dialogs/TextInputDialog.tsx
Normal file
147
geneit_app/src/dialogs/TextInputDialog.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
export function TextInputDialog(p: {
|
||||||
|
/**
|
||||||
|
* Minimum amount of text that must be entered before the text can be validated
|
||||||
|
*/
|
||||||
|
minLen?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max len of text than can be entered
|
||||||
|
*/
|
||||||
|
maxLen?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title of the dialog
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message shown above the input dialog
|
||||||
|
*/
|
||||||
|
text: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label of the text field
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current value in the dialog
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of value
|
||||||
|
*/
|
||||||
|
type?: React.HTMLInputTypeAttribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when the value entered by the user is changed
|
||||||
|
*/
|
||||||
|
onValueChange: (v: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify whether the value is being checked
|
||||||
|
*/
|
||||||
|
isChecking?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message shown while the value is checked
|
||||||
|
*/
|
||||||
|
checkingMessage?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text of the submit button
|
||||||
|
*/
|
||||||
|
submitButton?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function called when the user click on the submit button
|
||||||
|
*/
|
||||||
|
onSubmit: (value: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function called when the user click on the submit button
|
||||||
|
*/
|
||||||
|
onCancel: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify whether the dialog should be open or not
|
||||||
|
*/
|
||||||
|
open: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function called when the user click outside the dialog
|
||||||
|
*/
|
||||||
|
onClose: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error message to show on the input text
|
||||||
|
*/
|
||||||
|
error?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to true (the default), when an error is set, the submit button will be disabled
|
||||||
|
*/
|
||||||
|
errorIsBlocking?: boolean;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const canSubmit =
|
||||||
|
p.value.length >= (p.minLen ?? 0) &&
|
||||||
|
(p.errorIsBlocking === false || p.error === undefined);
|
||||||
|
|
||||||
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!canSubmit) return;
|
||||||
|
|
||||||
|
p.onSubmit(p.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={p.open} onClose={p.onClose}>
|
||||||
|
<Box component="form" noValidate onSubmit={handleSubmit}>
|
||||||
|
{p.title && <DialogTitle>{p.title}</DialogTitle>}
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>{p.text}</DialogContentText>
|
||||||
|
<TextField
|
||||||
|
disabled={p.isChecking}
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
label={p.label}
|
||||||
|
type={p.type ?? "text"}
|
||||||
|
fullWidth
|
||||||
|
variant="standard"
|
||||||
|
value={p.value}
|
||||||
|
onChange={(e) => p.onValueChange(e.target.value)}
|
||||||
|
inputProps={{ maxLength: p.maxLen ?? 255 }}
|
||||||
|
error={p.error !== undefined}
|
||||||
|
helperText={p.error}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{p.isChecking && (
|
||||||
|
<span>
|
||||||
|
<CircularProgress size={15} />{" "}
|
||||||
|
{p.checkingMessage ?? "Contrôle en cours..."}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={p.onClose}>Annuler</Button>
|
||||||
|
<Button disabled={!canSubmit} type="submit">
|
||||||
|
{p.submitButton ?? "Valider"}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
104
geneit_app/src/routes/FamiliesListRoute.tsx
Normal file
104
geneit_app/src/routes/FamiliesListRoute.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
|
import { Family } from "../api/FamilyApi";
|
||||||
|
import { Button, Typography } from "@mui/material";
|
||||||
|
import { CreateFamilyDialog } from "../dialogs/CreateFamilyDialog";
|
||||||
|
|
||||||
|
export function FamiliesListRoute(): React.ReactElement {
|
||||||
|
const loadKey = React.useRef(1);
|
||||||
|
|
||||||
|
const [families, setFamilies] = React.useState<Family[] | null>(null);
|
||||||
|
|
||||||
|
const [createFamily, setCreateFamily] = React.useState(false);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
// TODO : implement
|
||||||
|
setFamilies([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reload = () => {
|
||||||
|
loadKey.current += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRequestCreateFamily = async () => {
|
||||||
|
setCreateFamily(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={loadKey.current}
|
||||||
|
load={load}
|
||||||
|
errMsg="Echec du chargement de la liste des familles"
|
||||||
|
build={() => (
|
||||||
|
<>
|
||||||
|
{families!!.length === 0 ? (
|
||||||
|
<NoFamilyWidget onRequestCreateFamily={onRequestCreateFamily} />
|
||||||
|
) : (
|
||||||
|
<HasFamilysWidget onReload={reload} families={families!} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/** Create family dialog anchor */}
|
||||||
|
<CreateFamilyDialog
|
||||||
|
open={createFamily}
|
||||||
|
onClose={() => setCreateFamily(false)}
|
||||||
|
onCreated={() => {
|
||||||
|
setCreateFamily(false);
|
||||||
|
reload();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoFamilyWidget(p: {
|
||||||
|
onRequestCreateFamily: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div style={{ flex: 1, display: "flex", alignItems: "center" }}>
|
||||||
|
<Typography variant="h3" style={{ flex: 1, textAlign: "center" }}>
|
||||||
|
Vous n'appartenez à aucune famille !
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NoFamilyButton
|
||||||
|
label="Créer une famille"
|
||||||
|
onClick={p.onRequestCreateFamily}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NoFamilyButton label="Rejoindre une famille" onClick={() => {}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoFamilyButton(p: {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
style={{ width: "300px", marginBottom: "10px" }}
|
||||||
|
onClick={p.onClick}
|
||||||
|
>
|
||||||
|
{p.label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HasFamilysWidget(p: {
|
||||||
|
families: Family[];
|
||||||
|
onReload: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
return <p>todo has families</p>;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user