Skip to content

Commit

Permalink
🚀 worxpace 6 - Sidebar action: archive/restore/delete
Browse files Browse the repository at this point in the history
  • Loading branch information
steeeee0223 committed Jan 26, 2024
1 parent 2711d10 commit 6b83243
Show file tree
Hide file tree
Showing 15 changed files with 468 additions and 13 deletions.
38 changes: 38 additions & 0 deletions apps/worxpace/src/actions/archive-document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use server";

import { revalidatePath } from "next/cache";

import type { Document } from "@acme/prisma";
import {
createSafeAction,
type ActionHandler,
type Modified,
} from "@acme/ui/lib";
import { DeleteDocument, type DeleteDocumentInput } from "@acme/validators";

import { archive, createAuditLog, fetchClient, UnauthorizedError } from "~/lib";

const handler: ActionHandler<DeleteDocumentInput, Modified<Document>> = async (
data,
) => {
let result;

try {
const { userId, orgId } = fetchClient();
result = await archive({ ...data, userId, orgId });
/** Activity Log */
await createAuditLog(
{ title: result.item.title, entityId: data.id, type: "DOCUMENT" },
"DELETE",
);
} catch (error) {
if (error instanceof UnauthorizedError) return { error: "Unauthorized" };
console.log(`ERROR`, error);
return { error: "Failed to archive document." };
}

revalidatePath(`/documents/${data.id}`);
return { data: result };
};

export const archiveDocument = createSafeAction(DeleteDocument, handler);
42 changes: 42 additions & 0 deletions apps/worxpace/src/actions/delete-document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use server";

import { revalidatePath } from "next/cache";

import type { Document } from "@acme/prisma";
import {
createSafeAction,
type ActionHandler,
type Modified,
} from "@acme/ui/lib";
import { DeleteDocument, type DeleteDocumentInput } from "@acme/validators";

import { createAuditLog, fetchClient, remove, UnauthorizedError } from "~/lib";

const handler: ActionHandler<DeleteDocumentInput, Modified<Document>> = async (
data,
) => {
let result;

try {
const { userId, orgId } = fetchClient();
result = await remove({ userId, orgId, ...data });
/** Activity Log */
await createAuditLog(
{
title: result.item.title,
entityId: data.id,
type: "DOCUMENT",
},
"DELETE",
);
} catch (error) {
if (error instanceof UnauthorizedError) return { error: "Unauthorized" };
console.log(`ERROR`, error);
return { error: "Failed to delete document." };
}

revalidatePath(`/documents`);
return { data: result };
};

export const deleteDocument = createSafeAction(DeleteDocument, handler);
5 changes: 4 additions & 1 deletion apps/worxpace/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from "./create-document"
export * from "./create-document";
export * from "./archive-document";
export * from "./restore-document";
export * from "./delete-document";
42 changes: 42 additions & 0 deletions apps/worxpace/src/actions/restore-document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use server";

import { revalidatePath } from "next/cache";

import type { Document } from "@acme/prisma";
import {
createSafeAction,
type ActionHandler,
type Modified,
} from "@acme/ui/lib";
import { DeleteDocument, type DeleteDocumentInput } from "@acme/validators";

import { createAuditLog, fetchClient, restore, UnauthorizedError } from "~/lib";

const handler: ActionHandler<DeleteDocumentInput, Modified<Document>> = async (
data,
) => {
let result;

try {
const { userId, orgId } = fetchClient();
result = await restore({ ...data, userId, orgId });
/** Activity Log */
await createAuditLog(
{
title: result.item.title,
entityId: data.id,
type: "DOCUMENT",
},
"UPDATE",
);
} catch (error) {
if (error instanceof UnauthorizedError) return { error: "Unauthorized" };
console.log(`ERROR`, error);
return { error: "Failed to restore document." };
}

revalidatePath(`/documents/${data.id}`);
return { data: result };
};

export const restoreDocument = createSafeAction(DeleteDocument, handler);
38 changes: 32 additions & 6 deletions apps/worxpace/src/app/(platform)/(tools)/_components/doc-list.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import { useRouter } from "next/navigation";
import { OrganizationSwitcher } from "@clerk/nextjs";
import { PlusCircle } from "lucide-react";
import { PlusCircle, Trash } from "lucide-react";
import { toast } from "sonner";

import {
CRUDItem as Item,
Popover,
PopoverContent,
PopoverTrigger,
TreeList,
useTree,
useTreeAction,
} from "@acme/ui/components";
import { useAction } from "@acme/ui/hooks";

import { createDocument } from "~/actions";
import { archiveDocument, createDocument } from "~/actions";
import TrashBox from "./trash-box";

interface DocListProps {
isMobile?: boolean;
}

