-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add pages and page blocks (#495)
* chore: add page types and page api service * chore: add create, list, update and delete on pages * chore: add create, delete and patch page blocks * feat: add and remove pages to favorite * fix: made neccessary changes - used tailwind for hover events - add error toast alert - used partial for patch request * fix: replace absolute positiong with a flex box
- Loading branch information
1 parent
d477c19
commit 10e5ba7
Showing
13 changed files
with
1,168 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import React from "react"; | ||
|
||
import { useRouter } from "next/router"; | ||
|
||
import { mutate } from "swr"; | ||
|
||
// headless ui | ||
import { Dialog, Transition } from "@headlessui/react"; | ||
// services | ||
import pagesService from "services/pages.service"; | ||
// hooks | ||
import useToast from "hooks/use-toast"; | ||
// components | ||
import { PageForm } from "./page-form"; | ||
// types | ||
import { IPage, IPageForm } from "types"; | ||
// fetch-keys | ||
import { PAGE_LIST } from "constants/fetch-keys"; | ||
|
||
type Props = { | ||
isOpen: boolean; | ||
handleClose: () => void; | ||
data?: IPage; | ||
}; | ||
|
||
export const CreateUpdatePageModal: React.FC<Props> = ({ isOpen, handleClose, data }) => { | ||
const router = useRouter(); | ||
const { workspaceSlug, projectId } = router.query; | ||
|
||
const { setToastAlert } = useToast(); | ||
|
||
const onClose = () => { | ||
handleClose(); | ||
}; | ||
|
||
const createPage = async (payload: IPageForm) => { | ||
await pagesService | ||
.createPage(workspaceSlug as string, projectId as string, payload) | ||
.then(() => { | ||
mutate(PAGE_LIST(projectId as string)); | ||
onClose(); | ||
|
||
setToastAlert({ | ||
type: "success", | ||
title: "Success!", | ||
message: "Page created successfully.", | ||
}); | ||
}) | ||
.catch(() => { | ||
setToastAlert({ | ||
type: "error", | ||
title: "Error!", | ||
message: "Page could not be created. Please try again.", | ||
}); | ||
}); | ||
}; | ||
|
||
const updatePage = async (payload: IPageForm) => { | ||
await pagesService | ||
.patchPage(workspaceSlug as string, projectId as string, data?.id ?? "", payload) | ||
.then((res) => { | ||
mutate<IPage[]>( | ||
PAGE_LIST(projectId as string), | ||
(prevData) => | ||
prevData?.map((p) => { | ||
if (p.id === res.id) return { ...p, ...payload }; | ||
|
||
return p; | ||
}), | ||
false | ||
); | ||
onClose(); | ||
|
||
setToastAlert({ | ||
type: "success", | ||
title: "Success!", | ||
message: "Page updated successfully.", | ||
}); | ||
}) | ||
.catch(() => { | ||
setToastAlert({ | ||
type: "error", | ||
title: "Error!", | ||
message: "Page could not be updated. Please try again.", | ||
}); | ||
}); | ||
}; | ||
|
||
const handleFormSubmit = async (formData: IPageForm) => { | ||
if (!workspaceSlug || !projectId) return; | ||
|
||
if (!data) await createPage(formData); | ||
else await updatePage(formData); | ||
}; | ||
|
||
return ( | ||
<Transition.Root show={isOpen} as={React.Fragment}> | ||
<Dialog as="div" className="relative z-20" onClose={handleClose}> | ||
<Transition.Child | ||
as={React.Fragment} | ||
enter="ease-out duration-300" | ||
enterFrom="opacity-0" | ||
enterTo="opacity-100" | ||
leave="ease-in duration-200" | ||
leaveFrom="opacity-100" | ||
leaveTo="opacity-0" | ||
> | ||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> | ||
</Transition.Child> | ||
|
||
<div className="fixed inset-0 z-20 overflow-y-auto"> | ||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0"> | ||
<Transition.Child | ||
as={React.Fragment} | ||
enter="ease-out duration-300" | ||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" | ||
enterTo="opacity-100 translate-y-0 sm:scale-100" | ||
leave="ease-in duration-200" | ||
leaveFrom="opacity-100 translate-y-0 sm:scale-100" | ||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" | ||
> | ||
<Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6"> | ||
<PageForm | ||
handleFormSubmit={handleFormSubmit} | ||
handleClose={handleClose} | ||
status={data ? true : false} | ||
data={data} | ||
/> | ||
</Dialog.Panel> | ||
</Transition.Child> | ||
</div> | ||
</div> | ||
</Dialog> | ||
</Transition.Root> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import React, { useState } from "react"; | ||
// next | ||
import { useRouter } from "next/router"; | ||
// swr | ||
import { mutate } from "swr"; | ||
// headless ui | ||
import { Dialog, Transition } from "@headlessui/react"; | ||
// services | ||
import pagesService from "services/pages.service"; | ||
// hooks | ||
import useToast from "hooks/use-toast"; | ||
// ui | ||
import { DangerButton, SecondaryButton } from "components/ui"; | ||
// icons | ||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; | ||
// types | ||
import type { IPage } from "types"; | ||
type TConfirmPageDeletionProps = { | ||
isOpen: boolean; | ||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; | ||
data?: IPage; | ||
}; | ||
// fetch-keys | ||
import { PAGE_LIST } from "constants/fetch-keys"; | ||
|
||
export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = ({ | ||
isOpen, | ||
setIsOpen, | ||
data, | ||
}) => { | ||
const [isDeleteLoading, setIsDeleteLoading] = useState(false); | ||
|
||
const router = useRouter(); | ||
const { workspaceSlug } = router.query; | ||
|
||
const { setToastAlert } = useToast(); | ||
|
||
const handleClose = () => { | ||
setIsOpen(false); | ||
setIsDeleteLoading(false); | ||
}; | ||
|
||
const handleDeletion = async () => { | ||
setIsDeleteLoading(true); | ||
if (!data || !workspaceSlug) return; | ||
|
||
await pagesService | ||
.deletePage(workspaceSlug as string, data.project, data.id) | ||
.then(() => { | ||
mutate<IPage[]>( | ||
PAGE_LIST(data.project), | ||
(prevData) => prevData?.filter((page) => page.id !== data?.id), | ||
false | ||
); | ||
handleClose(); | ||
|
||
setToastAlert({ | ||
title: "Success", | ||
type: "success", | ||
message: "Page deleted successfully", | ||
}); | ||
}) | ||
.catch(() => { | ||
setToastAlert({ | ||
type: "error", | ||
title: "Error!", | ||
message: "Page could not be deleted. Please try again.", | ||
}); | ||
}) | ||
.finally(() => { | ||
setIsDeleteLoading(false); | ||
}); | ||
}; | ||
|
||
return ( | ||
<Transition.Root show={isOpen} as={React.Fragment}> | ||
<Dialog as="div" className="relative z-20" onClose={handleClose}> | ||
<Transition.Child | ||
as={React.Fragment} | ||
enter="ease-out duration-300" | ||
enterFrom="opacity-0" | ||
enterTo="opacity-100" | ||
leave="ease-in duration-200" | ||
leaveFrom="opacity-100" | ||
leaveTo="opacity-0" | ||
> | ||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> | ||
</Transition.Child> | ||
|
||
<div className="fixed inset-0 z-20 overflow-y-auto"> | ||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> | ||
<Transition.Child | ||
as={React.Fragment} | ||
enter="ease-out duration-300" | ||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" | ||
enterTo="opacity-100 translate-y-0 sm:scale-100" | ||
leave="ease-in duration-200" | ||
leaveFrom="opacity-100 translate-y-0 sm:scale-100" | ||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" | ||
> | ||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"> | ||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> | ||
<div className="sm:flex sm:items-start"> | ||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"> | ||
<ExclamationTriangleIcon | ||
className="h-6 w-6 text-red-600" | ||
aria-hidden="true" | ||
/> | ||
</div> | ||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> | ||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900"> | ||
Delete Page | ||
</Dialog.Title> | ||
<div className="mt-2"> | ||
<p className="text-sm text-gray-500"> | ||
Are you sure you want to delete Page - {`"`} | ||
<span className="italic">{data?.name}</span> | ||
{`"`} ? All of the data related to the page will be permanently removed. | ||
This action cannot be undone. | ||
</p> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<div className="flex justify-end gap-2 bg-gray-50 p-4 sm:px-6"> | ||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> | ||
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}> | ||
{isDeleteLoading ? "Deleting..." : "Delete"} | ||
</DangerButton> | ||
</div> | ||
</Dialog.Panel> | ||
</Transition.Child> | ||
</div> | ||
</div> | ||
</Dialog> | ||
</Transition.Root> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export * from "./create-update-page-modal"; | ||
export * from "./delete-page-modal"; | ||
export * from "./page-form"; | ||
export * from "./pages-list"; | ||
export * from "./single-page-list-item"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { useEffect } from "react"; | ||
import { useForm } from "react-hook-form"; | ||
// ui | ||
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; | ||
// types | ||
import { IPageForm } from "types"; | ||
|
||
type Props = { | ||
handleFormSubmit: (values: IPageForm) => Promise<void>; | ||
handleClose: () => void; | ||
status: boolean; | ||
data?: IPageForm; | ||
}; | ||
|
||
const defaultValues: IPageForm = { | ||
name: "", | ||
description: "", | ||
}; | ||
|
||
export const PageForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => { | ||
const { | ||
register, | ||
formState: { errors, isSubmitting }, | ||
handleSubmit, | ||
reset, | ||
} = useForm<IPageForm>({ | ||
defaultValues, | ||
}); | ||
|
||
const handleCreateUpdatePage = async (formData: IPageForm) => { | ||
await handleFormSubmit(formData); | ||
|
||
reset({ | ||
...defaultValues, | ||
}); | ||
}; | ||
|
||
useEffect(() => { | ||
reset({ | ||
...defaultValues, | ||
...data, | ||
}); | ||
}, [data, reset]); | ||
|
||
return ( | ||
<form onSubmit={handleSubmit(handleCreateUpdatePage)}> | ||
<div className="space-y-5"> | ||
<h3 className="text-lg font-medium leading-6 text-gray-900"> | ||
{status ? "Update" : "Create"} Page | ||
</h3> | ||
<div className="space-y-3"> | ||
<div> | ||
<Input | ||
id="name" | ||
label="Name" | ||
name="name" | ||
type="name" | ||
placeholder="Enter name" | ||
autoComplete="off" | ||
error={errors.name} | ||
register={register} | ||
validations={{ | ||
required: "Name is required", | ||
maxLength: { | ||
value: 255, | ||
message: "Name should be less than 255 characters", | ||
}, | ||
}} | ||
/> | ||
</div> | ||
<div> | ||
<TextArea | ||
id="description" | ||
name="description" | ||
label="Description" | ||
placeholder="Enter description" | ||
error={errors.description} | ||
register={register} | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
<div className="mt-5 flex justify-end gap-2"> | ||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> | ||
<PrimaryButton type="submit" loading={isSubmitting}> | ||
{status | ||
? isSubmitting | ||
? "Updating Page..." | ||
: "Update Page" | ||
: isSubmitting | ||
? "Creating Page..." | ||
: "Create Page"} | ||
</PrimaryButton> | ||
</div> | ||
</form> | ||
); | ||
}; |
Oops, something went wrong.
10e5ba7
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
plane-dev – ./apps/app
plane-dev-git-develop-caravel.vercel.app
plane-dev-caravel.vercel.app
plane-dev.vercel.app