Separate genealogy settings from family settings
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This commit is contained in:
Pierre HUBERT 2024-05-16 20:39:37 +02:00
parent 28b29d6cd6
commit 9c8b424759
6 changed files with 257 additions and 153 deletions

View File

@ -38,6 +38,7 @@ import { FamilyTreeRoute } from "./routes/family/genealogy/FamilyTreeRoute";
import { FamilyMemberTreeRoute } from "./routes/family/genealogy/FamilyMemberTreeRoute"; import { FamilyMemberTreeRoute } from "./routes/family/genealogy/FamilyMemberTreeRoute";
import { GenealogyHomeRoute } from "./routes/family/genealogy/GenealogyHomeRoute"; import { GenealogyHomeRoute } from "./routes/family/genealogy/GenealogyHomeRoute";
import { BaseGenealogyRoute } from "./widgets/genealogy/BaseGenealogyRoute"; import { BaseGenealogyRoute } from "./widgets/genealogy/BaseGenealogyRoute";
import { GenalogySettingsRoute } from "./routes/family/genealogy/GenalogySettingsRoute";
interface AuthContext { interface AuthContext {
signedIn: boolean; signedIn: boolean;
@ -105,6 +106,7 @@ export function App(): React.ReactElement {
path="tree/:memberId" path="tree/:memberId"
element={<FamilyMemberTreeRoute />} element={<FamilyMemberTreeRoute />}
/> />
<Route path="settings" element={<GenalogySettingsRoute />} />
<Route path="*" element={<NotFoundRoute />} /> <Route path="*" element={<NotFoundRoute />} />
</Route> </Route>

View File

@ -233,9 +233,9 @@ export class FamilyApi {
*/ */
static async UpdateFamily(settings: { static async UpdateFamily(settings: {
id: number; id: number;
name: string; name?: string;
enable_genealogy: boolean; enable_genealogy?: boolean;
disable_couple_photos: boolean; disable_couple_photos?: boolean;
}): Promise<void> { }): Promise<void> {
await APIClient.exec({ await APIClient.exec({
method: "PATCH", method: "PATCH",

View File

@ -1,26 +1,19 @@
import DownloadIcon from "@mui/icons-material/Download";
import UploadIcon from "@mui/icons-material/Upload";
import { import {
Alert,
Box, Box,
Button, Button,
CardActions, CardActions,
CardContent, CardContent,
Checkbox,
FormControlLabel, FormControlLabel,
Switch,
TextField, TextField,
Tooltip,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { FamilyApi } from "../../api/FamilyApi"; import { FamilyApi } from "../../api/FamilyApi";
import { ServerApi } from "../../api/ServerApi"; import { ServerApi } from "../../api/ServerApi";
import { DataApi } from "../../api/genealogy/DataApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
import { downloadBlob, selectFileToUpload } from "../../utils/files_utils";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useFamily } from "../../widgets/BaseFamilyRoute";
import { FamilyCard } from "../../widgets/FamilyCard"; import { FamilyCard } from "../../widgets/FamilyCard";
import { formatDate } from "../../widgets/TimeWidget"; import { formatDate } from "../../widgets/TimeWidget";
@ -55,7 +48,6 @@ export function FamilySettingsRoute(): React.ReactElement {
return ( return (
<> <>
<FamilySettingsCard /> <FamilySettingsCard />
{family.family.enable_genealogy && <GenealogyExportCard />}
<div style={{ textAlign: "center", marginTop: "50px" }}> <div style={{ textAlign: "center", marginTop: "50px" }}>
<Button <Button
size="small" size="small"
@ -79,9 +71,6 @@ function FamilySettingsCard(): React.ReactElement {
const [enableGenealogy, setEnableGenealogy] = React.useState( const [enableGenealogy, setEnableGenealogy] = React.useState(
family.family.enable_genealogy family.family.enable_genealogy
); );
const [disableCouplePhotos, setDisableCouplePhotos] = React.useState(
family.family.disable_couple_photos
);
const canEdit = family.family.is_admin; const canEdit = family.family.is_admin;
@ -97,7 +86,6 @@ function FamilySettingsCard(): React.ReactElement {
id: family.family.family_id, id: family.family.family_id,
name: newName, name: newName,
enable_genealogy: enableGenealogy, enable_genealogy: enableGenealogy,
disable_couple_photos: disableCouplePhotos,
}); });
family.reloadFamilyInfo(); family.reloadFamilyInfo();
@ -152,30 +140,13 @@ function FamilySettingsCard(): React.ReactElement {
<FormControlLabel <FormControlLabel
disabled={!canEdit} disabled={!canEdit}
control={ control={
<Checkbox <Switch
checked={enableGenealogy} checked={enableGenealogy}
onChange={(_e, c) => setEnableGenealogy(c)} onChange={(_e, c) => setEnableGenealogy(c)}
/> />
} }
label="Activer la généalogie" label="Activer le module de généalogie"
/> />
{enableGenealogy && (
<Tooltip
title="Les photos de couple ne sont pas utilisées en pratique dans les arbres généalogiques. Il est possible de masquer les formulaires d'édition de photos de couple pour limiter le risque de confusion."
arrow
>
<FormControlLabel
disabled={!canEdit}
control={
<Checkbox
checked={disableCouplePhotos}
onChange={(_e, c) => setDisableCouplePhotos(c)}
/>
}
label="Désactiver les photos de couple"
/>
</Tooltip>
)}
</Box> </Box>
</CardContent> </CardContent>
<CardActions> <CardActions>
@ -190,109 +161,3 @@ function FamilySettingsCard(): React.ReactElement {
</FamilyCard> </FamilyCard>
); );
} }
function GenealogyExportCard(): React.ReactElement {
const loading = useLoadingMessage();
const confirm = useConfirm();
const alert = useAlert();
const family = useFamily();
const [error, setError] = React.useState<string>();
const [success, setSuccess] = React.useState<string>();
const exportData = async () => {
loading.show("Export des données");
try {
setError(undefined);
setSuccess(undefined);
const blob = await DataApi.ExportData(family.familyId);
downloadBlob(blob, `Export-${new Date().getTime()}.zip`);
setSuccess("Export des données effectué avec succès !");
} catch (e) {
console.error(e);
setError("Echec de l'export des données de la famille !");
}
loading.hide();
};
const importData = async () => {
try {
if (
!(await confirm(
"Attention ! Cette opération a pour effet d'effacer toutes les données existantes en base ! Voulez-vous vraiment poursuivre l'opération ?"
))
)
return;
const file = await selectFileToUpload({
allowedTypes: ["application/zip"],
});
if (file === null) return;
setError(undefined);
setSuccess(undefined);
loading.show("Restauration des données de la famille en cours...");
await DataApi.ImportData(family.familyId, file);
family.reloadFamilyInfo();
alert("Import des données de la famille effectué avec succès !");
} catch (e) {
console.error(e);
setError(`Echec de l'import des données de la famille ! (${e})`);
}
loading.hide();
};
return (
<FamilyCard error={error} success={success}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Export / import des données de généalogie
</Typography>
<p>
Vous pouvez, à des fins de sauvegardes ou de transfert, exporter et
importer l'ensemble des données des membres et des couples de cette
famille, sous format ZIP.
</p>
<Alert severity="warning">
Attention ! La restauration des données de la famille provoque
préalablement l'effacement de toutes les données enregistrées dans la
famille ! Par ailleurs, la restauration n'est pas réversible !
</Alert>
<p>&nbsp;</p>
<Button
startIcon={<DownloadIcon />}
variant="outlined"
fullWidth
onClick={exportData}
size={"large"}
style={{ marginBottom: "10px" }}
>
Exporter les données de la famille
</Button>
<Button
startIcon={<UploadIcon />}
variant="outlined"
color="warning"
fullWidth
onClick={importData}
disabled={!family.family.is_admin}
size={"large"}
>
Importer les données de la famille
</Button>
</CardContent>
</FamilyCard>
);
}

View File

@ -0,0 +1,221 @@
import DownloadIcon from "@mui/icons-material/Download";
import UploadIcon from "@mui/icons-material/Upload";
import {
Alert,
Box,
Button,
CardActions,
CardContent,
FormControlLabel,
Switch,
Tooltip,
Typography,
} from "@mui/material";
import React from "react";
import { FamilyApi } from "../../../api/FamilyApi";
import { DataApi } from "../../../api/genealogy/DataApi";
import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
import { downloadBlob, selectFileToUpload } from "../../../utils/files_utils";
import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { FamilyCard } from "../../../widgets/FamilyCard";
export function GenalogySettingsRoute(): React.ReactElement {
return (
<>
<GenealogySettingsCard />
<GenealogyExportCard />
</>
);
}
function GenealogySettingsCard(): React.ReactElement {
const alert = useAlert();
const family = useFamily();
const [disableCouplePhotos, setDisableCouplePhotos] = React.useState(
family.family.disable_couple_photos
);
const canEdit = family.family.is_admin;
const [error, setError] = React.useState<string>();
const [success, setSuccess] = React.useState<string>();
const updateFamily = async () => {
try {
setError(undefined);
setSuccess(undefined);
await FamilyApi.UpdateFamily({
id: family.family.family_id,
disable_couple_photos: disableCouplePhotos,
});
family.reloadFamilyInfo();
alert("Les paramètres de la famille ont été mis à jour avec succès !");
} catch (e) {
console.error(e);
setError("Echec de la mise à jour des paramètres de la famille !");
}
};
return (
<FamilyCard error={error} success={success}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Paramètres du module de généalogie
</Typography>
<Box
component="form"
sx={{
"& .MuiTextField-root": { my: 1 },
}}
noValidate
autoComplete="off"
>
<Tooltip
title="Les photos de couple ne sont pas utilisées en pratique dans les arbres généalogiques. Il est possible de masquer les formulaires d'édition de photos de couple pour limiter le risque de confusion."
arrow
>
<FormControlLabel
disabled={!canEdit}
control={
<Switch
checked={disableCouplePhotos}
onChange={(_e, c) => setDisableCouplePhotos(c)}
/>
}
label="Désactiver les photos de couple"
/>
</Tooltip>
</Box>
</CardContent>
<CardActions>
<Button
onClick={updateFamily}
disabled={!canEdit}
style={{ marginLeft: "auto" }}
>
Enregistrer
</Button>
</CardActions>
</FamilyCard>
);
}
function GenealogyExportCard(): React.ReactElement {
const loading = useLoadingMessage();
const confirm = useConfirm();
const alert = useAlert();
const family = useFamily();
const [error, setError] = React.useState<string>();
const [success, setSuccess] = React.useState<string>();
const exportData = async () => {
loading.show("Export des données");
try {
setError(undefined);
setSuccess(undefined);
const blob = await DataApi.ExportData(family.familyId);
downloadBlob(blob, `Export-${new Date().getTime()}.zip`);
setSuccess("Export des données effectué avec succès !");
} catch (e) {
console.error(e);
setError("Echec de l'export des données de la famille !");
}
loading.hide();
};
const importData = async () => {
try {
if (
!(await confirm(
"Attention ! Cette opération a pour effet d'effacer toutes les données existantes en base ! Voulez-vous vraiment poursuivre l'opération ?"
))
)
return;
const file = await selectFileToUpload({
allowedTypes: ["application/zip"],
});
if (file === null) return;
setError(undefined);
setSuccess(undefined);
loading.show(
"Restauration des données de généalogie de la famille en cours..."
);
await DataApi.ImportData(family.familyId, file);
family.reloadFamilyInfo();
alert(
"Import des données de généalogie de la famille effectué avec succès !"
);
} catch (e) {
console.error(e);
setError(
`Echec de l'import des données de généalogie de la famille ! (${e})`
);
}
loading.hide();
};
return (
<FamilyCard error={error} success={success}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Export / import des données de généalogie
</Typography>
<p>
Vous pouvez, à des fins de sauvegardes ou de transfert, exporter et
importer l'ensemble des données des membres et des couples de cette
famille, sous format ZIP.
</p>
<Alert severity="warning">
Attention ! La restauration des données de généalogie de la famille
provoque préalablement l'effacement de toutes les données enregistrées
dans la famille ! Par ailleurs, la restauration n'est pas réversible !
</Alert>
<p>&nbsp;</p>
<Button
startIcon={<DownloadIcon />}
variant="outlined"
fullWidth
onClick={exportData}
size={"large"}
style={{ marginBottom: "10px" }}
>
Exporter les données de généalogie
</Button>
<Button
startIcon={<UploadIcon />}
variant="outlined"
color="warning"
fullWidth
onClick={importData}
disabled={!family.family.is_admin}
size={"large"}
>
Importer les données de généalogie
</Button>
</CardContent>
</FamilyCard>
);
}

View File

@ -4,6 +4,7 @@ import {
mdiContentCopy, mdiContentCopy,
mdiCrowd, mdiCrowd,
mdiFamilyTree, mdiFamilyTree,
mdiFileTree,
mdiHumanMaleFemale, mdiHumanMaleFemale,
mdiLockCheck, mdiLockCheck,
mdiPlus, mdiPlus,
@ -190,6 +191,14 @@ export function BaseFamilyRoute(): React.ReactElement {
uri="settings" uri="settings"
/> />
{family?.enable_genealogy && (
<FamilyLink
icon={<Icon path={mdiFileTree} size={1} />}
label="Généalogie"
uri="genealogy/settings"
/>
)}
{/* Invitation code */} {/* Invitation code */}
<ListItem <ListItem

View File

@ -103,9 +103,9 @@ pub async fn leave(f: FamilyInPath) -> HttpResult {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct UpdateFamilyBody { pub struct UpdateFamilyBody {
name: String, name: Option<String>,
enable_genealogy: bool, enable_genealogy: Option<bool>,
disable_couple_photos: bool, disable_couple_photos: Option<bool>,
} }
/// Update a family /// Update a family
@ -113,17 +113,24 @@ pub async fn update(
f: FamilyInPathWithAdminMembership, f: FamilyInPathWithAdminMembership,
req: web::Json<UpdateFamilyBody>, req: web::Json<UpdateFamilyBody>,
) -> HttpResult { ) -> HttpResult {
if !StaticConstraints::default() let mut family = families_service::get_by_id(f.family_id()).await?;
.family_name_len
.validate(&req.name) if let Some(name) = &req.name {
{ if !StaticConstraints::default().family_name_len.validate(name) {
return Ok(HttpResponse::BadRequest().body("Invalid family name!")); return Ok(HttpResponse::BadRequest().body("Invalid family name!"));
}
family.name = name.to_string();
}
if let Some(enable_genealogy) = req.enable_genealogy {
family.enable_genealogy = enable_genealogy;
}
if let Some(disable_couple_photos) = req.disable_couple_photos {
family.disable_couple_photos = disable_couple_photos;
} }
let mut family = families_service::get_by_id(f.family_id()).await?;
family.name = req.0.name;
family.enable_genealogy = req.0.enable_genealogy;
family.disable_couple_photos = req.0.disable_couple_photos;
families_service::update_family(&family).await?; families_service::update_family(&family).await?;
log::info!("User {:?} updated family {:?}", f.user_id(), f.family_id()); log::info!("User {:?} updated family {:?}", f.user_id(), f.family_id());