Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance modify #317

Merged
merged 10 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ services:
PYTHON_PLUGIN_ENABLED: "true"
TRANSFERS_PLUGIN_ENABLED: "true"
WORKLISTS_PLUGIN_ENABLED: "true"
ORTHANC__OVERWRITE_INSTANCES: "true"
ORTHANC__DICOM_WEB__ENABLE: "true"
ORTHANC__DICOM_WEB__ROOT: "/dicom-web/"
ORTHANC__DICOM_WEB__ENABLEWADO: "true"
ORTHANC__DICOM_WEB__WADOROOT: "/wado"
ORTHANC__DICOM_WEB__SSL: "true"
ORTHANC__DICOM_WEB__STUDIES_METADATA: "MainDicomTags"
ORTHANC__DICOM_WEB__SERIES_METADATA: "Full"


volumes:
orthanc-flow:
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"axios": "^1.7.3",
"i18next": "^23.12.2",
"jwt-decode": "^4.0.0",
"mime-types": "^2.1.35",
"moment": "^2.30.1",
"native-file-system-adapter": "^3.0.1",
"preline": "^2.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down Expand Up @@ -53,6 +55,7 @@
"@storybook/theming": "^8.2.8",
"@tanstack/eslint-plugin-query": "^5.51.15",
"@types/jwt-decode": "^3.1.0",
"@types/mime-types": "^2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-query": "^1.2.9",
Expand Down
1 change: 1 addition & 0 deletions src/content/ContentRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const ContentRoot: React.FC = () => {
return (
<div className="flex flex-col items-center w-full">
<EditPatient
key={editingPatient?.id ?? undefined}
patient={editingPatient as Patient}
onEditPatient={handlePatientUpdate}
onClose={closeEditModal}
Expand Down
13 changes: 11 additions & 2 deletions src/content/patients/AccordionPatient.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { useState } from "react";

import { Accordion, DeleteButton, EditButton } from "../../ui";
import { Accordion, DeleteButton, DownloadButton, EditButton } from "../../ui";

import Patient from "../../model/Patient";


import StudyRoot from "../studies/StudyRoot";
import SeriesRoot from "../series/SeriesRoot";
import { AccordionHeader } from "../../ui/Accordion";
import { exportRessource } from "../../services/export";
import { useCustomToast } from "../../utils";

type AccordionPatientProps = {
patient: Patient;
Expand All @@ -17,6 +18,7 @@ type AccordionPatientProps = {
};

const AccordionPatient: React.FC<AccordionPatientProps> = ({ patient, onEditPatient, onDeletePatient, onStudyUpdated }) => {
const {toastSuccess} = useCustomToast()
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);

const handleStudySelected = (studyId: string) => {
Expand All @@ -33,6 +35,12 @@ const AccordionPatient: React.FC<AccordionPatientProps> = ({ patient, onEditPati
onDeletePatient(patient);
}

const handleSaveClick = (event: React.MouseEvent<HTMLButtonElement|SVGElement>) => {
event.stopPropagation();
toastSuccess("Download started, follow progression in console")
exportRessource("patients", patient.id, (mb)=>{console.log(mb + "mb")})
}

return (
<>
<Accordion
Expand All @@ -44,6 +52,7 @@ const AccordionPatient: React.FC<AccordionPatientProps> = ({ patient, onEditPati
<span className="text-sm group-hover:text-white">Nb of Studies: {patient.getStudies().length}</span>
<div className="flex justify-end w-full space-x-7">
<EditButton onClick={handleEditClick} />
<DownloadButton onClick={handleSaveClick} />
<DeleteButton onClick={handleDeleteClick} />
</div>
</div>
Expand Down
32 changes: 20 additions & 12 deletions src/content/patients/EditPatient.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from "react";
import React, { useState } from "react";
import Patient from "../../model/Patient";
import { modifyPatient } from "../../services";
import { useCustomMutation, useCustomToast } from "../../utils";
import { PatientPayload, OrthancResponse } from "../../utils/types";
import { PatientModifyPayload, OrthancResponse } from "../../utils/types";
import PatientEditForm from './PatientEditForm';
import { Modal } from "../../ui";

Expand All @@ -14,32 +14,40 @@ type EditPatientProps = {
}

const EditPatient: React.FC<EditPatientProps> = ({ patient, onEditPatient, onClose, show }) => {
const { toastSuccess, toastError } = useCustomToast();
const { toastError } = useCustomToast();
const [jobId, setJobId] = useState<string | null>(null);

const { mutateAsync: mutatePatient } = useCustomMutation<OrthancResponse, { id: string, payload: PatientPayload }>(
const { mutateAsync: mutatePatient } = useCustomMutation<OrthancResponse, { id: string, payload: PatientModifyPayload }>(
({ id, payload }) => modifyPatient(id, payload),
[['jobs']],
{
onSuccess: async () => {
toastSuccess(`Patient updated successfully`);
onEditPatient(patient);
onClose();
onSuccess: async (data) => {
setJobId(data.id);
},
onError: (error: any) => {
toastError(`Failed to update patient: ${error}`);
onError: () => {
toastError(`Failed to update patient`);
}
}
);

const handleSubmit = ({ id, payload }: { id: string; payload: PatientPayload }) => {
const handleSubmit = ({ id, payload }: { id: string; payload: PatientModifyPayload }) => {
mutatePatient({ id, payload });
};

const handleJobCompletion = (job: string) => {
if (job === "Success") {
onEditPatient(patient);
onClose();
} else if (job === "Failure") {
toastError(`Failed to update Study `);
}
};

return (
<Modal show={show} size='xl'>
<Modal.Header onClose={onClose}> Edit patient </Modal.Header>
<Modal.Body>
<PatientEditForm patient={patient} onSubmit={handleSubmit} onCancel={onClose} />
<PatientEditForm patient={patient} jobId={jobId} onSubmit={handleSubmit} onJobCompleted={handleJobCompletion} />
</Modal.Body>
</Modal>
);
Expand Down
107 changes: 63 additions & 44 deletions src/content/patients/PatientEditForm.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,62 @@
import React, { ChangeEvent, useState, useEffect } from "react";
import { Button, Input, Spinner } from "../../ui";
import Patient from "../../model/Patient";
import { PatientMainDicomTags, PatientPayload } from "../../utils/types";
import { PatientMainDicomTags, PatientModifyPayload } from "../../utils/types";
import CheckBox from "../../ui/Checkbox";
import { Colors } from "../../utils";
import InputWithDelete from "../../ui/InputWithDelete";
import ProgressJobs from "../../query/ProgressJobs";

type PatientEditFormProps = {
jobId: string | null;
patient: Patient;
onSubmit: (data: { id: string; payload: PatientPayload }) => void;
onCancel: () => void;
onSubmit: (data: { id: string; payload: PatientModifyPayload }) => void;
onJobCompleted: (jobStatus :string) => void;
};

const PatientEditForm = ({ patient, onSubmit, onCancel }: PatientEditFormProps) => {
const [patientId, setPatientId] = useState<string>(patient?.patientId ?? "");
const [patientName, setPatientName] = useState<string | null>(patient?.patientName ?? null);
const [patientBirthDate, setPatientBirthDate] = useState<string | null>(patient?.patientBirthDate ?? null);
const [patientSex, setPatientSex] = useState<string | null>(patient?.patientSex ?? null);
const [removePrivateTags, setRemovePrivateTags] = useState<boolean>(false);
const [keepSource, setKeepSource] = useState<boolean>(false);
const [fieldsToRemove, setFieldsToRemove] = useState<string[]>([]);

if (!patient) return <Spinner/>;

const handleFieldRemoval = (field: string, checked: boolean) => {
setFieldsToRemove((prev) =>
checked ? [...prev, field] : prev.filter((item) => item !== field)
);
};

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const replace: Partial<PatientMainDicomTags> = {};

if (patientName !== patient.patientName) replace.patientName = patientName;
if (patientBirthDate !== patient.patientBirthDate) replace.patientBirthDate = patientBirthDate;
if (patientSex !== patient.patientSex) replace.patientSex = patientSex;

const payload: PatientPayload = {
replace,
remove: fieldsToRemove,
removePrivateTags,
keepSource,
force: true,
synchronous: false,
};
onSubmit({ id: patientId, payload });
const PatientEditForm = ({ patient, jobId, onSubmit, onJobCompleted }: PatientEditFormProps) => {
const [patientId, setPatientId] = useState<string>(patient?.patientId ?? "");
const [patientName, setPatientName] = useState<string | null>(patient?.patientName ?? null);
const [patientBirthDate, setPatientBirthDate] = useState<string | null>(patient?.patientBirthDate ?? null);
const [patientSex, setPatientSex] = useState<string | null>(patient?.patientSex ?? null);
const [removePrivateTags, setRemovePrivateTags] = useState<boolean>(false);
const [keepSource, setKeepSource] = useState<boolean>(false);
const [fieldsToRemove, setFieldsToRemove] = useState<string[]>([]);
const [keepUIDs, setKeepUIDs] = useState(false)

if (!patient) return <Spinner />;

useEffect(() => {
if (keepUIDs) setKeepSource(true)
}, [keepUIDs])

const handleFieldRemoval = (field: string, checked: boolean) => {
setFieldsToRemove((prev) =>
checked ? [...prev, field] : prev.filter((item) => item !== field)
);
};

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const replace: Partial<PatientMainDicomTags> = {};

if (patientName !== patient.patientName) replace.patientName = patientName;
if (patientId !== patient.patientId) replace.patientId = patientId;
if (patientBirthDate !== patient.patientBirthDate) replace.patientBirthDate = patientBirthDate;
if (patientSex !== patient.patientSex) replace.patientSex = patientSex;

const payload: PatientModifyPayload = {
replace,
remove: fieldsToRemove,
removePrivateTags,
keepSource,
force: true,
synchronous: false,
keep: keepUIDs ? ['StudyInstanceUID', 'SeriesInstanceUID', 'SOPInstanceUID'] : [],
};

onSubmit({ id: patient.id, payload });
};


return (
<form onSubmit={handleSubmit} className="mt-5 space-y-8">
Expand Down Expand Up @@ -91,9 +100,9 @@ type PatientEditFormProps = {
placeholder="Enter patient sex"
/>
</div>
<div className="grid justify-center grid-cols-1 lg:grid-cols-2">
<div className="flex justify-around">
<CheckBox
label="Removing private tags"
label="Remove private tags"
checked={removePrivateTags}
onChange={(event: ChangeEvent<HTMLInputElement>) => setRemovePrivateTags(event.target.checked)}
bordered={false}
Expand All @@ -104,14 +113,24 @@ type PatientEditFormProps = {
onChange={(event: ChangeEvent<HTMLInputElement>) => setKeepSource(event.target.checked)}
bordered={false}
/>
<CheckBox
label="Keep UIDs"
checked={keepUIDs}
onChange={(e: ChangeEvent<HTMLInputElement>) => setKeepUIDs(e.target.checked)}
bordered={false}
/>
</div>
<div className="flex justify-center mt-4 space-x-4">
<Button color={Colors.secondary} type="button" onClick={onCancel}>
Cancel
</Button>
<div className="flex justify-center">
<Button type="submit" color={Colors.success}>
Save Changes
</Button>
{jobId &&
(
<div className="flex flex-col items-center justify-center">
<ProgressJobs jobId={jobId} onJobCompleted={onJobCompleted} />
</div>
)
}
</div>
</form>
);
Expand Down
7 changes: 4 additions & 3 deletions src/content/series/EditSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@

import React, { useState } from "react";
import { modifySeries } from "../../services/orthanc";
import { useCustomMutation, useCustomToast, Series, SeriesPayload } from "../../utils";
import { useCustomMutation, useCustomToast, Series } from "../../utils";
import SeriesEditForm from './SeriesEditForm';
import { Modal } from "../../ui";
import { SeriesModifyPayload } from "../../utils/types";

type EditSeriesProps = {
series: Series;
Expand All @@ -21,7 +22,7 @@ const EditSeries: React.FC<EditSeriesProps> = ({ series, onEditSeries, onClose,
const [jobId, setJobId] = useState<string | null>(null);


const { mutateAsync: mutateSeries } = useCustomMutation<any, { id: string, payload: SeriesPayload }>(
const { mutateAsync: mutateSeries } = useCustomMutation<any, { id: string, payload: SeriesModifyPayload }>(
({ id, payload }) => modifySeries(id, payload),
[['series'], ['jobs']],
{
Expand All @@ -34,7 +35,7 @@ const EditSeries: React.FC<EditSeriesProps> = ({ series, onEditSeries, onClose,
}
);

const handleSubmit = ({ id, payload }: { id: string; payload: SeriesPayload }) => {
const handleSubmit = ({ id, payload }: { id: string; payload: SeriesModifyPayload }) => {
mutateSeries({ id, payload });
};

Expand Down
1 change: 0 additions & 1 deletion src/content/series/PreviewSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ const PreviewSeries: React.FC<PreviewSeriesProps> = ({ seriesId}) => {
if (!instanceUIDs) return null
const start = Math.max(imageIndex, 0)
const end = Math.min(start + (pageSize - 1), instanceUIDs.length - 1)
console.log(start, end)
const selectedUIDs = []
for (let i = start; i <= end; i++) {
selectedUIDs.push(instanceUIDs[i])
Expand Down
21 changes: 17 additions & 4 deletions src/content/series/SeriesActions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SeriesActions.tsx
import React from 'react';
import { FaEdit, FaEye, FaTrash } from "react-icons/fa";
import { RiDownload2Line as DownloadIcon } from "react-icons/ri";
import { Series } from "../../utils/types";
import DropdownButton from '../../ui/menu/DropDownButton';

Expand All @@ -18,17 +19,29 @@ const SeriesActions: React.FC<SeriesActionsProps> = ({ series, onActionClick })
action: () => onActionClick('edit', series)
},
{
label: 'Delete',
icon: <FaTrash />,
color: 'red',
action: () => onActionClick('delete', series)
label: 'Metadata',
icon: <FaEye />,
color: 'green',
action: () => onActionClick('metadata', series),
},
{
label: 'Preview Series',
icon: <FaEye />,
color: 'green',
action: () => onActionClick('preview', series),
},
{
label: 'Download',
icon: <DownloadIcon />,
color: 'green',
action: () => onActionClick('download', series)
},
{
label: 'Delete',
icon: <FaTrash />,
color: 'red',
action: () => onActionClick('delete', series)
},
];

const handleClick = (e: React.MouseEvent) => {
Expand Down
Loading