Skip to content
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
106 changes: 106 additions & 0 deletions webapp/src/components/ui/ConfirmActionDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Trash2, Eraser } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { Dialog } from "@/components/ui/Dialog";

type ActionKind = "unindex" | "remove";

interface ConfirmActionDialogProps {
open: boolean;
action: ActionKind;
level: "study" | "series" | "instance";
description: string;
fileCount?: number;
onConfirm: () => void;
onCancel: () => void;
loading?: boolean;
}

const ACTION_CONFIG = {
unindex: {
title: "Unindex",
icon: Eraser,
iconClass: "text-amber-500",
bgClass: "bg-amber-50 dark:bg-amber-950/30 border-amber-200 dark:border-amber-800",
message:
"This will remove the data from the search index. The files will remain on disk but will no longer appear in search results. This operation can be reversed by re-indexing the files.",
confirmLabel: "Unindex",
confirmVariant: "outline-solid" as const,
},
remove: {
title: "Remove",
icon: Trash2,
iconClass: "text-destructive",
bgClass: "bg-red-50 dark:bg-red-950/30 border-red-200 dark:border-red-800",
message:
"This will permanently delete the files from disk AND remove them from the search index. This operation is irreversible and data cannot be recovered.",
confirmLabel: "Remove",
confirmVariant: "destructive" as const,
},
} as const;

const LEVEL_LABEL: Record<string, string> = {
study: "Study",
series: "Series",
instance: "Instance",
};