const DocList = ({ isMobile: _isMobile }: DocListProps) => {
const _router = useRouter();
const DocList = ({ isMobile }: DocListProps) => {
const router = useRouter();
/** Docs */
const { isLoading } = useTree();
const { dispatch } = useTreeAction();
const onError = (e: string) => toast.error(e);
/** Create */
/** Action: Create */
const { execute: create } = useAction(createDocument, {
onSuccess: (data) => {
const { id, title, parentId, isArchived, icon } = data;
Expand All @@ -32,7 +36,17 @@ const DocList = ({ isMobile: _isMobile }: DocListProps) => {
payload: [{ id, title, parentId, isArchived, icon }],
});
toast.success(`Document Created: ${title}`);
// router.push(`/documents/${id}`);
router.push(`/documents/${id}`);
},
onError,
});
/** Action: Archive */
const { execute: archive } = useAction(archiveDocument, {
onSuccess: (data) => {
console.log(`archived`, data);
dispatch({ type: "archive", payload: data });
toast.success(`Document "${data.item.title}" Moved to Trash`);
// router.push(`/documents`);
},
onError,
});
Expand Down Expand Up @@ -84,8 +98,20 @@ const DocList = ({ isMobile: _isMobile }: DocListProps) => {
<TreeList
parentId={null}
onAddItem={(parentId) => create({ title: "Untitled", parentId })}
onDeleteItem={(id) => archive({ id })}
/>
)}
<Popover>
<PopoverTrigger className="mt-4 w-full">
<Item label="Trash" icon={Trash} />
</PopoverTrigger>
<PopoverContent
className="z-[99999] w-72 p-0"
side={isMobile ? "bottom" : "right"}
>
<TrashBox />
</PopoverContent>
</Popover>
</div>
</>
);
Expand Down
118 changes: 118 additions & 0 deletions apps/worxpace/src/app/(platform)/(tools)/_components/trash-box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/* eslint-disable jsx-a11y/interactive-supports-focus */
/* eslint-disable jsx-a11y/click-events-have-key-events */
"use client";

import { MouseEvent, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Search, Trash, Undo } from "lucide-react";
import { toast } from "sonner";

import { Input, Spinner, useTree, useTreeAction } from "@acme/ui/components";
import { useAction } from "@acme/ui/hooks";
import { cn } from "@acme/ui/lib";

import { deleteDocument, restoreDocument } from "~/actions";
import { ConfirmModal } from "~/components";
import { theme } from "~/constants/theme";

const TrashBox = () => {
const router = useRouter();
const params = useParams();
/** Tree */
const { archivedItems: archivedDocs } = useTree();
const [search, setSearch] = useState("");
const filteredDocuments = archivedDocs.filter(({ title }) =>
title.toLowerCase().includes(search.toLowerCase()),
);
/** Action */
const { dispatch } = useTreeAction();
const onClick = (documentId: string) =>
router.push(`/documents/${documentId}`);
const onError = (e: string) => toast.error(e);
/** Action: Restore */
const { execute: restore } = useAction(restoreDocument, {
onSuccess: (data) => {
dispatch({ type: "restore", payload: data });
toast.success(`Restored document "${data.item.title}"`);
},
onError,
});
const onRestore = (e: MouseEvent<HTMLDivElement>, documentId: string) => {
e.stopPropagation();
restore({ id: documentId })
.then(() => console.log(`processing restore`))
.catch((e) => console.log(e));
};
/** Action: Remove */
const { execute: remove } = useAction(deleteDocument, {
onSuccess: (data) => {
dispatch({ type: "delete", payload: data.ids });
toast.success(`Deleted document "${data.item.title}"`);
if (params.documentId === data.item.id)
console.log(`redirect to user/org page`);
// router.push(`/documents`);
},
onError,
});

if (archivedDocs === undefined)
return (
<div className={cn(theme.flex.center, "h-full justify-center p-4")}>
<Spinner size="lg" />
</div>
);
return (
<div className="text-sm">
<div className={cn(theme.flex.gap1, "p-2")}>
<Search className={cn(theme.size.icon, "mr-2")} />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
className="h-7 bg-secondary px-2 focus-visible:ring-transparent"
placeholder="Filter by page title..."
/>
</div>
<div className="mt-2 px-1 pb-1">
<p className="hidden pb-2 text-center text-xs text-muted-foreground last:block">
No documents found.
</p>
{filteredDocuments?.map(({ id, title }) => (
<div
key={id}
role="button"
onClick={() => onClick(id)}
className={cn(
theme.flex.center,
"w-full justify-between rounded-sm text-sm text-primary hover:bg-primary/5",
)}
>
<span className="truncate pl-2">{title}</span>
<div className={cn(theme.flex.gap1, "p-1")}>
<div
onClick={(e) => onRestore(e, id)}
role="button"
className={cn(theme.bg.hover, "rounded-sm p-1")}
>
<Undo
className={cn(theme.size.icon, "text-muted-foreground")}
/>
</div>
<ConfirmModal onConfirm={() => remove({ id })}>
<div
role="button"
className={cn(theme.bg.hover, "rounded-sm p-1")}
>
<Trash
className={cn(theme.size.icon, "text-muted-foreground")}
/>
</div>
</ConfirmModal>
</div>
</div>
))}
</div>
</div>
);
};

export default TrashBox;
2 changes: 1 addition & 1 deletion apps/worxpace/src/app/(platform)/(tools)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const ToolsLayout = ({ children }: PropsWithChildren) => {
"use server";
try {
const { userId, orgId } = fetchClient();
const documents = await fetchDocuments(userId, orgId, false);
const documents = await fetchDocuments(userId, orgId);
console.log(`docs:`, documents);
const data = documents.map(({ id, title, parentId, isArchived }) => ({
id,
Expand Down
1 change: 1 addition & 0 deletions apps/worxpace/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./modal";
49 changes: 49 additions & 0 deletions apps/worxpace/src/components/modal/confirm-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { MouseEvent, ReactNode } from "react";

import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@acme/ui/components";

interface ConfirmModalProps {
children: ReactNode;
onConfirm: () => void;
}

export const ConfirmModal = ({ children, onConfirm }: ConfirmModalProps) => {
const handleConfirm = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onConfirm();
};

return (
<AlertDialog>
<AlertDialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{children}
</AlertDialogTrigger>
<AlertDialogContent className="z-[99999]">
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={(e) => e.stopPropagation()}>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm}>Confirm</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
1 change: 1 addition & 0 deletions apps/worxpace/src/components/modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./confirm-modal";
Loading

0 comments on commit 6b83243

Please sign in to comment.