Genealogy as a feature (#175)
All checks were successful
continuous-integration/drone/push Build is passing

Start our journey into turning GeneIT as afully featured family intranet by making genealogy a feature that can be disabled by family admins

Reviewed-on: #175
This commit is contained in:
2024-05-16 19:15:15 +00:00
parent 0442538bd5
commit c8ee881b2c
34 changed files with 726 additions and 443 deletions

View File

@ -4,6 +4,7 @@ import {
mdiContentCopy,
mdiCrowd,
mdiFamilyTree,
mdiFileTree,
mdiHumanMaleFemale,
mdiLockCheck,
mdiPlus,
@ -26,9 +27,7 @@ import {
} from "@mui/material";
import React from "react";
import { Outlet, useLocation, useParams } from "react-router-dom";
import { CoupleApi, CouplesList } from "../api/CoupleApi";
import { ExtendedFamilyInfo, FamilyApi } from "../api/FamilyApi";
import { MemberApi, MembersList } from "../api/MemberApi";
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
@ -37,12 +36,8 @@ import { RouterLink } from "./RouterLink";
interface FamilyContext {
family: ExtendedFamilyInfo;
members: MembersList;
couples: CouplesList;
familyId: number;
reloadFamilyInfo: () => void;
reloadMembersList: () => Promise<void>;
reloadCouplesList: () => Promise<void>;
}
const FamilyContextK = React.createContext<FamilyContext | null>(null);
@ -54,8 +49,6 @@ export function BaseFamilyRoute(): React.ReactElement {
const confirm = useConfirm();
const [family, setFamily] = React.useState<null | ExtendedFamilyInfo>(null);
const [members, setMembers] = React.useState<null | MembersList>(null);
const [couples, setCouples] = React.useState<null | CouplesList>(null);
const loadKey = React.useRef(1);
@ -64,15 +57,11 @@ export function BaseFamilyRoute(): React.ReactElement {
const load = async () => {
const familyID = Number(familyId);
setFamily(await FamilyApi.GetSingle(familyID));
setMembers(await MemberApi.GetEntireList(familyID));
setCouples(await CoupleApi.GetEntireList(familyID));
};
const onReload = async () => {
loadKey.current += 1;
setFamily(null);
setMembers(null);
setCouples(null);
return new Promise<void>((res, _rej) => {
loadPromise.current = () => res();
@ -106,7 +95,7 @@ export function BaseFamilyRoute(): React.ReactElement {
return (
<AsyncWidget
ready={family !== null && members !== null}
ready={family !== null}
loadKey={`${familyId}-${loadKey.current}`}
load={load}
errMsg="Échec du chargement des informations de la famille !"
@ -120,12 +109,8 @@ export function BaseFamilyRoute(): React.ReactElement {
<FamilyContextK.Provider
value={{
family: family!,
members: members!,
couples: couples!,
familyId: family!.family_id,
reloadFamilyInfo: onReload,
reloadMembersList: onReload,
reloadCouplesList: onReload,
}}
>
<Box
@ -147,41 +132,57 @@ export function BaseFamilyRoute(): React.ReactElement {
<FamilyLink icon={<HomeIcon />} label="Accueil" uri="" />
<FamilyLink
icon={<Icon path={mdiCrowd} size={1} />}
label="Membres"
uri="members"
secondaryAction={
<Tooltip title="Créer une nouvelle fiche de membre">
<RouterLink to={family!.URL("member/create")}>
<IconButton>
<Icon path={mdiPlus} size={0.75} />
</IconButton>
</RouterLink>
</Tooltip>
}
/>
{family?.enable_genealogy && (
<>
<Divider sx={{ my: 1 }} />
<ListSubheader component="div">Généalogie</ListSubheader>
<FamilyLink
icon={<Icon path={mdiHumanMaleFemale} size={1} />}
label="Couples"
uri="couples"
secondaryAction={
<Tooltip title="Créer une nouvelle fiche de couple">
<RouterLink to={family!.URL("couple/create")}>
<IconButton>
<Icon path={mdiPlus} size={0.75} />
</IconButton>
</RouterLink>
</Tooltip>
}
/>
<FamilyLink
icon={<HomeIcon />}
label="Accueil"
uri="genealogy"
/>
<FamilyLink
icon={<Icon path={mdiCrowd} size={1} />}
label="Membres"
uri="genealogy/members"
secondaryAction={
<Tooltip title="Créer une nouvelle fiche de membre">
<RouterLink
to={family!.URL("genealogy/member/create")}
>
<IconButton>
<Icon path={mdiPlus} size={0.75} />
</IconButton>
</RouterLink>
</Tooltip>
}
/>
<FamilyLink
icon={<Icon path={mdiFamilyTree} size={1} />}
label="Arbre"
uri="tree"
/>
<FamilyLink
icon={<Icon path={mdiHumanMaleFemale} size={1} />}
label="Couples"
uri="genealogy/couples"
secondaryAction={
<Tooltip title="Créer une nouvelle fiche de couple">
<RouterLink
to={family!.URL("genealogy/couple/create")}
>
<IconButton>
<Icon path={mdiPlus} size={0.75} />
</IconButton>
</RouterLink>
</Tooltip>
}
/>
<FamilyLink
icon={<Icon path={mdiFamilyTree} size={1} />}
label="Arbre"
uri="genealogy/tree"
/>
</>
)}
<Divider sx={{ my: 1 }} />
<ListSubheader component="div">Administration</ListSubheader>
@ -198,6 +199,14 @@ export function BaseFamilyRoute(): React.ReactElement {
uri="settings"
/>
{family?.enable_genealogy && (
<FamilyLink
icon={<Icon path={mdiFileTree} size={1} />}
label="Généalogie"
uri="genealogy/settings"
/>
)}
{/* Invitation code */}
<ListItem

View File

@ -5,8 +5,8 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { TreeItem, SimpleTreeView } from "@mui/x-tree-view";
import React from "react";
import { useNavigate } from "react-router-dom";
import { Couple } from "../api/CoupleApi";
import { Member, fmtDate } from "../api/MemberApi";
import { Couple } from "../api/genealogy/CoupleApi";
import { Member, fmtDate } from "../api/genealogy/MemberApi";
import { FamilyTreeNode } from "../utils/family_tree";
import { useFamily } from "./BaseFamilyRoute";
import { MemberPhoto } from "./MemberPhoto";

View File

@ -1,5 +1,5 @@
import { Avatar } from "@mui/material";
import { Couple } from "../api/CoupleApi";
import { Couple } from "../api/genealogy/CoupleApi";
export function CouplePhoto(p: {
couple: Couple;

View File

@ -5,7 +5,7 @@ import {
ListItemSecondaryAction,
ListItemText,
} from "@mui/material";
import { Member, fmtDate } from "../api/MemberApi";
import { Member, fmtDate } from "../api/genealogy/MemberApi";
import { MemberPhoto } from "./MemberPhoto";
import Icon from "@mdi/react";
import FemaleIcon from "@mui/icons-material/Female";

View File

@ -1,5 +1,5 @@
import { Avatar } from "@mui/material";
import { Member } from "../api/MemberApi";
import { Member } from "../api/genealogy/MemberApi";
export function MemberPhoto(p: {
member?: Member;

View File

@ -1,6 +1,6 @@
import { Stack, TextField, Typography } from "@mui/material";
import { NumberConstraint, ServerApi } from "../../api/ServerApi";
import { DateValue, fmtDate } from "../../api/MemberApi";
import { DateValue, fmtDate } from "../../api/genealogy/MemberApi";
import { PropEdit } from "./PropEdit";
export function DateInput(p: {

View File

@ -2,9 +2,10 @@ import ClearIcon from "@mui/icons-material/Clear";
import { Autocomplete, IconButton, TextField, Typography } from "@mui/material";
import React from "react";
import { useNavigate } from "react-router-dom";
import { Member } from "../../api/MemberApi";
import { Member } from "../../api/genealogy/MemberApi";
import { useFamily } from "../BaseFamilyRoute";
import { MemberItem } from "../MemberItem";
import { useGenealogy } from "../genealogy/BaseGenealogyRoute";
export function MemberInput(p: {
editable: boolean;
@ -15,13 +16,14 @@ export function MemberInput(p: {
}): React.ReactElement {
const n = useNavigate();
const family = useFamily();
const genealogy = useGenealogy();
const choices = family.members.filter(p.filter);
const choices = genealogy.members.filter(p.filter);
const [inputValue, setInputValue] = React.useState("");
if (p.current) {
const member = family.members.get(p.current)!;
const member = genealogy.members.get(p.current)!;
return (
<div style={{ display: "flex", alignItems: "center" }}>
<Typography variant="body2">{p.label}</Typography>
@ -30,7 +32,7 @@ export function MemberInput(p: {
onClick={
!p.editable
? () => {
n(family.family.URL(`member/${member.id}`));
n(family.family.memberURL(member));
}
: undefined
}
@ -55,7 +57,7 @@ export function MemberInput(p: {
return (
<Autocomplete
value={p.current ? family.members.get(p.current) : undefined}
value={p.current ? genealogy.members.get(p.current) : undefined}
onChange={(_event: any, newValue: Member | null | undefined) => {
p.onValueChange(newValue?.id);
}}

View File

@ -6,7 +6,7 @@ import {
Radio,
Typography,
} from "@mui/material";
import { Sex } from "../../api/MemberApi";
import { Sex } from "../../api/genealogy/MemberApi";
export function SexSelection(p: {
readonly?: boolean;

View File

@ -0,0 +1,73 @@
import React from "react";
import { Outlet } from "react-router-dom";
import { CoupleApi, CouplesList } from "../../api/genealogy/CoupleApi";
import { MemberApi, MembersList } from "../../api/genealogy/MemberApi";
import { AsyncWidget } from "../AsyncWidget";
import { useFamily } from "../BaseFamilyRoute";
interface FamilyContext {
members: MembersList;
couples: CouplesList;
reloadMembersList: () => Promise<void>;
reloadCouplesList: () => Promise<void>;
}
const GenealogyContextK = React.createContext<FamilyContext | null>(null);
export function BaseGenealogyRoute(): React.ReactElement {
const family = useFamily();
const [members, setMembers] = React.useState<null | MembersList>(null);
const [couples, setCouples] = React.useState<null | CouplesList>(null);
const loadKey = React.useRef(1);
const loadPromise = React.useRef<() => void>();
const load = async () => {
setMembers(await MemberApi.GetEntireList(family.familyId));
setCouples(await CoupleApi.GetEntireList(family.familyId));
};
const onReload = async () => {
loadKey.current += 1;
setMembers(null);
setCouples(null);
return new Promise<void>((res, _rej) => {
loadPromise.current = () => res();
});
};
return (
<AsyncWidget
ready={members !== null && couples !== null}
loadKey={`${family.familyId}-${loadKey.current}`}
load={load}
errMsg="Échec du chargement des informations de généalogie de la famille !"
build={() => {
if (loadPromise.current != null) {
loadPromise.current?.();
loadPromise.current = undefined;
}
return (
<GenealogyContextK.Provider
value={{
members: members!,
couples: couples!,
reloadMembersList: onReload,
reloadCouplesList: onReload,
}}
>
<Outlet />
</GenealogyContextK.Provider>
);
}}
/>
);
}
export function useGenealogy(): FamilyContext {
return React.useContext(GenealogyContextK)!;
}

View File

@ -5,8 +5,8 @@ import { IconButton, Tooltip } from "@mui/material";
import jsPDF from "jspdf";
import React from "react";
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
import { Couple } from "../../api/CoupleApi";
import { Member } from "../../api/MemberApi";
import { Couple } from "../../api/genealogy/CoupleApi";
import { Member } from "../../api/genealogy/MemberApi";
import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider";
import { FamilyTreeNode } from "../../utils/family_tree";
import { downloadBlob } from "../../utils/files_utils";