export function ConfirmActionDialog({
open,
action,
level,
description,
fileCount,
onConfirm,
onCancel,
loading = false,
}: ConfirmActionDialogProps) {
const config = ACTION_CONFIG[action];
const Icon = config.icon;

return (
<Dialog open={open} onClose={onCancel} title={`Confirm ${config.title}`} size="sm">
<div className="p-6 space-y-4">
{/* Warning banner */}
<div className={`flex gap-3 p-4 rounded-lg border ${config.bgClass}`}>
<Icon className={`w-5 h-5 shrink-0 mt-0.5 ${config.iconClass}`} />
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">
{LEVEL_LABEL[level]} level {config.title}
</p>
<p className="text-sm text-muted-foreground">{config.message}</p>
</div>
</div>

{/* Scope details */}
<div className="text-sm text-muted-foreground space-y-1">
<p>
<span className="font-medium text-foreground">{LEVEL_LABEL[level]}:</span>{" "}
{description}
</p>
{fileCount !== undefined && (
<p>
<span className="font-medium text-foreground">Files:</span>{" "}
{fileCount} {fileCount === 1 ? "file" : "files"} will be affected
</p>
)}
</div>

{/* Actions */}
<div className="flex justify-end gap-3 pt-2">
<Button variant="ghost" size="sm" onClick={onCancel} disabled={loading}>
Cancel
</Button>
<Button
variant={config.confirmVariant}
size="sm"
onClick={onConfirm}
disabled={loading}
>
<Icon className="w-4 h-4 mr-1.5" />
{loading ? `${config.title}...` : config.confirmLabel}
</Button>
</div>
</div>
</Dialog>
);
}
1 change: 1 addition & 0 deletions webapp/src/components/ui/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function Dialog({ open, onClose, children, title, size = "md" }: DialogPr
{/* Dialog */}
<div
className={`relative bg-background rounded-lg shadow-xl w-full ${sizeClasses[size]} max-h-[90vh] flex flex-col`}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
{title && (
Expand Down
65 changes: 64 additions & 1 deletion webapp/src/features/search/SeriesViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from "react";
import { useSearchStore } from "@/stores/SearchStore";
import { useAuthStore } from "@/stores/AuthStore";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { Button } from "@/components/ui/Button";
import {
Expand All @@ -9,18 +10,74 @@ import {
List,
FileText,
ArrowLeft,
Eraser,
Trash2,
} from "lucide-react";
import { dicoogleService } from "@/services/dicoogleService";
import { ImageList } from "./components/ImageList";
import { DICOMDumpModal } from "./components/DICOMDumpModal";
import { QuickViewer } from "./components/QuickViewer";
import { AdvancedViewer } from "./components/AdvancedViewer";
import { ConfirmActionDialog } from "@/components/ui/ConfirmActionDialog";
import { useEntryActions } from "./hooks";
import type { Study, Series } from "@/types";

interface SeriesViewerProps {
study: Study;
}

function SeriesActions({ study, series }: { study: Study; series: Series }) {
const { user } = useAuthStore();
const {
openDialog,
pendingAction,
description,
fileCount,
loading,
handleUnindex,
handleRemove,
handleConfirm,
handleCancel,
} = useEntryActions({ level: "series", study, series });

if (!user?.admin) return null;

return (
<>
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-amber-600 hover:text-amber-700 hover:bg-amber-50 dark:hover:bg-amber-950/30"
title="Unindex (does not remove files physically)"
onClick={handleUnindex}
>
<Eraser className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-destructive hover:bg-red-50 dark:hover:bg-red-950/30"
title="Remove files permanently"
onClick={handleRemove}
>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
<ConfirmActionDialog
open={openDialog}
action={pendingAction || "unindex"}
level="series"
description={description}
fileCount={fileCount}
loading={loading}
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
</>
);
}

export function SeriesViewer({ study }: SeriesViewerProps) {
const { selectedSeries, deselectStudy } = useSearchStore();
const series = selectedSeries || [];
Expand Down Expand Up @@ -163,6 +220,7 @@ export function SeriesViewer({ study }: SeriesViewerProps) {
return (
<div ref={containerRef}>
<ImageList
study={study}
series={selectedSeriesForImages}
onViewQuick={handleViewImageQuick}
onViewAdvanced={handleViewImageAdvanced}
Expand Down Expand Up @@ -308,6 +366,10 @@ export function SeriesViewer({ study }: SeriesViewerProps) {
<FileText className="w-3.5 h-3.5 mr-1.5" /> Images
</Button>
</div>

<div className="flex justify-center pt-1">
<SeriesActions study={study} series={s} />
</div>
</div>
))}
</div>
Expand Down Expand Up @@ -354,7 +416,7 @@ export function SeriesViewer({ study }: SeriesViewerProps) {
</p>
</div>

<div className="flex gap-2">
<div className="flex gap-2 items-center">
<Button
size="sm"
variant="outline-solid"
Expand All @@ -373,6 +435,7 @@ export function SeriesViewer({ study }: SeriesViewerProps) {
>
<FileText className="w-4 h-4 mr-1" /> View Images
</Button>
<SeriesActions study={study} series={s} />
</div>
</div>
</div>
Expand Down
63 changes: 63 additions & 0 deletions webapp/src/features/search/StudyList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, Suspense } from "react";
import { useSearchStore } from "@/stores/SearchStore";
import { useAuthStore } from "@/stores/AuthStore";
import { Card, CardContent } from "@/components/ui/Card";
import { Button } from "@/components/ui/Button";
import {
Expand All @@ -12,6 +13,8 @@ import {
ChevronLeft,
LayoutList,
Grid3X3,
Eraser,
Trash2,
} from "lucide-react";
import type { Study } from "@/types";
import {
Expand All @@ -22,6 +25,8 @@ import {
invokeResultOptionAction,
invokeResultBatchAction,
} from "@/plugin-system";
import { ConfirmActionDialog } from "@/components/ui/ConfirmActionDialog";
import { useEntryActions } from "./hooks";

interface StudyListProps {
studies: Study[];
Expand All @@ -37,6 +42,61 @@ interface StudyItemProps {
const ITEMS_PER_PAGE_CARD = 3;
const ITEMS_PER_PAGE_LIST = 10;

function StudyActions({ study }: { study: Study }) {
const { user } = useAuthStore();
const {
openDialog,
pendingAction,
description,
fileCount,
loading,
handleUnindex,
handleRemove,
handleConfirm,
handleCancel,
} = useEntryActions({ level: "study", study });

if (!user?.admin) return null;

return (
<>
<div
className="flex items-center gap-1 pt-1"
onClick={(e) => e.stopPropagation()}
>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-amber-600 hover:text-amber-700 hover:bg-amber-50 dark:hover:bg-amber-950/30"
title="Unindex (does not remove files physically)"
onClick={handleUnindex}
>
<Eraser className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-destructive hover:bg-red-50 dark:hover:bg-red-950/30"
title="Remove files permanently"
onClick={handleRemove}
>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
<ConfirmActionDialog
open={openDialog}
action={pendingAction || "unindex"}
level="study"
description={description}
fileCount={fileCount}
loading={loading}
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
</>
);
}

const formatDate = (date?: string) => {
if (!date) return "N/A";
const year = date.substring(0, 4);
Expand Down Expand Up @@ -116,6 +176,8 @@ const StudyCard = ({ study, isSelected, onClick }: StudyItemProps) => {
</code>
</details>

<StudyActions study={study} />

{optionExtensions.length > 0 && (
<div
className="flex items-center gap-2 pt-2"
Expand Down Expand Up @@ -218,6 +280,7 @@ const StudyListItem = ({
{ext.icon}
</Button>
))}
<StudyActions study={study} />
</div>
)}
</div>
Expand Down
Loading
Loading