diff --git a/docker-compose.yml b/docker-compose.yml index b8c830cb..c486fb20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -63,6 +63,7 @@ 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" @@ -70,6 +71,7 @@ services: ORTHANC__DICOM_WEB__SSL: "true" ORTHANC__DICOM_WEB__STUDIES_METADATA: "MainDicomTags" ORTHANC__DICOM_WEB__SERIES_METADATA: "Full" + volumes: orthanc-flow: diff --git a/package.json b/package.json index 6ad9f12b..499b6de7 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/content/ContentRoot.tsx b/src/content/ContentRoot.tsx index 883ab6be..d7c5a66f 100644 --- a/src/content/ContentRoot.tsx +++ b/src/content/ContentRoot.tsx @@ -95,6 +95,7 @@ const ContentRoot: React.FC = () => { return (
= ({ patient, onEditPatient, onDeletePatient, onStudyUpdated }) => { + const {toastSuccess} = useCustomToast() const [selectedStudyId, setSelectedStudyId] = useState(null); const handleStudySelected = (studyId: string) => { @@ -33,6 +35,12 @@ const AccordionPatient: React.FC = ({ patient, onEditPati onDeletePatient(patient); } + const handleSaveClick = (event: React.MouseEvent) => { + event.stopPropagation(); + toastSuccess("Download started, follow progression in console") + exportRessource("patients", patient.id, (mb)=>{console.log(mb + "mb")}) + } + return ( <> = ({ patient, onEditPati Nb of Studies: {patient.getStudies().length}
+
diff --git a/src/content/patients/EditPatient.tsx b/src/content/patients/EditPatient.tsx index c8b6fa5f..7b47e0c8 100644 --- a/src/content/patients/EditPatient.tsx +++ b/src/content/patients/EditPatient.tsx @@ -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"; @@ -14,32 +14,40 @@ type EditPatientProps = { } const EditPatient: React.FC = ({ patient, onEditPatient, onClose, show }) => { - const { toastSuccess, toastError } = useCustomToast(); + const { toastError } = useCustomToast(); + const [jobId, setJobId] = useState(null); - const { mutateAsync: mutatePatient } = useCustomMutation( + const { mutateAsync: mutatePatient } = useCustomMutation( ({ 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 ( Edit patient - + ); diff --git a/src/content/patients/PatientEditForm.tsx b/src/content/patients/PatientEditForm.tsx index 8471a9f3..67c540fc 100644 --- a/src/content/patients/PatientEditForm.tsx +++ b/src/content/patients/PatientEditForm.tsx @@ -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(patient?.patientId ?? ""); - const [patientName, setPatientName] = useState(patient?.patientName ?? null); - const [patientBirthDate, setPatientBirthDate] = useState(patient?.patientBirthDate ?? null); - const [patientSex, setPatientSex] = useState(patient?.patientSex ?? null); - const [removePrivateTags, setRemovePrivateTags] = useState(false); - const [keepSource, setKeepSource] = useState(false); - const [fieldsToRemove, setFieldsToRemove] = useState([]); - - if (!patient) return ; - - const handleFieldRemoval = (field: string, checked: boolean) => { - setFieldsToRemove((prev) => - checked ? [...prev, field] : prev.filter((item) => item !== field) - ); - }; - - const handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - const replace: Partial = {}; - - 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(patient?.patientId ?? ""); + const [patientName, setPatientName] = useState(patient?.patientName ?? null); + const [patientBirthDate, setPatientBirthDate] = useState(patient?.patientBirthDate ?? null); + const [patientSex, setPatientSex] = useState(patient?.patientSex ?? null); + const [removePrivateTags, setRemovePrivateTags] = useState(false); + const [keepSource, setKeepSource] = useState(false); + const [fieldsToRemove, setFieldsToRemove] = useState([]); + const [keepUIDs, setKeepUIDs] = useState(false) + + if (!patient) return ; + + 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) => { + event.preventDefault(); + const replace: Partial = {}; + + 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 (
@@ -91,9 +100,9 @@ type PatientEditFormProps = { placeholder="Enter patient sex" /> -
+
) => setRemovePrivateTags(event.target.checked)} bordered={false} @@ -104,14 +113,24 @@ type PatientEditFormProps = { onChange={(event: ChangeEvent) => setKeepSource(event.target.checked)} bordered={false} /> + ) => setKeepUIDs(e.target.checked)} + bordered={false} + />
-
- +
+ {jobId && + ( +
+ +
+ ) + }
); diff --git a/src/content/series/EditSeries.tsx b/src/content/series/EditSeries.tsx index a42ef190..5b972962 100644 --- a/src/content/series/EditSeries.tsx +++ b/src/content/series/EditSeries.tsx @@ -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; @@ -21,7 +22,7 @@ const EditSeries: React.FC = ({ series, onEditSeries, onClose, const [jobId, setJobId] = useState(null); - const { mutateAsync: mutateSeries } = useCustomMutation( + const { mutateAsync: mutateSeries } = useCustomMutation( ({ id, payload }) => modifySeries(id, payload), [['series'], ['jobs']], { @@ -34,7 +35,7 @@ const EditSeries: React.FC = ({ series, onEditSeries, onClose, } ); - const handleSubmit = ({ id, payload }: { id: string; payload: SeriesPayload }) => { + const handleSubmit = ({ id, payload }: { id: string; payload: SeriesModifyPayload }) => { mutateSeries({ id, payload }); }; diff --git a/src/content/series/PreviewSeries.tsx b/src/content/series/PreviewSeries.tsx index af4a84d4..614eb046 100644 --- a/src/content/series/PreviewSeries.tsx +++ b/src/content/series/PreviewSeries.tsx @@ -40,7 +40,6 @@ const PreviewSeries: React.FC = ({ 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]) diff --git a/src/content/series/SeriesActions.tsx b/src/content/series/SeriesActions.tsx index 2b38139a..e2bdf167 100644 --- a/src/content/series/SeriesActions.tsx +++ b/src/content/series/SeriesActions.tsx @@ -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'; @@ -18,10 +19,10 @@ const SeriesActions: React.FC = ({ series, onActionClick }) action: () => onActionClick('edit', series) }, { - label: 'Delete', - icon: , - color: 'red', - action: () => onActionClick('delete', series) + label: 'Metadata', + icon: , + color: 'green', + action: () => onActionClick('metadata', series), }, { label: 'Preview Series', @@ -29,6 +30,18 @@ const SeriesActions: React.FC = ({ series, onActionClick }) color: 'green', action: () => onActionClick('preview', series), }, + { + label: 'Download', + icon: , + color: 'green', + action: () => onActionClick('download', series) + }, + { + label: 'Delete', + icon: , + color: 'red', + action: () => onActionClick('delete', series) + }, ]; const handleClick = (e: React.MouseEvent) => { diff --git a/src/content/series/SeriesEditForm.tsx b/src/content/series/SeriesEditForm.tsx index b5e45f85..8b6c2a5b 100644 --- a/src/content/series/SeriesEditForm.tsx +++ b/src/content/series/SeriesEditForm.tsx @@ -1,5 +1,5 @@ -import React, { ChangeEvent, useState } from "react"; -import { Series, SeriesPayload, SeriesMainDicomTags } from '../../utils/types'; +import React, { ChangeEvent, useEffect, useState } from "react"; +import { Series, SeriesModifyPayload, SeriesMainDicomTags } from '../../utils/types'; import { InputWithDelete, CheckBox, Button } from "../../ui"; import ProgressJobs from "../../query/ProgressJobs"; @@ -7,7 +7,7 @@ import { Colors } from "../../utils"; type SeriesEditFormProps = { data: Series; - onSubmit: (data: { id: string; payload: SeriesPayload }) => void; + onSubmit: (data: { id: string; payload: SeriesModifyPayload }) => void; jobId?: string; onJobCompleted?: (jobState: string) => void; } @@ -21,6 +21,11 @@ const SeriesEditForm = ({ data, onSubmit, jobId, onJobCompleted }: SeriesEditFor const [removePrivateTags, setRemovePrivateTags] = useState(false); const [keepSource, setKeepSource] = useState(false); const [fieldsToRemove, setFieldsToRemove] = useState([]); + const [keepUIDs, setKeepUIDs] = useState(false) + + useEffect(() => { + if (keepUIDs) setKeepSource(true) + }, [keepUIDs]) const handleFieldRemoval = (field: string, checked: boolean) => { setFieldsToRemove((prev) => @@ -40,13 +45,14 @@ const SeriesEditForm = ({ data, onSubmit, jobId, onJobCompleted }: SeriesEditFor if (seriesDate !== data.mainDicomTags.seriesDate) replace.seriesDate = seriesDate; if (seriesTime !== data.mainDicomTags.seriesTime) replace.seriesTime = seriesTime; - const payload: SeriesPayload = { + const payload: SeriesModifyPayload = { replace, remove: fieldsToRemove, removePrivateTags, keepSource, force: true, synchronous: false, + keep: keepUIDs ? ['SeriesInstanceUID', 'SOPInstanceUID'] : [], }; onSubmit({ id: data.id, payload }); @@ -107,7 +113,7 @@ const SeriesEditForm = ({ data, onSubmit, jobId, onJobCompleted }: SeriesEditFor fieldsToRemove={fieldsToRemove} />
-
+
) => setKeepSource(e.target.checked)} bordered={false} /> + ) => setKeepUIDs(e.target.checked)} + bordered={false} + />
-
+
+ { + jobId && ( +
+ +
+ ) + }
- { - jobId && ( -
- -
- ) - } + ); }; diff --git a/src/content/series/SeriesRoot.tsx b/src/content/series/SeriesRoot.tsx index ca16e370..b34881b7 100644 --- a/src/content/series/SeriesRoot.tsx +++ b/src/content/series/SeriesRoot.tsx @@ -12,6 +12,8 @@ import PreviewSeries from './PreviewSeries'; import { useConfirm } from '../../services/ConfirmContextProvider'; import { useCustomToast } from '../../utils/toastify'; import { Modal, Spinner } from '../../ui'; +import Tags from './Tags'; +import { exportRessource } from '../../services/export'; interface SeriesRootProps { studyId: string; @@ -20,6 +22,7 @@ interface SeriesRootProps { const SeriesRoot: React.FC = ({ studyId }) => { const [editingSeries, setEditingSeries] = useState(null); const [previewSeries, setPreviewSeries] = useState(null); + const [previewMetadata, setPreviewMetadata] = useState(null); const { confirm } = useConfirm(); const { toastSuccess, toastError } = useCustomToast(); @@ -55,6 +58,15 @@ const SeriesRoot: React.FC = ({ studyId }) => { setPreviewSeries(series); } + const handleDownloadSeries = (series: Series) => { + toastSuccess("Download started, follow progression in console") + exportRessource('series', series.id, (mb)=>{console.log(mb+ "mb")}) + } + + const handleMetadataPreview = (series: Series) => { + setPreviewMetadata(series) + } + const handleDeleteSeries = async (seriesId: string) => { const confirmContent = (
@@ -79,6 +91,12 @@ const SeriesRoot: React.FC = ({ studyId }) => { case 'preview': handlePreviewSeries(series); break; + case 'metadata': + setPreviewMetadata(series); + break; + case 'download': + handleDownloadSeries(series); + break; default: console.log(`Unhandled action: ${action}`); } @@ -119,6 +137,17 @@ const SeriesRoot: React.FC = ({ studyId }) => { )} + + {previewMetadata && ( + + setPreviewMetadata(null)} > + Preview Metadata + + + + + + )}
); }; diff --git a/src/content/series/Tags.tsx b/src/content/series/Tags.tsx new file mode 100644 index 00000000..bbe981e6 --- /dev/null +++ b/src/content/series/Tags.tsx @@ -0,0 +1,45 @@ +import { useState } from "react" +import { getInstancesOfSeries } from "../../services/orthanc" +import { Input, Spinner } from "../../ui" +import { useCustomQuery } from "../../utils" +import { instanceHeader, instanceTags } from "../../services/instances" + +type TagsProps = { + seriesId: string +} +const Tags = ({ seriesId }: TagsProps) => { + + const { data: instances } = useCustomQuery(['series', seriesId, 'instances'], () => getInstancesOfSeries(seriesId)) + const [instanceNumber, setInstanceNumber] = useState(1) + + const currentInstanceId = (instanceNumber != null && instances != null) ? instances[instanceNumber - 1].id : null + + const { data: header } = useCustomQuery( + ['instances', currentInstanceId, 'metadata'], + () => instanceHeader(currentInstanceId), + { + enabled: (currentInstanceId !== null) + } + ) + + const { data: tags } = useCustomQuery( + ['instances', currentInstanceId, 'tags'], + () => instanceTags(currentInstanceId), + { + enabled: (currentInstanceId !== null) + } + ) + + if (!instances) return + return ( + <> + setInstanceNumber(Number(event.target?.value))} /> +
+                {JSON.stringify(header, null, 2)}
+                {JSON.stringify(tags, null, 2)}
+            
+ + ) +} + +export default Tags \ No newline at end of file diff --git a/src/content/studies/EditStudy.tsx b/src/content/studies/EditStudy.tsx index 4d72d1a0..136756e1 100644 --- a/src/content/studies/EditStudy.tsx +++ b/src/content/studies/EditStudy.tsx @@ -3,7 +3,7 @@ */ import React, { useState } from "react"; -import { Study, StudyPayload } from "../../utils/types"; +import { Study, StudyModifyPayload } from "../../utils/types"; import { getStudy, modifyStudy } from "../../services/orthanc"; import { useCustomMutation, useCustomQuery, useCustomToast } from "../../utils"; import StudyEditForm from './StudyEditForm'; @@ -20,17 +20,15 @@ const EditStudy: React.FC = ({ studyId, onStudyUpdated, onClose, const { toastSuccess, toastError } = useCustomToast(); const [jobId, setJobId] = useState(null); - console.log("EditStudyProps", studyId, onStudyUpdated, onClose, show); - const { mutateAsync: mutateStudy } = useCustomMutation( + const { mutateAsync: mutateStudy } = useCustomMutation( ({ id, payload }) => modifyStudy(id, payload), [['studies'], ['jobs']], { onSuccess: (data) => { - // toastSuccess(`Study ${data.id} updated successfully`); setJobId(data.id); - // onClose(); }, onError: (error) => { + console.log(error) toastError("Failed to update study: " + error ); }, } @@ -46,7 +44,7 @@ const EditStudy: React.FC = ({ studyId, onStudyUpdated, onClose, } ); - const handleSubmit = ({ id, payload }: { id: string; payload: StudyPayload }) => { + const handleSubmit = ({ id, payload }: { id: string; payload: StudyModifyPayload }) => { mutateStudy({ id, payload }); }; @@ -71,7 +69,7 @@ const EditStudy: React.FC = ({ studyId, onStudyUpdated, onClose, {editingStudyDetails && ( = ({ study, onActionClick }) => color: 'green', action: () => onActionClick('preview', study.id) }, + { + label: 'Download', + icon: , + color: 'green', + action: () => onActionClick('download', study.id) + }, { label: 'Delete', icon: , color: 'red', action: () => onActionClick('delete', study.id) }, - ]; const handleClick = (e: React.MouseEvent) => { diff --git a/src/content/studies/StudyEditForm.tsx b/src/content/studies/StudyEditForm.tsx index 6a58ddd1..c2200ec9 100644 --- a/src/content/studies/StudyEditForm.tsx +++ b/src/content/studies/StudyEditForm.tsx @@ -1,27 +1,35 @@ -import React, { ChangeEvent, useState } from "react"; -import { StudyPayload, StudyMainDicomTags } from '../../utils/types'; -import { CheckBox, InputWithDelete } from "../../ui"; +import React, { ChangeEvent, useEffect, useState } from "react"; +import { StudyModifyPayload, StudyMainDicomTags, Study, PatientMainDicomTags } from '../../utils/types'; +import { Button, CheckBox, Input, InputWithDelete } from "../../ui"; import ProgressJobs from "../../query/ProgressJobs"; +import { Colors } from "../../utils"; type StudyEditFormProps = { - data: StudyMainDicomTags & { id: string }; - onSubmit: (params: { id: string; payload: StudyPayload }) => void; + data: Study; + onSubmit: (params: { id: string; payload: StudyModifyPayload }) => void; jobId?: string; onJobCompleted?: (jobState: string) => void; }; const StudyEditForm = ({ data, onSubmit, jobId, onJobCompleted }: StudyEditFormProps) => { - const [accessionNumber, setAccessionNumber] = useState(data?.accessionNumber ?? null); - const [studyDate, setStudyDate] = useState(data?.studyDate ?? null); - const [studyDescription, setStudyDescription] = useState(data?.studyDescription ?? null); - const [studyTime, setStudyTime] = useState(data?.studyTime ?? null); - const [studyId, setStudyId] = useState(data?.studyId ?? null); + const [patientName, setPatientName] = useState(data?.patientMainDicomTags?.patientName ?? null); + const [patientId, setPatientId] = useState(data?.patientMainDicomTags?.patientId ?? null); + const [patientBirthDate, setPatientBirthDate] = useState(data?.patientMainDicomTags?.patientBirthDate ?? null); + const [patientSex, setPatientSex] = useState(data?.patientMainDicomTags?.patientSex ?? null); + + const [accessionNumber, setAccessionNumber] = useState(data?.mainDicomTags?.accessionNumber ?? null); + const [studyDate, setStudyDate] = useState(data?.mainDicomTags?.studyDate ?? null); + const [studyDescription, setStudyDescription] = useState(data?.mainDicomTags?.studyDescription ?? null); + const [studyTime, setStudyTime] = useState(data?.mainDicomTags?.studyTime ?? null); + const [studyId, setStudyId] = useState(data?.mainDicomTags?.studyId ?? null); const [removePrivateTags, setRemovePrivateTags] = useState(false); const [keepSource, setKeepSource] = useState(false); const [fieldsToRemove, setFieldsToRemove] = useState([]); + const [keepUIDs, setKeepUIDs] = useState(false) + const handleFieldRemoval = (field: string, checked: boolean) => { setFieldsToRemove((prev) => checked ? [...prev, field] : prev.filter((item) => item !== field) @@ -29,28 +37,63 @@ const StudyEditForm = ({ data, onSubmit, jobId, onJobCompleted }: StudyEditFormP }; const handleSubmit = (event: React.FormEvent | React.MouseEvent) => { event.preventDefault(); - const replace: Partial = {}; + const replace: Partial = {}; - if (accessionNumber !== data.accessionNumber) replace.accessionNumber = accessionNumber; - if (studyDate !== data.studyDate) replace.studyDate = studyDate; - if (studyDescription !== data.studyDescription) replace.studyDescription = studyDescription; - if (studyId !== data.studyId) replace.studyId = studyId; - if (studyTime !== data.studyTime) replace.studyTime = studyTime; + if (patientId !== data?.patientMainDicomTags?.patientId) replace.patientId = patientId; + if (patientName !== data?.patientMainDicomTags?.patientName) replace.patientName = patientName; + if (replace.patientId || replace.patientName) { + replace.patientBirthDate = patientBirthDate; + replace.patientSex = patientSex; + } - const payload: StudyPayload = { + if (accessionNumber !== data?.mainDicomTags?.accessionNumber) replace.accessionNumber = accessionNumber; + if (studyDate !== data?.mainDicomTags?.studyDate) replace.studyDate = studyDate; + if (studyDescription !== data?.mainDicomTags?.studyDescription) replace.studyDescription = studyDescription; + if (studyId !== data?.mainDicomTags?.studyId) replace.studyId = studyId; + if (studyTime !== data?.mainDicomTags?.studyTime) replace.studyTime = studyTime; + + const payload: StudyModifyPayload = { replace, remove: fieldsToRemove, removePrivateTags, + keep: keepUIDs ? ['StudyInstanceUID', 'SeriesInstanceUID', 'SOPInstanceUID'] : [], keepSource, force: true, synchronous: false, }; + onSubmit({ id: data.id, payload }); }; return (
+
+ ) => setPatientName(e.target.value)} + fieldName="patientName" + /> + ) => setPatientId(e.target.value)} + fieldName="patientID" + /> + ) => setPatientBirthDate(e.target.value)} + fieldName="patientBirthdate" + /> + ) => setPatientSex(e.target.value)} + fieldName="patientSex" + /> +
-
+
) => setRemovePrivateTags(e.target.checked)} bordered={false} @@ -110,6 +153,15 @@ const StudyEditForm = ({ data, onSubmit, jobId, onJobCompleted }: StudyEditFormP onChange={(e: ChangeEvent) => setKeepSource(e.target.checked)} bordered={false} /> + ) => setKeepUIDs(e.target.checked)} + bordered={false} + /> +
+
+
{jobId && ( diff --git a/src/content/studies/StudyRoot.tsx b/src/content/studies/StudyRoot.tsx index 355c969f..b6ab43ec 100644 --- a/src/content/studies/StudyRoot.tsx +++ b/src/content/studies/StudyRoot.tsx @@ -8,6 +8,7 @@ import { useConfirm } from '../../services/ConfirmContextProvider'; import { useCustomToast } from '../../utils/toastify'; import Patient from '../../model/Patient'; import AiStudy from './AiStudy'; +import { exportRessource } from '../../services/export'; type StudyRootProps = { patient: Patient; @@ -62,10 +63,15 @@ const StudyRoot: React.FC = ({ patient, onStudyUpdated, onStudyS setPreviewStudyId(studyId); } - const handleAIStudy=(studyId : string) => { + const handleAIStudy = (studyId: string) => { setAIStudyId(studyId) } + const handleDownloadStudy = (studyId: string) => { + toastSuccess("Download started, follow progression in console") + exportRessource('studies', studyId, (mb)=>{console.log(mb+ "mb")}) + } + const handleStudyAction = (action: string, studyId: string) => { switch (action) { case 'edit': @@ -80,6 +86,9 @@ const StudyRoot: React.FC = ({ patient, onStudyUpdated, onStudyS case 'ai': handleAIStudy(studyId); break; + case 'download': + handleDownloadStudy(studyId); + break; default: break; } diff --git a/src/services/export.ts b/src/services/export.ts new file mode 100644 index 00000000..75f36a51 --- /dev/null +++ b/src/services/export.ts @@ -0,0 +1,90 @@ +import { getToken } from "./axios"; +import { showSaveFilePicker } from "native-file-system-adapter"; +import mime from "mime-types"; + +const getContentType = (headers: any) => { + const contentType = headers.get("Content-Type"); + const parts = contentType?.split(","); + return parts?.[0]; +}; + +const getContentDispositionFilename = (headers: any) => { + const contentDisposition = headers.get("Content-Disposition"); + const parts = contentDisposition?.split(";"); + if (!parts) return null; + let filename = parts[1].split("=")[1]; + return filename; +}; + +export const exportFileThroughFilesystemAPI = async ( + readableStream: ReadableStream | null, + mimeType: string, + suggestedName: string | null, + onProgress?: (mb: number) => void +) => { + if (!readableStream) return; + let acceptedTypes = []; + + let extension = mime.extension(mimeType); + acceptedTypes.push({ accept: { [mimeType]: ["." + extension] } }); + + const fileHandle = await showSaveFilePicker({ + _preferPolyfill: true, + suggestedName: suggestedName ?? "download." + extension, + types: acceptedTypes, + excludeAcceptAllOption: false, // default + }).catch((err: any) => console.log(err)); + + let writableStream = await ( + fileHandle as FileSystemFileHandle + ).createWritable(); + + let loaded = 0; + let progress = new TransformStream({ + transform(chunk, controller) { + loaded += chunk.length; + let progressMb = Math.round(loaded / 1000000); + if (progressMb > 1) { + if (onProgress) onProgress(progressMb); + } + controller.enqueue(chunk); + }, + }); + + await readableStream.pipeThrough(progress).pipeTo(writableStream); +}; + +export const exportRessource = ( + level: "studies" | "patients" | "series", + studyId: string, + onProgress = (_mb: number) => {}, + abortController = new AbortController(), + transferSyntax: string | undefined = undefined, +): Promise => { + const body = { + Asynchronous: false, + Transcode: transferSyntax, + }; + + return fetch("/api/" + level + "/" + studyId + "/archive", { + method: "POST", + headers: { + Authorization: "Bearer " + getToken(), + "Content-Type": "application/json", + Accept: "application/zip", + }, + body: JSON.stringify(body), + signal: abortController.signal + }) + .then((answer) => { + if (!answer.ok) throw answer; + const readableStream = answer.body; + let contentType = getContentType(answer.headers); + let filename = getContentDispositionFilename(answer.headers); + exportFileThroughFilesystemAPI(readableStream, contentType, filename, onProgress); + return true; + }) + .catch((error) => { + throw error; + }); +}; diff --git a/src/services/instances.ts b/src/services/instances.ts index 9194bee8..6be106ee 100644 --- a/src/services/instances.ts +++ b/src/services/instances.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios from './axios'; import { OrthancImportDicom } from "../utils/types"; export const sendDicom = (payload: Uint8Array): Promise => { @@ -37,9 +37,29 @@ export const createDicom = (content: string[], tags: object = {}, parentOrthancI } -export const previewInstance = (instanceUID : string) : Promise=> { +export const previewInstance = (instanceId : string) : Promise=> { - return axios.get('/api/instances/'+instanceUID+'/preview', {responseType : "blob"}) + return axios.get('/api/instances/'+instanceId+'/preview', {responseType : "blob"}) + .then((response) => { + return response.data + }).catch(error => { + console.error(error) + }) +} + +export const instanceTags = (instanceId : string) : Promise=> { + + return axios.get('/api/instances/'+instanceId+'/tags') + .then((response) => { + return response.data + }).catch(error => { + console.error(error) + }) +} + +export const instanceHeader = (instanceId : string) : Promise=> { + + return axios.get('/api/instances/'+instanceId+'/header') .then((response) => { return response.data }).catch(error => { diff --git a/src/services/orthanc.ts b/src/services/orthanc.ts index 7a205bf7..61b62371 100644 --- a/src/services/orthanc.ts +++ b/src/services/orthanc.ts @@ -1,8 +1,19 @@ import axios from "axios"; -import { Patient, Study, Series, PatientPayload, OrthancResponse, StudyPayload, SeriesPayload, Instances } from '../utils/types'; +import { + Patient, + Study, + Series, + PatientModifyPayload, + OrthancResponse, + StudyModifyPayload, + Instances, + SeriesModifyPayload, +} from "../utils/types"; export const getOrthancSystem = (): Promise => { - return axios.get("/api/system").then(response => response.data) + return axios + .get("/api/system") + .then((response) => response.data) .catch(function (error) { if (error.response) { throw error.response; @@ -12,7 +23,8 @@ export const getOrthancSystem = (): Promise => { }; export const orthancReset = (): Promise => { - return axios.post("/api/tools/reset") + return axios + .post("/api/tools/reset") .then(function () { return null; }) @@ -22,10 +34,11 @@ export const orthancReset = (): Promise => { } throw error; }); -} +}; export const orthancShutdown = (): Promise => { - return axios.post("/api/tools/shutdown") + return axios + .post("/api/tools/shutdown") .then(function (response) { return response.data; }) @@ -35,11 +48,13 @@ export const orthancShutdown = (): Promise => { } throw error; }); -} - +}; export const updateVerbosity = (level: string): Promise => { - return axios.put("/api/tools/log-level", level, { headers: { "Content-Type": "text/plain" } }) + return axios + .put("/api/tools/log-level", level, { + headers: { "Content-Type": "text/plain" }, + }) .then(function () { return null; }) @@ -49,10 +64,11 @@ export const updateVerbosity = (level: string): Promise => { } throw error; }); -} +}; export const getVerbosity = (): Promise => { - return axios.get("/api/tools/log-level") + return axios + .get("/api/tools/log-level") .then(function (response) { return response.data; }) @@ -62,12 +78,13 @@ export const getVerbosity = (): Promise => { } throw error; }); -} +}; export const getSeries = (seriesId: string): Promise => { - return axios.get("/api/series/" + seriesId + '?expand') - .then(response => { - const data = response.data + return axios + .get("/api/series/" + seriesId + "?expand") + .then((response) => { + const data = response.data; console.log("Series data:", data); return { expectedNumberOfInstances: data.ExpectedNumberOfInstances, @@ -87,12 +104,12 @@ export const getSeries = (seriesId: string): Promise => { seriesNumber: data.MainDicomTags.SeriesNumber, stationName: data.MainDicomTags.StationName, seriesDate: data.MainDicomTags.SeriesDate, - seriesTime: data.MainDicomTags.seriesTime + seriesTime: data.MainDicomTags.seriesTime, }, parentStudy: data.ParentStudy, status: data.Status, - type: data.Type - } + type: data.Type, + }; }) .catch(function (error) { if (error.response) { @@ -103,9 +120,10 @@ export const getSeries = (seriesId: string): Promise => { }; export const getStudy = (studyId: string): Promise => { - return axios.get("/api/studies/" + studyId + '?expand') + return axios + .get("/api/studies/" + studyId + "?expand") .then((response): Study => { - const data = response.data + const data = response.data; return { id: data.ID, isStable: data.IsStable, @@ -119,18 +137,18 @@ export const getStudy = (studyId: string): Promise => { studyDescription: data.MainDicomTags.StudyDescription, studyId: data.MainDicomTags.StudyID, studyInstanceUID: data.MainDicomTags.StudyInstanceUID, - studyTime: data.MainDicomTags.StudyTime + studyTime: data.MainDicomTags.StudyTime, }, patientMainDicomTags: { patientBirthDate: data.PatientMainDicomTags.PatientBirthDate, patientId: data.PatientMainDicomTags.PatientID, patientName: data.PatientMainDicomTags.PatientName, - patientSex: data.PatientMainDicomTags.PatientSex + patientSex: data.PatientMainDicomTags.PatientSex, }, parentPatient: data.ParentPatient, series: data.Series, - type: data.Type - } + type: data.Type, + }; }) .catch(function (error) { if (error.response) { @@ -141,34 +159,38 @@ export const getStudy = (studyId: string): Promise => { }; export const getSeriesOfStudy = (studyId: string): Promise => { - return axios.get(`/api/studies/${studyId}/series?expand`) + return axios + .get(`/api/studies/${studyId}/series?expand`) .then((response: any): Series[] => { - const mappedData = response.data.map((data: any): Series => ({ - expectedNumberOfInstances: data.ExpectedNumberOfInstances, - id: data.ID, - instances: data.Instances, - isStable: data.IsStable, - labels: data.Labels, - lastUpdate: data.LastUpdate, - mainDicomTags: { - imageOrientationPatient: data.MainDicomTags.ImageOrientationPatient, - manufacturer: data.MainDicomTags.Manufacturer, - modality: data.MainDicomTags.Modality, - operatorsName: data.MainDicomTags.OperatorsName, - protocolName: data.MainDicomTags.ProtocolName, - seriesDescription: data.MainDicomTags.SeriesDescription, - seriesInstanceUID: data.MainDicomTags.SeriesInstanceUID, - seriesNumber: data.MainDicomTags.SeriesNumber, - stationName: data.MainDicomTags.StationName, - seriesDate: data.MainDicomTags.SeriesDate, - seriesTime: data.MainDicomTags.SeriesTime - }, - parentStudy: data.ParentStudy, - status: data.Status, - type: data.Type - })); + const mappedData = response.data.map( + (data: any): Series => ({ + expectedNumberOfInstances: data.ExpectedNumberOfInstances, + id: data.ID, + instances: data.Instances, + isStable: data.IsStable, + labels: data.Labels, + lastUpdate: data.LastUpdate, + mainDicomTags: { + imageOrientationPatient: data.MainDicomTags.ImageOrientationPatient, + manufacturer: data.MainDicomTags.Manufacturer, + modality: data.MainDicomTags.Modality, + operatorsName: data.MainDicomTags.OperatorsName, + protocolName: data.MainDicomTags.ProtocolName, + seriesDescription: data.MainDicomTags.SeriesDescription, + seriesInstanceUID: data.MainDicomTags.SeriesInstanceUID, + seriesNumber: data.MainDicomTags.SeriesNumber, + stationName: data.MainDicomTags.StationName, + seriesDate: data.MainDicomTags.SeriesDate, + seriesTime: data.MainDicomTags.SeriesTime, + }, + parentStudy: data.ParentStudy, + status: data.Status, + type: data.Type, + }) + ); return mappedData; - }).catch((error: any) => { + }) + .catch((error: any) => { if (error.response) { console.error("Error response:", error.response); throw error.response; @@ -179,29 +201,33 @@ export const getSeriesOfStudy = (studyId: string): Promise => { }; export const getInstancesOfSeries = (seriesId: string) => { - return axios.get(`/api/series/${seriesId}/instances`) + return axios + .get(`/api/series/${seriesId}/instances`) .then((response: any): Instances[] => { - const mappedData = response.data.map((data: any) : Instances => ({ - fileSize: data.FileSize, - fileUuid: data.FileUuid, - id: data.ID, - indexInSeries: data.IndexInSeries, - labels: data.Labels, - mainDicomTags: { - acquisitionNumber: data.MainDicomTags.AcquisitionNumber, - imageComments: data.MainDicomTags.ImageComments, - imageOrientationPatient: data.MainDicomTags.ImageOrientationPatient, - imagePositionPatient: data.MainDicomTags.ImagePositionPatient, - instanceCreationDate: data.MainDicomTags.InstanceCreationDate, - instanceCreationTime: data.MainDicomTags.InstanceCreationTime, - instanceNumber: data.MainDicomTags.InstanceNumber, - sopInstanceUID: data.MainDicomTags.SopInstanceUID - }, - parentSeries: data.ParentSeries, - type: data.Type - })); + const mappedData = response.data.map( + (data: any): Instances => ({ + fileSize: data.FileSize, + fileUuid: data.FileUuid, + id: data.ID, + indexInSeries: data.IndexInSeries, + labels: data.Labels, + mainDicomTags: { + acquisitionNumber: data.MainDicomTags.AcquisitionNumber, + imageComments: data.MainDicomTags.ImageComments, + imageOrientationPatient: data.MainDicomTags.ImageOrientationPatient, + imagePositionPatient: data.MainDicomTags.ImagePositionPatient, + instanceCreationDate: data.MainDicomTags.InstanceCreationDate, + instanceCreationTime: data.MainDicomTags.InstanceCreationTime, + instanceNumber: data.MainDicomTags.InstanceNumber, + sopInstanceUID: data.MainDicomTags.SopInstanceUID, + }, + parentSeries: data.ParentSeries, + type: data.Type, + }) + ); return mappedData; - }).catch((error: any) => { + }) + .catch((error: any) => { if (error.response) { console.error("Error response:", error.response); throw error.response; @@ -209,30 +235,36 @@ export const getInstancesOfSeries = (seriesId: string) => { console.error("Error:", error); throw error; }); -} - +}; -export const modifyPatient = (patientId: string, patient: PatientPayload): Promise => { +export const modifyPatient = ( + patientId: string, + patient: PatientModifyPayload +): Promise => { const patientPayloadUpdate = { Replace: { PatientID: patient.replace.patientId, PatientName: patient.replace.patientName, PatientBirthDate: patient.replace.patientBirthDate, - PatientSex: patient.replace.patientSex + PatientSex: patient.replace.patientSex, }, - Remove: patient.remove.map(field => field.charAt(0).toUpperCase() + field.slice(1)), + Keep: patient.keep, + Remove: patient.remove.map( + (field) => field.charAt(0).toUpperCase() + field.slice(1) + ), RemovePrivateTags: patient.removePrivateTags, Force: true, Synchronous: false, - KeepSource: patient.keepSource + KeepSource: patient.keepSource, }; - return axios.post(`/api/patients/${patientId}/modify`, patientPayloadUpdate) + return axios + .post(`/api/patients/${patientId}/modify`, patientPayloadUpdate) .then((response: any): OrthancResponse => { - const data = response.data + const data = response.data; return { id: data.ID, - path: data.Path - } + path: data.Path, + }; }) .catch(function (error) { if (error.response) { @@ -242,38 +274,51 @@ export const modifyPatient = (patientId: string, patient: PatientPayload): Promi }); }; -export const modifyStudy = (studyId: string, study: StudyPayload): Promise => { +export const modifyStudy = ( + studyId: string, + study: StudyModifyPayload +): Promise => { const studyPayloadUpdate = { Replace: { + PatientID: study.replace.patientId, + PatientName: study.replace.patientName, + PatientSex : study.replace.patientSex, + PatientBirthDate : study.replace.patientBirthDate, AccessionNumber: study.replace.accessionNumber, StudyDate: study.replace.studyDate, StudyDescription: study.replace.studyDescription, StudyID: study.replace.studyId, }, - Remove: study.remove.map(field => field.charAt(0).toUpperCase() + field.slice(1)), + Remove: study.remove.map( + (field) => field.charAt(0).toUpperCase() + field.slice(1) + ), RemovePrivateTags: study.removePrivateTags, + Keep: study.keep, Force: true, Synchronous: false, - KeepSource: study.keepSource + KeepSource: study.keepSource, }; - return axios.post(`/api/studies/${studyId}/modify`, studyPayloadUpdate) + return axios + .post(`/api/studies/${studyId}/modify`, studyPayloadUpdate) .then((response: any): OrthancResponse => { - const data = response.data + const data = response.data; return { id: data.ID, - path: data.Path - } - } - ) + path: data.Path, + }; + }) .catch(function (error) { if (error.response) { throw error.response; } throw error; }); -} +}; -export const modifySeries = (seriesId: string, series: SeriesPayload): Promise => { +export const modifySeries = ( + seriesId: string, + series: SeriesModifyPayload +): Promise => { const seriesPayloadUpdate = { Replace: { ImageOrientationPatient: series.replace.imageOrientationPatient, @@ -286,35 +331,39 @@ export const modifySeries = (seriesId: string, series: SeriesPayload): Promise field.charAt(0).toUpperCase() + field.slice(1)), + Remove: series.remove.map( + (field) => field.charAt(0).toUpperCase() + field.slice(1) + ), RemovePrivateTags: series.removePrivateTags, Force: true, Synchronous: false, - KeepSource: series.keepSource + KeepSource: series.keepSource, + Keep: series.keep, }; - return axios.post(`/api/series/${seriesId}/modify`, seriesPayloadUpdate) + return axios + .post(`/api/series/${seriesId}/modify`, seriesPayloadUpdate) .then((response: any): OrthancResponse => { - const data = response.data + const data = response.data; return { id: data.ID, - path: data.Path - } - } - ) + path: data.Path, + }; + }) .catch(function (error) { if (error.response) { throw error.response; } throw error; }); -} +}; export const getPatient = (patientId: string): Promise => { - return axios.get("/api/patients/" + patientId) + return axios + .get("/api/patients/" + patientId) .then((response): Patient => { - const data = response.data + const data = response.data; return { id: data.ID, isStable: data.IsStable, @@ -324,11 +373,11 @@ export const getPatient = (patientId: string): Promise => { patientBirthDate: data.MainDicomTags.PatientBirthDate, patientId: data.MainDicomTags.PatientID, patientName: data.MainDicomTags.PatientName, - patientSex: data.MainDicomTags.PatientSex + patientSex: data.MainDicomTags.PatientSex, }, studies: data.Studies, - type: data.Type - } + type: data.Type, + }; }) .catch(function (error) { if (error.response) { @@ -339,9 +388,10 @@ export const getPatient = (patientId: string): Promise => { }; export const deletePatient = (patientId: string): Promise => { - return axios.delete("/api/patients/" + patientId) + return axios + .delete("/api/patients/" + patientId) .then(() => { - return undefined + return undefined; }) .catch(function (error) { if (error.response) { @@ -352,9 +402,10 @@ export const deletePatient = (patientId: string): Promise => { }; export const deleteStudy = (studyId: string): Promise => { - return axios.delete("/api/studies/" + studyId) + return axios + .delete("/api/studies/" + studyId) .then(() => { - return undefined + return undefined; }) .catch(function (error) { if (error.response) { @@ -365,9 +416,10 @@ export const deleteStudy = (studyId: string): Promise => { }; export const deleteSeries = (seriesId: string): Promise => { - return axios.delete("/api/series/" + seriesId) + return axios + .delete("/api/series/" + seriesId) .then(() => { - return undefined + return undefined; }) .catch(function (error) { if (error.response) { diff --git a/src/ui/Checkbox.tsx b/src/ui/Checkbox.tsx index 609593dc..f6b380b7 100644 --- a/src/ui/Checkbox.tsx +++ b/src/ui/Checkbox.tsx @@ -8,7 +8,7 @@ type CheckBoxProps = { [key: string]: any; }; -const CheckBox: React.FC = ({label, checked, onChange ,bordered=true }) => { +const CheckBox: React.FC = ({label, checked, onChange ,bordered=true, ...props}) => { const id = useId() return (
@@ -18,7 +18,8 @@ const CheckBox: React.FC = ({label, checked, onChange ,bordered=t checked={checked} onChange={onChange} className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" - />{ + {...props} + />{ label&& diff --git a/src/ui/DownloadButton.tsx b/src/ui/DownloadButton.tsx new file mode 100644 index 00000000..3201c4d1 --- /dev/null +++ b/src/ui/DownloadButton.tsx @@ -0,0 +1,18 @@ +import { RiDownload2Line as Download } from "react-icons/ri"; + +type DownloadButtonProps = { + onClick: (e: React.MouseEvent) => void; +} + +const DownloadButtonProps = ({ onClick }: DownloadButtonProps) => ( + { + e.stopPropagation(); + onClick(e); + }} + /> +); + +export default DownloadButtonProps; diff --git a/src/ui/InputWithDelete.tsx b/src/ui/InputWithDelete.tsx index 726ed092..c4c93983 100644 --- a/src/ui/InputWithDelete.tsx +++ b/src/ui/InputWithDelete.tsx @@ -28,33 +28,26 @@ const InputWithDelete: React.FC = ({ const isMarkedForRemoval = fieldsToRemove.includes(fieldName); return ( -
-
- - } - value={value || ""} - onChange={onChange} - readOnly={readOnly || isMarkedForRemoval} - required={required} - placeholder={placeholder} - disabled={isMarkedForRemoval} - className={` +
+ + } + value={value || ""} + onChange={onChange} + readOnly={readOnly || isMarkedForRemoval} + required={required} + placeholder={placeholder} + disabled={isMarkedForRemoval} + className={` w-full ${isMarkedForRemoval ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : ''} `} - /> -
- } - checked={isMarkedForRemoval} - onChange={(e: ChangeEvent) => onRemove(fieldName, e.target.checked)} - bordered={false} /> + onRemove(fieldName, !isMarkedForRemoval)} />
); }; diff --git a/src/ui/index.ts b/src/ui/index.ts index 3a35ea7b..2e165657 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -4,6 +4,7 @@ import Button from "./Button"; import CheckBox from "./Checkbox"; import EditButton from "./EditButton"; import DeleteButton from "./DeleteButton"; +import DownloadButton from "./DownloadButton" import BooleanIcon from "./BooleanIcon"; import CloseButton from "./CloseButton"; import RetrieveButton from "../query/RetrieveButton"; @@ -49,6 +50,7 @@ export { ConfirmModal, CheckBox, CloseButton, + DownloadButton, DropDown, EditButton, DeleteButton, diff --git a/src/utils/index.ts b/src/utils/index.ts index 7f89f5db..1cdf1022 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -25,7 +25,6 @@ import type { Option, QueryPayload, Series, - SeriesPayload, Study, } from "./types"; import { useCustomToast } from "./toastify"; @@ -52,6 +51,5 @@ export { User, UserPayload, UserUpdatePayload, - Series, - SeriesPayload + Series }; diff --git a/src/utils/types.ts b/src/utils/types.ts index 13738593..3f0e635c 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -335,13 +335,14 @@ export type Patient = { type: string; }; -export type PatientPayload = { +export type PatientModifyPayload = { replace: Partial; remove: string[]; removePrivateTags: boolean; force: boolean; synchronous: boolean; keepSource: boolean; + keep : string[]; }; export type OrthancResponse = { @@ -361,20 +362,22 @@ export type Study = { type: string; }; -export type StudyPayload = { - replace: Partial; +export type StudyModifyPayload = { + replace: Partial; remove: string[]; removePrivateTags: boolean; force: boolean; + keep : string[]; synchronous: boolean; keepSource: boolean; }; -export type SeriesPayload = { +export type SeriesModifyPayload = { replace: Partial; remove: string[]; removePrivateTags: boolean; keepSource: boolean; force: boolean; synchronous: boolean; + keep : string[]; }; diff --git a/yarn.lock b/yarn.lock index 3d72bd24..e83f1b4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3146,6 +3146,13 @@ __metadata: languageName: node linkType: hard +"@types/mime-types@npm:^2": + version: 2.1.4 + resolution: "@types/mime-types@npm:2.1.4" + checksum: 10c0/a10d57881d14a053556b3d09292de467968d965b0a06d06732c748da39b3aa569270b5b9f32529fd0e9ac1e5f3b91abb894f5b1996373254a65cb87903c86622 + languageName: node + linkType: hard + "@types/mime@npm:^1": version: 1.3.5 resolution: "@types/mime@npm:1.3.5" @@ -5504,6 +5511,16 @@ __metadata: languageName: node linkType: hard +"fetch-blob@npm:^3.2.0": + version: 3.2.0 + resolution: "fetch-blob@npm:3.2.0" + dependencies: + node-domexception: "npm:^1.0.0" + web-streams-polyfill: "npm:^3.0.3" + checksum: 10c0/60054bf47bfa10fb0ba6cb7742acec2f37c1f56344f79a70bb8b1c48d77675927c720ff3191fa546410a0442c998d27ab05e9144c32d530d8a52fbe68f843b69 + languageName: node + linkType: hard + "file-entry-cache@npm:^8.0.0": version: 8.0.0 resolution: "file-entry-cache@npm:8.0.0" @@ -5806,6 +5823,7 @@ __metadata: "@tanstack/react-query-devtools": "npm:^5.51.23" "@tanstack/react-table": "npm:^8.20.1" "@types/jwt-decode": "npm:^3.1.0" + "@types/mime-types": "npm:^2" "@types/react": "npm:^18.3.3" "@types/react-dom": "npm:^18.3.0" "@types/react-query": "npm:^1.2.9" @@ -5823,7 +5841,9 @@ __metadata: eslint-plugin-tailwindcss: "npm:^3.17.4" i18next: "npm:^23.12.2" jwt-decode: "npm:^4.0.0" + mime-types: "npm:^2.1.35" moment: "npm:^2.30.1" + native-file-system-adapter: "npm:^3.0.1" postcss: "npm:^8.4.41" preline: "npm:^2.4.1" prettier: "npm:^3.3.3" @@ -7203,7 +7223,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.35, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -7438,6 +7458,18 @@ __metadata: languageName: node linkType: hard +"native-file-system-adapter@npm:^3.0.1": + version: 3.0.1 + resolution: "native-file-system-adapter@npm:3.0.1" + dependencies: + fetch-blob: "npm:^3.2.0" + dependenciesMeta: + fetch-blob: + optional: true + checksum: 10c0/2a60cd7fa6b92a85b1fc9dbbda91135f66af424e240e36c7c3708b987330844fb689e29a92a8a472d7216f685423e863af8acfb4b9a7bd73c0f6093fff4bf2fd + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -7478,6 +7510,13 @@ __metadata: languageName: node linkType: hard +"node-domexception@npm:^1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10c0/5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b + languageName: node + linkType: hard + "node-fetch-native@npm:^1.6.3": version: 1.6.4 resolution: "node-fetch-native@npm:1.6.4" @@ -10124,6 +10163,13 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:^3.0.3": + version: 3.3.3 + resolution: "web-streams-polyfill@npm:3.3.3" + checksum: 10c0/64e855c47f6c8330b5436147db1c75cb7e7474d924166800e8e2aab5eb6c76aac4981a84261dd2982b3e754490900b99791c80ae1407a9fa0dcff74f82ea3a7f + languageName: node + linkType: hard + "webpack-sources@npm:^3.2.3": version: 3.2.3 resolution: "webpack-sources@npm:3.2.3"