Skip to content

Commit 3f12bff

Browse files
committed
WEB-100 Add rename actions, extract shared ActionButton component
1 parent 3d4534d commit 3f12bff

5 files changed

Lines changed: 136 additions & 23 deletions

File tree

src/app/editor/DocumentList.tsx

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import Link from "next/link";
44
import { useRouter } from "next/navigation";
55
import { useTransition } from "react";
6-
import { FileText } from "lucide-react";
7-
import { createDocumentAction } from "../../lib/actions";
6+
import { FileText, Pencil } from "lucide-react";
7+
import { createDocumentAction, renameDocumentAction } from "../../lib/actions";
8+
import { runAction } from "./runAction";
89
import { getDocumentName } from "../../lib/documents";
9-
import { ResourceCard, NewResourceCard, formatRelativeTime } from "./ResourceCard";
10+
import { ResourceCard, NewResourceCard, ActionButton, formatRelativeTime } from "./ResourceCard";
1011

1112
function NewDocumentCard() {
1213
const router = useRouter();
@@ -49,7 +50,19 @@ function NewDocumentCard() {
4950
);
5051
}
5152

52-
function DocumentCard({ id, name, lastModified }: { id: number; name: string | null; lastModified: Date | null }) {
53+
function DocumentCard({
54+
id,
55+
name,
56+
lastModified,
57+
onRename,
58+
disabled,
59+
}: {
60+
id: number;
61+
name: string | null;
62+
lastModified: Date | null;
63+
onRename: (id: number, currentName: string) => void;
64+
disabled: boolean;
65+
}) {
5366
const displayName = getDocumentName({ id, name });
5467

5568
return (
@@ -58,6 +71,18 @@ function DocumentCard({ id, name, lastModified }: { id: number; name: string | n
5871
preview={<FileText className="h-10 w-10 text-gray-400" />}
5972
name={displayName}
6073
date={lastModified ? formatRelativeTime(lastModified) : "No versions"}
74+
actions={
75+
<ActionButton
76+
onClick={(e) => {
77+
e.preventDefault();
78+
onRename(id, displayName);
79+
}}
80+
disabled={disabled}
81+
title="Rename"
82+
>
83+
<Pencil className="h-3 w-3" />
84+
</ActionButton>
85+
}
6186
/>
6287
</Link>
6388
);
@@ -66,6 +91,22 @@ function DocumentCard({ id, name, lastModified }: { id: number; name: string | n
6691
export function DocumentList({ documents }: {
6792
documents: { id: number; name: string | null; lastModified: Date | null }[];
6893
}) {
94+
const router = useRouter();
95+
const [isPending, startTransition] = useTransition();
96+
97+
function handleRename(id: number, currentName: string) {
98+
const newName = window.prompt("Rename document", currentName);
99+
if (newName === null || newName.trim() === "" || newName.trim() === currentName) return;
100+
startTransition(async () => {
101+
const result = await runAction(renameDocumentAction({ id, name: newName.trim() }));
102+
if (result.success) {
103+
router.refresh();
104+
} else {
105+
alert(result.error);
106+
}
107+
});
108+
}
109+
69110
return (
70111
<div className="flex flex-col gap-4">
71112
<h1 className="text-2xl font-bold">Documents</h1>
@@ -79,6 +120,8 @@ export function DocumentList({ documents }: {
79120
id={doc.id}
80121
name={doc.name}
81122
lastModified={doc.lastModified}
123+
onRename={handleRename}
124+
disabled={isPending}
82125
/>
83126
))}
84127
</div>

src/app/editor/MediaLibrary.tsx

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"use client";
22

33
import { useRef, useState, useTransition } from "react";
4-
import { File as FileIcon, Trash2, Copy } from "lucide-react";
5-
import { uploadMediaAction, deleteMediaAction } from "../../lib/actions";
4+
import { File as FileIcon, Trash2, Copy, Pencil } from "lucide-react";
5+
import { uploadMediaAction, deleteMediaAction, renameMediaAction } from "../../lib/actions";
66
import type { Media } from "../../generated/prisma/client";
77
import { runAction } from "./runAction";
8-
import { ResourceCard, NewResourceCard, formatRelativeTime } from "./ResourceCard";
8+
import { ResourceCard, NewResourceCard, ActionButton, formatRelativeTime } from "./ResourceCard";
99

1010
type MediaWithUrl = Media & { url: string };
1111

@@ -48,10 +48,12 @@ function UploadCard({
4848

4949
function MediaCard({
5050
file,
51+
onRename,
5152
onDelete,
5253
disabled,
5354
}: {
5455
file: MediaWithUrl;
56+
onRename: (file: MediaWithUrl) => void;
5557
onDelete: (file: MediaWithUrl) => void;
5658
disabled: boolean;
5759
}) {
@@ -75,23 +77,15 @@ function MediaCard({
7577
date={`${createdDate} \u00B7 ${formatFileSize(file.size)}`}
7678
actions={
7779
<>
78-
<button
79-
type="button"
80-
onClick={() => navigator.clipboard.writeText(file.url)}
81-
className="rounded p-1 text-gray-500 hover:text-blue-600"
82-
title="Copy URL"
83-
>
80+
<ActionButton onClick={() => onRename(file)} disabled={disabled} title="Rename">
81+
<Pencil className="h-3 w-3" />
82+
</ActionButton>
83+
<ActionButton onClick={() => navigator.clipboard.writeText(file.url)} title="Copy URL">
8484
<Copy className="h-3 w-3" />
85-
</button>
86-
<button
87-
type="button"
88-
onClick={() => onDelete(file)}
89-
disabled={disabled}
90-
className="rounded p-1 text-gray-500 hover:text-red-600 disabled:opacity-50"
91-
title="Delete"
92-
>
85+
</ActionButton>
86+
<ActionButton onClick={() => onDelete(file)} disabled={disabled} title="Delete" variant="danger">
9387
<Trash2 className="h-3 w-3" />
94-
</button>
88+
</ActionButton>
9589
</>
9690
}
9791
/>
@@ -115,6 +109,19 @@ export function MediaLibrary({ files: initialFiles }: { files: MediaWithUrl[] })
115109
});
116110
}
117111

112+
function handleRename(file: MediaWithUrl) {
113+
const newName = window.prompt("Rename file", file.name);
114+
if (newName === null || newName.trim() === "" || newName.trim() === file.name) return;
115+
startTransition(async () => {
116+
const result = await runAction(renameMediaAction({ id: file.id, name: newName.trim() }));
117+
if (result.success) {
118+
setFiles((prev) => prev.map((f) => (f.id === file.id ? { ...f, name: newName.trim() } : f)));
119+
} else {
120+
alert(result.error);
121+
}
122+
});
123+
}
124+
118125
function handleDelete(file: MediaWithUrl) {
119126
if (!window.confirm(`Delete "${file.name}"?`)) return;
120127
startTransition(async () => {
@@ -138,6 +145,7 @@ export function MediaLibrary({ files: initialFiles }: { files: MediaWithUrl[] })
138145
<MediaCard
139146
key={file.id}
140147
file={file}
148+
onRename={handleRename}
141149
onDelete={handleDelete}
142150
disabled={isPending}
143151
/>

src/app/editor/ResourceCard.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,30 @@ export function NewResourceCard({
8383
</button>
8484
);
8585
}
86+
87+
export function ActionButton({
88+
onClick,
89+
disabled,
90+
title,
91+
variant = "default",
92+
children,
93+
}: {
94+
onClick: (e: React.MouseEvent) => void;
95+
disabled?: boolean;
96+
title: string;
97+
variant?: "default" | "danger";
98+
children: ReactNode;
99+
}) {
100+
const hoverColor = variant === "danger" ? "hover:text-red-600" : "hover:text-blue-600";
101+
return (
102+
<button
103+
type="button"
104+
onClick={onClick}
105+
disabled={disabled}
106+
className={`rounded p-1 text-gray-500 ${hoverColor} disabled:opacity-50`}
107+
title={title}
108+
>
109+
{children}
110+
</button>
111+
);
112+
}

src/lib/actions.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
CreateRouteInput,
1515
DeleteMediaInput,
1616
DeleteRouteInput,
17+
RenameInput,
1718
SaveVersionInput,
1819
PublishVersionInput,
1920
UpdateRouteInput,
@@ -60,6 +61,28 @@ export async function publishVersionAction(
6061
});
6162
}
6263

64+
async function renameRecord(
65+
model: "document" | "media",
66+
input: RenameInput
67+
): Promise<ActionResult<void>> {
68+
return wrapAction(async () => {
69+
const name = input.name.trim();
70+
if (!name) {
71+
throw new Error("Name cannot be empty");
72+
}
73+
await (prisma[model] as typeof prisma.document).update({
74+
where: { id: input.id },
75+
data: { name },
76+
});
77+
});
78+
}
79+
80+
export async function renameDocumentAction(
81+
input: RenameInput
82+
): Promise<ActionResult<void>> {
83+
return renameRecord("document", input);
84+
}
85+
6386
export async function createRouteAction(
6487
input: CreateRouteInput
6588
): Promise<ActionResult<{ routeId: number }>> {
@@ -114,7 +137,8 @@ export async function uploadMediaAction(
114137
throw new Error("File is empty");
115138
}
116139

117-
const storagePath = `${Date.now()}-${file.name}`;
140+
const ext = file.name.includes(".") ? `.${file.name.split(".").pop()}` : "";
141+
const storagePath = `${crypto.randomUUID()}${ext}`;
118142

119143
const { error } = await supabase.storage
120144
.from(MEDIA_BUCKET)
@@ -137,6 +161,12 @@ export async function uploadMediaAction(
137161
});
138162
}
139163

164+
export async function renameMediaAction(
165+
input: RenameInput
166+
): Promise<ActionResult<void>> {
167+
return renameRecord("media", input);
168+
}
169+
140170
export async function deleteMediaAction(
141171
input: DeleteMediaInput
142172
): Promise<ActionResult<void>> {

src/lib/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export type DeleteRouteInput = {
4040
id: number;
4141
};
4242

43+
export type RenameInput = {
44+
id: number;
45+
name: string;
46+
};
47+
4348
export type DeleteMediaInput = {
4449
id: number;
4550
};

0 commit comments

Comments
 (0)