Genealogy as a feature (#175)
All checks were successful
continuous-integration/drone/push Build is passing
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:
@ -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
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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: {
|
||||
|
@ -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);
|
||||
}}
|
||||
|
@ -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;
|
||||
|
73
geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx
Normal file
73
geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx
Normal 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)!;
|
||||
}
|
@ -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";
|
||||
|
Reference in New Issue
Block a user