Skip to content
Snippets Groups Projects
Unverified Commit ad778dd3 authored by Imtiaz Mehmood's avatar Imtiaz Mehmood Committed by GitHub
Browse files

Feat/quick delete chat (#1302)


* feat:quick delete chat thread

* update:pull request template

* refactor bulk-deletion implementation

* unset pull_request_changes

* add border none for desktop support

* unset marks when toggling bulk mode

---------

Co-authored-by: default avatartimothycarambat <rambat1010@gmail.com>
parent 79ea15e0
No related branches found
No related tags found
No related merge requests found
import Workspace from "@/models/workspace"; import Workspace from "@/models/workspace";
import paths from "@/utils/paths"; import paths from "@/utils/paths";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { DotsThree, PencilSimple, Trash } from "@phosphor-icons/react"; import {
ArrowCounterClockwise,
DotsThree,
PencilSimple,
Trash,
X,
} from "@phosphor-icons/react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import truncate from "truncate"; import truncate from "truncate";
...@@ -14,7 +20,9 @@ export default function ThreadItem({ ...@@ -14,7 +20,9 @@ export default function ThreadItem({
workspace, workspace,
thread, thread,
onRemove, onRemove,
toggleMarkForDeletion,
hasNext, hasNext,
ctrlPressed = false,
}) { }) {
const { slug } = useParams(); const { slug } = useParams();
const optionsContainer = useRef(null); const optionsContainer = useRef(null);
...@@ -57,14 +65,30 @@ export default function ThreadItem({ ...@@ -57,14 +65,30 @@ export default function ThreadItem({
/> />
<div className="flex w-full items-center justify-between pr-2 group relative"> <div className="flex w-full items-center justify-between pr-2 group relative">
{thread.deleted ? ( {thread.deleted ? (
<a className="w-full"> <div className="w-full flex justify-between">
<p className={`text-left text-sm text-slate-400/50 italic`}> <div className="w-full ">
deleted thread <p className={`text-left text-sm text-slate-400/50 italic`}>
</p> deleted thread
</a> </p>
</div>
{ctrlPressed && (
<button
type="button"
className="border-none"
onClick={() => toggleMarkForDeletion(thread.id)}
>
<ArrowCounterClockwise
className="text-zinc-300 hover:text-white"
size={18}
/>
</button>
)}
</div>
) : ( ) : (
<a <a
href={window.location.pathname === linkTo ? "#" : linkTo} href={
window.location.pathname === linkTo || ctrlPressed ? "#" : linkTo
}
className="w-full" className="w-full"
aria-current={isActive ? "page" : ""} aria-current={isActive ? "page" : ""}
> >
...@@ -79,15 +103,30 @@ export default function ThreadItem({ ...@@ -79,15 +103,30 @@ export default function ThreadItem({
)} )}
{!!thread.slug && !thread.deleted && ( {!!thread.slug && !thread.deleted && (
<div ref={optionsContainer}> <div ref={optionsContainer}>
<div className="flex items-center w-fit group-hover:visible md:invisible gap-x-1"> {ctrlPressed ? (
<button <button
type="button" type="button"
onClick={() => setShowOptions(!showOptions)} className="border-none"
aria-label="Thread options" onClick={() => toggleMarkForDeletion(thread.id)}
> >
<DotsThree className="text-slate-300" size={25} /> <X
className="text-zinc-300 hover:text-white"
weight="bold"
size={18}
/>
</button> </button>
</div> ) : (
<div className="flex items-center w-fit group-hover:visible md:invisible gap-x-1">
<button
type="button"
className="border-none"
onClick={() => setShowOptions(!showOptions)}
aria-label="Thread options"
>
<DotsThree className="text-slate-300" size={25} />
</button>
</div>
)}
{showOptions && ( {showOptions && (
<OptionsMenu <OptionsMenu
containerRef={optionsContainer} containerRef={optionsContainer}
......
import Workspace from "@/models/workspace"; import Workspace from "@/models/workspace";
import paths from "@/utils/paths"; import paths from "@/utils/paths";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { Plus, CircleNotch } from "@phosphor-icons/react"; import { Plus, CircleNotch, Trash } from "@phosphor-icons/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import ThreadItem from "./ThreadItem"; import ThreadItem from "./ThreadItem";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
...@@ -10,6 +10,7 @@ export default function ThreadContainer({ workspace }) { ...@@ -10,6 +10,7 @@ export default function ThreadContainer({ workspace }) {
const { threadSlug = null } = useParams(); const { threadSlug = null } = useParams();
const [threads, setThreads] = useState([]); const [threads, setThreads] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [ctrlPressed, setCtrlPressed] = useState(false);
useEffect(() => { useEffect(() => {
async function fetchThreads() { async function fetchThreads() {
...@@ -21,6 +22,43 @@ export default function ThreadContainer({ workspace }) { ...@@ -21,6 +22,43 @@ export default function ThreadContainer({ workspace }) {
fetchThreads(); fetchThreads();
}, [workspace.slug]); }, [workspace.slug]);
// Enable toggling of meta-key (ctrl on win and cmd/fn on others)
useEffect(() => {
const handleKeyDown = (event) => {
if (["Control", "Meta"].includes(event.key)) {
setCtrlPressed((prev) => !prev);
// when toggling, unset bulk progress so
// previously marked threads that were never deleted
// come back to life.
setThreads((prev) =>
prev.map((t) => {
return { ...t, deleted: false };
})
);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
const toggleForDeletion = (id) => {
setThreads((prev) =>
prev.map((t) => {
if (t.id !== id) return t;
return { ...t, deleted: !t.deleted };
})
);
};
const handleDeleteAll = async () => {
const slugs = threads.filter((t) => t.deleted === true).map((t) => t.slug);
await Workspace.threads.deleteBulk(workspace.slug, slugs);
setThreads((prev) => prev.filter((t) => !t.deleted));
setCtrlPressed(false);
};
function removeThread(threadId) { function removeThread(threadId) {
setThreads((prev) => setThreads((prev) =>
prev.map((_t) => { prev.map((_t) => {
...@@ -28,6 +66,12 @@ export default function ThreadContainer({ workspace }) { ...@@ -28,6 +66,12 @@ export default function ThreadContainer({ workspace }) {
return { ..._t, deleted: true }; return { ..._t, deleted: true };
}) })
); );
// Show thread was deleted, but then remove from threads entirely so it will
// not appear in bulk-selection.
setTimeout(() => {
setThreads((prev) => prev.filter((t) => !t.deleted));
}, 500);
} }
if (loading) { if (loading) {
...@@ -58,6 +102,8 @@ export default function ThreadContainer({ workspace }) { ...@@ -58,6 +102,8 @@ export default function ThreadContainer({ workspace }) {
<ThreadItem <ThreadItem
key={thread.slug} key={thread.slug}
idx={i + 1} idx={i + 1}
ctrlPressed={ctrlPressed}
toggleMarkForDeletion={toggleForDeletion}
activeIdx={activeThreadIdx} activeIdx={activeThreadIdx}
isActive={activeThreadIdx === i + 1} isActive={activeThreadIdx === i + 1}
workspace={workspace} workspace={workspace}
...@@ -66,6 +112,11 @@ export default function ThreadContainer({ workspace }) { ...@@ -66,6 +112,11 @@ export default function ThreadContainer({ workspace }) {
hasNext={i !== threads.length - 1} hasNext={i !== threads.length - 1}
/> />
))} ))}
<DeleteAllThreadButton
ctrlPressed={ctrlPressed}
threads={threads}
onDelete={handleDeleteAll}
/>
<NewThreadButton workspace={workspace} /> <NewThreadButton workspace={workspace} />
</div> </div>
); );
...@@ -113,3 +164,28 @@ function NewThreadButton({ workspace }) { ...@@ -113,3 +164,28 @@ function NewThreadButton({ workspace }) {
</button> </button>
); );
} }
function DeleteAllThreadButton({ ctrlPressed, threads, onDelete }) {
if (!ctrlPressed || threads.filter((t) => t.deleted).length === 0)
return null;
return (
<button
type="button"
onClick={onDelete}
className="w-full relative flex h-[40px] items-center border-none hover:bg-red-400/20 rounded-lg group"
>
<div className="flex w-full gap-x-2 items-center pl-4">
<div className="bg-zinc-600 p-2 rounded-lg h-[24px] w-[24px] flex items-center justify-center">
<Trash
weight="bold"
size={14}
className="shrink-0 text-slate-100 group-hover:text-red-400"
/>
</div>
<p className="text-white text-left text-sm group-hover:text-red-400">
Delete Selected
</p>
</div>
</button>
);
}
...@@ -62,6 +62,18 @@ const WorkspaceThread = { ...@@ -62,6 +62,18 @@ const WorkspaceThread = {
.then((res) => res.ok) .then((res) => res.ok)
.catch(() => false); .catch(() => false);
}, },
deleteBulk: async function (workspaceSlug, threadSlugs = []) {
return await fetch(
`${API_BASE}/workspace/${workspaceSlug}/thread-bulk-delete`,
{
method: "DELETE",
body: JSON.stringify({ slugs: threadSlugs }),
headers: baseHeaders(),
}
)
.then((res) => res.ok)
.catch(() => false);
},
chatHistory: async function (workspaceSlug, threadSlug) { chatHistory: async function (workspaceSlug, threadSlug) {
const history = await fetch( const history = await fetch(
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/chats`, `${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/chats`,
......
...@@ -92,6 +92,29 @@ function workspaceThreadEndpoints(app) { ...@@ -92,6 +92,29 @@ function workspaceThreadEndpoints(app) {
} }
); );
app.delete(
"/workspace/:slug/thread-bulk-delete",
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
async (request, response) => {
try {
const { slugs = [] } = reqBody(request);
if (slugs.length === 0) return response.sendStatus(200).end();
const user = await userFromSession(request, response);
const workspace = response.locals.workspace;
await WorkspaceThread.delete({
slug: { in: slugs },
user_id: user?.id ?? null,
workspace_id: workspace.id,
});
response.sendStatus(200).end();
} catch (e) {
console.log(e.message, e);
response.sendStatus(500).end();
}
}
);
app.get( app.get(
"/workspace/:slug/thread/:threadSlug/chats", "/workspace/:slug/thread/:threadSlug/chats",
[ [
......
...@@ -61,7 +61,7 @@ const WorkspaceThread = { ...@@ -61,7 +61,7 @@ const WorkspaceThread = {
delete: async function (clause = {}) { delete: async function (clause = {}) {
try { try {
await prisma.workspace_threads.delete({ await prisma.workspace_threads.deleteMany({
where: clause, where: clause,
}); });
return true; return true;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment