Can create a family from the GUI
This commit is contained in:
		@@ -12,6 +12,7 @@ import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute";
 | 
			
		||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
 | 
			
		||||
import { BaseLoginPage } from "./widgets/BaseLoginpage";
 | 
			
		||||
import { DeleteAccountRoute } from "./routes/DeleteAccountRoute";
 | 
			
		||||
import { FamiliesListRoute } from "./routes/FamiliesListRoute";
 | 
			
		||||
 | 
			
		||||
interface AuthContext {
 | 
			
		||||
  signedIn: boolean;
 | 
			
		||||
@@ -38,6 +39,7 @@ export function App(): React.ReactElement {
 | 
			
		||||
 | 
			
		||||
        {signedIn ? (
 | 
			
		||||
          <Route path="*" element={<BaseAuthenticatedPage />}>
 | 
			
		||||
            <Route path="" element={<FamiliesListRoute />} />
 | 
			
		||||
            <Route path="profile" element={<ProfileRoute />} />
 | 
			
		||||
            <Route path="*" element={<NotFoundRoute />} />
 | 
			
		||||
          </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;
 | 
			
		||||
  user_name_len: LenConstraint;
 | 
			
		||||
  password_len: LenConstraint;
 | 
			
		||||
  family_name_len: LenConstraint;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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>;
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user