Skip to content

Commit

Permalink
🔄 Merge pull request #12 from steeeee0223/feature/worxpace
Browse files Browse the repository at this point in the history
Feature/worxpace: implement sidebar CRUD actions
  • Loading branch information
steeeee0223 authored Jan 28, 2024
2 parents 3ab2d06 + 6b83243 commit 4ca1da4
Show file tree
Hide file tree
Showing 19 changed files with 534 additions and 13 deletions.
1 change: 1 addition & 0 deletions apps/worxpace/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"next": "^14.0.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"sonner": "^1.3.1",
"superjson": "2.2.1",
"zod": "^3.22.4"
},
Expand Down
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);
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Image from "next/image";
import { useOrganization, useUser } from "@clerk/nextjs";
import { PlusCircle } from "lucide-react";
import { toast } from "sonner";

import { Button, useTreeAction } from "@acme/ui/components";
import { useAction } from "@acme/ui/hooks";
Expand All @@ -23,11 +24,14 @@ const Client = ({ params: { role } }: Params) => {
const { dispatch } = useTreeAction();
const { execute } = useAction(createDocument, {
onSuccess: (data) => {
const { id, title, isArchived, parentId } = data;
console.log(`Document created: ${title}`);
dispatch({ type: "add", payload: [{ id, title, isArchived, parentId }] });
const { id, title, isArchived, parentId, icon } = data;
toast.success(`Document created: ${title}`);
dispatch({
type: "add",
payload: [{ id, title, isArchived, parentId, icon }],
});
},
onError: (e) => console.log(e),
onError: (e) => toast.error(e),
});
const onSubmit = () => {
execute({ title: "Untitled", parentId: undefined })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,56 @@
import { useRouter } from "next/navigation";
import { OrganizationSwitcher } from "@clerk/nextjs";
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 { archiveDocument, createDocument } from "~/actions";
import TrashBox from "./trash-box";

interface DocListProps {
isMobile?: boolean;
}

const DocList = ({ isMobile }: DocListProps) => {
const router = useRouter();
/** Docs */
const { isLoading } = useTree();
const { dispatch } = useTreeAction();
const onError = (e: string) => toast.error(e);
/** Action: Create */
const { execute: create } = useAction(createDocument, {
onSuccess: (data) => {
const { id, title, parentId, isArchived, icon } = data;
dispatch({
type: "add",
payload: [{ id, title, parentId, isArchived, icon }],
});
toast.success(`Document Created: ${title}`);
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,
});

return (
<>
<div>
Expand Down Expand Up @@ -33,8 +79,40 @@ const DocList = ({ isMobile }: DocListProps) => {
},
}}
/>
<Item
label="New page"
icon={PlusCircle}
onClick={() => create({ title: "Untitled" })}
/>
</div>
<div className="mt-4">
{isLoading ? (
<>
<div className="mt-4">
{Array.from([0, 1, 0, 1, 1]).map((level, i) => (
<Item.Skeleton key={i} level={level} />
))}
</div>
</>
) : (
<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>
<div className="mt-4">Doc Items</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
Loading

0 comments on commit 4ca1da4

Please sign in to comment.