fix: add option to remove episode from space

This commit is contained in:
Harshith Mullapudi 2025-10-09 16:12:30 +05:30
parent a14b83d66d
commit 3bdf051b32
7 changed files with 125 additions and 11 deletions

View File

@ -1,5 +1,4 @@
import ReactMarkdown from "react-markdown"; import ReactMarkdown, {type Components } from "react-markdown";
import type { Components } from "react-markdown";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
const markdownComponents: Components = { const markdownComponents: Components = {

View File

@ -0,0 +1,112 @@
import { EllipsisVertical, Trash } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { Button } from "../ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
import { useEffect, useState } from "react";
import { useFetcher, useNavigate } from "@remix-run/react";
import { toast } from "~/hooks/use-toast";
interface SpaceEpisodeActionsProps {
episodeId: string;
spaceId: string;
}
export const SpaceEpisodeActions = ({
episodeId,
spaceId,
}: SpaceEpisodeActionsProps) => {
const [removeDialogOpen, setRemoveDialogOpen] = useState(false);
const removeFetcher = useFetcher();
const navigate = useNavigate();
const handleRemove = () => {
removeFetcher.submit(
{
episodeIds: JSON.stringify([episodeId]),
spaceId,
action: "remove",
},
{
method: "post",
action: "/api/v1/episodes/assign-space",
encType: "application/json",
},
);
setRemoveDialogOpen(false);
};
useEffect(() => {
if (removeFetcher.state === "idle" && removeFetcher.data) {
if (removeFetcher.data.success) {
toast({
title: "Success",
description: "Episode removed from space",
});
// Reload the page to refresh the episode list
navigate(".", { replace: true });
} else {
toast({
title: "Error",
description: removeFetcher.data.error || "Failed to remove episode",
variant: "destructive",
});
}
}
}, [removeFetcher.state, removeFetcher.data, navigate]);
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-6 w-6 shrink-0 items-center justify-center p-0 opacity-0 transition-opacity group-hover:opacity-100"
onClick={(e) => e.stopPropagation()}
>
<EllipsisVertical size={16} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
<DropdownMenuItem onClick={() => setRemoveDialogOpen(true)}>
<Button variant="link" size="sm" className="gap-2 rounded">
<Trash size={15} /> Remove from space
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<AlertDialog open={removeDialogOpen} onOpenChange={setRemoveDialogOpen}>
<AlertDialogContent onClick={(e) => e.stopPropagation()}>
<AlertDialogHeader>
<AlertDialogTitle>Remove from space</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to remove this episode from the space? This
will not delete the episode itself.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleRemove}>
Remove
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};

View File

@ -5,6 +5,7 @@ import { cn } from "~/lib/utils";
import { useNavigate } from "@remix-run/react"; import { useNavigate } from "@remix-run/react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { StyledMarkdown } from "../common/styled-markdown"; import { StyledMarkdown } from "../common/styled-markdown";
import { SpaceEpisodeActions } from "./space-episode-actions";
export interface Episode { export interface Episode {
uuid: string; uuid: string;
@ -20,9 +21,10 @@ export interface Episode {
interface SpaceFactCardProps { interface SpaceFactCardProps {
episode: Episode; episode: Episode;
spaceId: string;
} }
export function SpaceEpisodeCard({ episode }: SpaceFactCardProps) { export function SpaceEpisodeCard({ episode, spaceId }: SpaceFactCardProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const formatDate = (date: Date | string) => { const formatDate = (date: Date | string) => {
const d = new Date(date); const d = new Date(date);
@ -62,6 +64,7 @@ export function SpaceEpisodeCard({ episode }: SpaceFactCardProps) {
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />
{formatDate(episode.validAt)} {formatDate(episode.validAt)}
</Badge> </Badge>
<SpaceEpisodeActions episodeId={episode.uuid} spaceId={spaceId} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -18,12 +18,14 @@ interface SpaceEpisodesListProps {
loadMore: () => void; loadMore: () => void;
isLoading: boolean; isLoading: boolean;
height?: number; height?: number;
spaceId: string;
} }
function EpisodeItemRenderer( function EpisodeItemRenderer(
props: ListRowProps, props: ListRowProps,
episodes: Episode[], episodes: Episode[],
cache: CellMeasurerCache, cache: CellMeasurerCache,
spaceId: string,
) { ) {
const { index, key, style, parent } = props; const { index, key, style, parent } = props;
const episode = episodes[index]; const episode = episodes[index];
@ -37,7 +39,7 @@ function EpisodeItemRenderer(
rowIndex={index} rowIndex={index}
> >
<div key={key} style={style} className="pb-2"> <div key={key} style={style} className="pb-2">
<SpaceEpisodeCard episode={episode} /> <SpaceEpisodeCard episode={episode} spaceId={spaceId} />
</div> </div>
</CellMeasurer> </CellMeasurer>
); );
@ -48,6 +50,7 @@ export function SpaceEpisodesList({
hasMore, hasMore,
loadMore, loadMore,
isLoading, isLoading,
spaceId,
}: SpaceEpisodesListProps) { }: SpaceEpisodesListProps) {
// Create a CellMeasurerCache instance using useRef to prevent recreation // Create a CellMeasurerCache instance using useRef to prevent recreation
const cacheRef = useRef<CellMeasurerCache | null>(null); const cacheRef = useRef<CellMeasurerCache | null>(null);
@ -91,7 +94,7 @@ export function SpaceEpisodesList({
}; };
const rowRenderer = (props: ListRowProps) => { const rowRenderer = (props: ListRowProps) => {
return EpisodeItemRenderer(props, episodes, cache); return EpisodeItemRenderer(props, episodes, cache, spaceId);
}; };
const rowHeight = ({ index }: Index) => { const rowHeight = ({ index }: Index) => {

View File

@ -37,7 +37,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
} }
export default function Episodes() { export default function Episodes() {
const { episodes } = useLoaderData<typeof loader>(); const { episodes, space } = useLoaderData<typeof loader>();
const [selectedValidDate, setSelectedValidDate] = useState< const [selectedValidDate, setSelectedValidDate] = useState<
string | undefined string | undefined
>(); >();
@ -98,6 +98,7 @@ export default function Episodes() {
hasMore={false} // TODO: Implement real pagination hasMore={false} // TODO: Implement real pagination
loadMore={loadMore} loadMore={loadMore}
isLoading={false} isLoading={false}
spaceId={space.id}
/> />
)} )}
</ClientOnly> </ClientOnly>

View File

@ -236,10 +236,6 @@ export class SpaceService {
throw new Error("Space not found"); throw new Error("Space not found");
} }
if (space.name === "Profile") {
throw new Error("Cannot reset Profile space");
}
// Delete all relationships in Neo4j (episodes, statements, etc.) // Delete all relationships in Neo4j (episodes, statements, etc.)
await deleteSpace(spaceId, userId); await deleteSpace(spaceId, userId);

View File

@ -198,7 +198,7 @@ async function generateSpaceSummary(
if ( if (
episodeDifference < CONFIG.summaryEpisodeThreshold || episodeDifference < CONFIG.summaryEpisodeThreshold ||
lastSummaryEpisodeCount === 0 lastSummaryEpisodeCount !== 0
) { ) {
logger.info( logger.info(
`Skipping summary generation for space ${spaceId}: only ${episodeDifference} new episodes (threshold: ${CONFIG.summaryEpisodeThreshold})`, `Skipping summary generation for space ${spaceId}: only ${episodeDifference} new episodes (threshold: ${CONFIG.summaryEpisodeThreshold})`,