refactor: pages folder structure (#544)

* refactor: pages folder structure, mutation issues

* fix: block edit and push

* fix: block title placeholder
This commit is contained in:
Aaryan Khandelwal 2023-03-27 23:19:05 +05:30 committed by GitHub
parent e13b679c28
commit 3503b22dd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 378 additions and 192 deletions

View File

@ -15,7 +15,12 @@ import { PageForm } from "./page-form";
// types
import { IPage } from "types";
// fetch-keys
import { RECENT_PAGES_LIST } from "constants/fetch-keys";
import {
ALL_PAGES_LIST,
FAVORITE_PAGES_LIST,
MY_PAGES_LIST,
RECENT_PAGES_LIST,
} from "constants/fetch-keys";
type Props = {
isOpen: boolean;
@ -36,8 +41,18 @@ export const CreateUpdatePageModal: React.FC<Props> = ({ isOpen, handleClose, da
const createPage = async (payload: IPage) => {
await pagesService
.createPage(workspaceSlug as string, projectId as string, payload)
.then(() => {
.then((res) => {
mutate(RECENT_PAGES_LIST(projectId as string));
mutate<IPage[]>(
MY_PAGES_LIST(projectId as string),
(prevData) => [res, ...(prevData as IPage[])],
false
);
mutate<IPage[]>(
ALL_PAGES_LIST(projectId as string),
(prevData) => [res, ...(prevData as IPage[])],
false
);
onClose();
setToastAlert({
@ -58,8 +73,38 @@ export const CreateUpdatePageModal: React.FC<Props> = ({ isOpen, handleClose, da
const updatePage = async (payload: IPage) => {
await pagesService
.patchPage(workspaceSlug as string, projectId as string, data?.id ?? "", payload)
.then(() => {
.then((res) => {
mutate(RECENT_PAGES_LIST(projectId as string));
mutate<IPage[]>(
FAVORITE_PAGES_LIST(projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === res.id) return { ...p, ...res };
return p;
}),
false
);
mutate<IPage[]>(
MY_PAGES_LIST(projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === res.id) return { ...p, ...res };
return p;
}),
false
);
mutate<IPage[]>(
ALL_PAGES_LIST(projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === res.id) return { ...p, ...res };
return p;
}),
false
);
onClose();
setToastAlert({

View File

@ -1,12 +1,8 @@
export * from "./all-pages-list";
export * from "./pages-list";
export * from "./create-update-page-modal";
export * from "./delete-page-modal";
export * from "./favorite-pages-list";
export * from "./my-pages-list";
export * from "./other-pages-list";
export * from "./page-form";
export * from "./pages-view";
export * from "./recent-pages-list";
export * from "./single-page-block";
export * from "./single-page-detailed-item";
export * from "./single-page-list-item";

View File

@ -1,7 +1,11 @@
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import dynamic from "next/dynamic";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// ui
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
import { Input, Loader, PrimaryButton, SecondaryButton } from "components/ui";
// types
import { IPage } from "types";
@ -12,6 +16,16 @@ type Props = {
data?: IPage | null;
};
// rich-text-editor
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
loading: () => (
<Loader>
<Loader.Item height="12rem" width="100%" />
</Loader>
),
});
const defaultValues = {
name: "",
description: "",
@ -23,6 +37,8 @@ export const PageForm: React.FC<Props> = ({ handleFormSubmit, handleClose, statu
formState: { errors, isSubmitting },
handleSubmit,
reset,
control,
setValue,
} = useForm<IPage>({
defaultValues,
});
@ -68,16 +84,20 @@ export const PageForm: React.FC<Props> = ({ handleFormSubmit, handleClose, statu
}}
/>
</div>
<div>
<TextArea
id="description"
{/* <div>
<Controller
name="description"
label="Description"
placeholder="Enter description"
error={errors.description}
register={register}
control={control}
render={({ field: { value } }) => (
<RemirrorRichTextEditor
value={value}
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
placeholder="Description"
/>
)}
/>
</div>
</div> */}
</div>
</div>
<div className="mt-5 flex justify-end gap-2">

View File

@ -7,15 +7,11 @@ import pagesService from "services/pages.service";
// components
import { PagesView } from "components/pages";
// types
import { TPageViewProps } from "types";
import { TPagesListProps } from "./types";
// fetch-keys
import { ALL_PAGES_LIST } from "constants/fetch-keys";
type Props = {
viewType: TPageViewProps;
};
export const AllPagesList: React.FC<Props> = ({ viewType }) => {
export const AllPagesList: React.FC<TPagesListProps> = ({ viewType }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;

View File

@ -7,15 +7,11 @@ import pagesService from "services/pages.service";
// components
import { PagesView } from "components/pages";
// types
import { TPageViewProps } from "types";
import { TPagesListProps } from "./types";
// fetch-keys
import { FAVORITE_PAGES_LIST } from "constants/fetch-keys";
type Props = {
viewType: TPageViewProps;
};
export const FavoritePagesList: React.FC<Props> = ({ viewType }) => {
export const FavoritePagesList: React.FC<TPagesListProps> = ({ viewType }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;

View File

@ -0,0 +1,6 @@
export * from "./all-pages-list";
export * from "./favorite-pages-list";
export * from "./my-pages-list";
export * from "./other-pages-list";
export * from "./recent-pages-list";
export * from "./types";

View File

@ -7,15 +7,11 @@ import pagesService from "services/pages.service";
// components
import { PagesView } from "components/pages";
// types
import { TPageViewProps } from "types";
import { TPagesListProps } from "./types";
// fetch-keys
import { MY_PAGES_LIST } from "constants/fetch-keys";
type Props = {
viewType: TPageViewProps;
};
export const MyPagesList: React.FC<Props> = ({ viewType }) => {
export const MyPagesList: React.FC<TPagesListProps> = ({ viewType }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;

View File

@ -7,15 +7,11 @@ import pagesService from "services/pages.service";
// components
import { PagesView } from "components/pages";
// types
import { TPageViewProps } from "types";
import { TPagesListProps } from "./types";
// fetch-keys
import { OTHER_PAGES_LIST } from "constants/fetch-keys";
type Props = {
viewType: TPageViewProps;
};
export const OtherPagesList: React.FC<Props> = ({ viewType }) => {
export const OtherPagesList: React.FC<TPagesListProps> = ({ viewType }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;

View File

@ -0,0 +1,62 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import pagesService from "services/pages.service";
// components
import { PagesView } from "components/pages";
// ui
import { Loader } from "components/ui";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { TPagesListProps } from "./types";
import { RecentPagesResponse } from "types";
// fetch-keys
import { RECENT_PAGES_LIST } from "constants/fetch-keys";
export const RecentPagesList: React.FC<TPagesListProps> = ({ viewType }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: pages } = useSWR(
workspaceSlug && projectId ? RECENT_PAGES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => pagesService.getRecentPages(workspaceSlug as string, projectId as string)
: null
);
return (
<>
{pages ? (
Object.keys(pages).length > 0 ? (
<div className="mt-4 space-y-4">
{Object.keys(pages).map((key) => {
if (pages[key].length === 0) return null;
return (
<React.Fragment key={key}>
<h2 className="text-xl font-medium capitalize">
{replaceUnderscoreIfSnakeCase(key)}
</h2>
<PagesView pages={pages[key as keyof RecentPagesResponse]} viewType={viewType} />
</React.Fragment>
);
})}
</div>
) : (
<p className="mt-4 text-center">No issues found</p>
)
) : (
<Loader className="mt-8 space-y-4">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</>
);
};

View File

@ -0,0 +1,5 @@
import { TPageViewProps } from "types";
export type TPagesListProps = {
viewType: TPageViewProps;
};

View File

@ -19,6 +19,13 @@ import { EmptyState, Loader } from "components/ui";
import emptyPage from "public/empty-state/empty-page.svg";
// types
import { IPage, TPageViewProps } from "types";
import {
ALL_PAGES_LIST,
FAVORITE_PAGES_LIST,
MY_PAGES_LIST,
RECENT_PAGES_LIST,
} from "constants/fetch-keys";
import { mutate } from "swr";
type Props = {
pages: IPage[] | undefined;
@ -50,6 +57,33 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
const handleAddToFavorites = (page: IPage) => {
if (!workspaceSlug || !projectId) return;
mutate(RECENT_PAGES_LIST(projectId as string));
mutate<IPage[]>(
ALL_PAGES_LIST(projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === page.id) p.is_favorite = true;
return p;
}),
false
);
mutate<IPage[]>(
MY_PAGES_LIST(projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === page.id) p.is_favorite = true;
return p;
}),
false
);
mutate<IPage[]>(
FAVORITE_PAGES_LIST(projectId as string),
(prevData) => [page, ...(prevData as IPage[])],
false
);
pagesService
.addPageToFavorites(workspaceSlug as string, projectId as string, {
page: page.id,
@ -73,6 +107,33 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
const handleRemoveFromFavorites = (page: IPage) => {
if (!workspaceSlug || !projectId) return;
mutate(RECENT_PAGES_LIST(projectId as string));
mutate<IPage[]>(
ALL_PAGES_LIST(projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === page.id) p.is_favorite = false;
return p;
}),
false
);
mutate<IPage[]>(
MY_PAGES_LIST(projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === page.id) p.is_favorite = false;
return p;
}),
false
);
mutate<IPage[]>(
FAVORITE_PAGES_LIST(projectId as string),
(prevData) => (prevData ?? []).filter((p) => p.id !== page.id),
false
);
pagesService
.removePageFromFavorites(workspaceSlug as string, projectId as string, page.id)
.then(() => {

View File

@ -1,46 +0,0 @@
import React from "react";
// components
import { PagesView } from "components/pages";
// ui
import { Loader } from "components/ui";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { RecentPagesResponse, TPageViewProps } from "types";
type Props = {
pages: RecentPagesResponse | undefined;
viewType: TPageViewProps;
};
export const RecentPagesList: React.FC<Props> = ({ pages, viewType }) => (
<>
{pages ? (
Object.keys(pages).length > 0 ? (
<div className="mt-4 space-y-4">
{Object.keys(pages).map((key) => {
if (pages[key].length === 0) return null;
return (
<React.Fragment key={key}>
<h2 className="text-xl font-medium capitalize">
{replaceUnderscoreIfSnakeCase(key)}
</h2>
<PagesView pages={pages[key as keyof RecentPagesResponse]} viewType={viewType} />
</React.Fragment>
);
})}
</div>
) : (
<p className="mt-4 text-center">No issues found</p>
)
) : (
<Loader className="mt-8 space-y-4">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</>
);

View File

@ -34,7 +34,7 @@ type Props = {
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
loading: () => (
<Loader>
<Loader className="mx-4 mt-6">
<Loader.Item height="100px" width="100%" />
</Loader>
),
@ -149,7 +149,20 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId) return;
setValue("description", {});
setValue("description", {
type: "doc",
content: [
{
type: "paragraph",
content: [
{
text: response,
type: "text",
},
],
},
],
});
setValue("description_html", `<p>${response}</p>`);
handleSubmit(updatePageBlock)()
.then(() => {
@ -204,7 +217,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
<TextArea
id="name"
name="name"
placeholder="Enter issue name"
placeholder="Enter block title"
value={watch("name")}
onBlur={handleSubmit(updatePageBlock)}
onChange={(e) => setValue("name", e.target.value)}

View File

@ -34,72 +34,99 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
return (
<li>
<div className="relative rounded p-4 hover:bg-gray-100">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<PencilScribbleIcon />
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`}>
<a>
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`}>
<a>
<div className="relative rounded p-4 hover:bg-gray-100">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<PencilScribbleIcon />
<p className="mr-2 truncate text-sm font-medium">{truncateText(page.name, 75)}</p>
</a>
</Link>
{page.label_details.length > 0 &&
page.label_details.map((label) => (
<div
key={label.id}
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
style={{
backgroundColor: `${
label?.color && label.color !== "" ? label.color : "#000000"
}20`,
}}
>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000000",
}}
/>
{label.name}
{page.label_details.length > 0 &&
page.label_details.map((label) => (
<div
key={label.id}
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
style={{
backgroundColor: `${
label?.color && label.color !== "" ? label.color : "#000000"
}20`,
}}
>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor:
label?.color && label.color !== "" ? label.color : "#000000",
}}
/>
{label.name}
</div>
))}
</div>
<div className="ml-2 flex flex-shrink-0">
<div className="flex items-center gap-2">
<Tooltip
tooltipContent={`Last updated at ${renderShortTime(
page.updated_at
)} ${renderShortDate(page.updated_at)}`}
>
<p className="text-sm text-gray-400">{renderShortTime(page.updated_at)}</p>
</Tooltip>
{page.is_favorite ? (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemoveFromFavorites();
}}
>
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
</button>
) : (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleAddToFavorites();
}}
>
<StarIcon className="h-4 w-4 " color="#858e96" />
</button>
)}
<CustomMenu width="auto" verticalEllipsis>
<CustomMenu.MenuItem
onClick={(e: any) => {
e.preventDefault();
e.stopPropagation();
handleEditPage();
}}
>
<span className="flex items-center justify-start gap-2 text-gray-800">
<PencilIcon className="h-3.5 w-3.5" />
<span>Edit Page</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e: any) => {
e.preventDefault();
e.stopPropagation();
handleDeletePage();
}}
>
<span className="flex items-center justify-start gap-2 text-gray-800">
<TrashIcon className="h-3.5 w-3.5" />
<span>Delete Page</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
))}
</div>
<div className="ml-2 flex flex-shrink-0">
<div className="flex items-center gap-2">
<Tooltip
tooltipContent={`Last updated at ${renderShortTime(
page.updated_at
)} ${renderShortDate(page.updated_at)}`}
>
<p className="text-sm text-gray-400">{renderShortTime(page.updated_at)}</p>
</Tooltip>
{page.is_favorite ? (
<button onClick={handleRemoveFromFavorites}>
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
</button>
) : (
<button onClick={handleAddToFavorites} type="button">
<StarIcon className="h-4 w-4 " color="#858e96" />
</button>
)}
<CustomMenu width="auto" verticalEllipsis>
<CustomMenu.MenuItem onClick={handleEditPage}>
<span className="flex items-center justify-start gap-2 text-gray-800">
<PencilIcon className="h-3.5 w-3.5" />
<span>Edit Page</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleDeletePage}>
<span className="flex items-center justify-start gap-2 text-gray-800">
<TrashIcon className="h-3.5 w-3.5" />
<span>Delete Page</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
</div>
</div>
</div>
</a>
</Link>
</li>
);
};

View File

@ -25,7 +25,7 @@ type MenuItemProps = {
children: JSX.Element | string;
renderAs?: "button" | "a";
href?: string;
onClick?: () => void;
onClick?: (args?: any) => void;
className?: string;
};

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
@ -43,6 +43,8 @@ import {
} from "constants/fetch-keys";
const SinglePage: NextPage<UserAuth> = (props) => {
const [isAddingBlock, setIsAddingBlock] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, pageId } = router.query;
@ -132,6 +134,8 @@ const SinglePage: NextPage<UserAuth> = (props) => {
const createPageBlock = async () => {
if (!workspaceSlug || !projectId || !pageId) return;
setIsAddingBlock(true);
await pagesService
.createPageBlock(workspaceSlug as string, projectId as string, pageId as string, {
name: "New block",
@ -149,6 +153,9 @@ const SinglePage: NextPage<UserAuth> = (props) => {
title: "Error!",
message: "Page could not be created. Please try again.",
});
})
.finally(() => {
setIsAddingBlock(false);
});
};
@ -392,17 +399,8 @@ const SinglePage: NextPage<UserAuth> = (props) => {
</div>
<div className="px-3">
{pageBlocks ? (
pageBlocks.length === 0 ? (
<button
type="button"
className="flex items-center gap-1 rounded px-2.5 py-1 text-xs hover:bg-gray-100"
onClick={createPageBlock}
>
<PlusIcon className="h-3 w-3" />
Add new block
</button>
) : (
<>
<>
{pageBlocks.length !== 0 && (
<div className="space-y-4">
{pageBlocks.map((block) => (
<SinglePageBlock
@ -412,18 +410,23 @@ const SinglePage: NextPage<UserAuth> = (props) => {
/>
))}
</div>
<div className="">
<button
type="button"
className="flex items-center gap-1 rounded px-2.5 py-1 text-xs hover:bg-gray-100"
onClick={createPageBlock}
>
)}
<button
type="button"
className="flex items-center gap-1 rounded px-2.5 py-1 text-xs hover:bg-gray-100"
onClick={createPageBlock}
disabled={isAddingBlock}
>
{isAddingBlock ? (
"Adding block..."
) : (
<>
<PlusIcon className="h-3 w-3" />
Add new block
</button>
</div>
</>
)
</>
)}
</button>
</>
) : (
<Loader>
<Loader.Item height="150px" />

View File

@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import type { GetServerSidePropsContext, NextPage } from "next";
import dynamic from "next/dynamic";
import useSWR from "swr";
import useSWR, { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
@ -22,7 +22,7 @@ import { PlusIcon } from "components/icons";
// layouts
import AppLayout from "layouts/app-layout";
// components
import { RecentPagesList, CreateUpdatePageModal } from "components/pages";
import { RecentPagesList, CreateUpdatePageModal, TPagesListProps } from "components/pages";
// ui
import { HeaderButton, Input, PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -31,30 +31,35 @@ import { ListBulletIcon, RectangleGroupIcon } from "@heroicons/react/20/solid";
// types
import { IPage, TPageViewProps, UserAuth } from "types";
// fetch-keys
import { PROJECT_DETAILS, RECENT_PAGES_LIST } from "constants/fetch-keys";
import {
ALL_PAGES_LIST,
MY_PAGES_LIST,
PROJECT_DETAILS,
RECENT_PAGES_LIST,
} from "constants/fetch-keys";
const AllPagesList = dynamic<{ viewType: TPageViewProps }>(
const AllPagesList = dynamic<TPagesListProps>(
() => import("components/pages").then((a) => a.AllPagesList),
{
ssr: false,
}
);
const FavoritePagesList = dynamic<{ viewType: TPageViewProps }>(
const FavoritePagesList = dynamic<TPagesListProps>(
() => import("components/pages").then((a) => a.FavoritePagesList),
{
ssr: false,
}
);
const MyPagesList = dynamic<{ viewType: TPageViewProps }>(
const MyPagesList = dynamic<TPagesListProps>(
() => import("components/pages").then((a) => a.MyPagesList),
{
ssr: false,
}
);
const OtherPagesList = dynamic<{ viewType: TPageViewProps }>(
const OtherPagesList = dynamic<TPagesListProps>(
() => import("components/pages").then((a) => a.OtherPagesList),
{
ssr: false,
@ -90,13 +95,6 @@ const ProjectPages: NextPage<UserAuth> = (props) => {
: null
);
const { data: recentPages } = useSWR(
workspaceSlug && projectId ? RECENT_PAGES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => pagesService.getRecentPages(workspaceSlug as string, projectId as string)
: null
);
const createPage = async (formData: Partial<IPage>) => {
if (!workspaceSlug || !projectId) return;
@ -111,13 +109,25 @@ const ProjectPages: NextPage<UserAuth> = (props) => {
await pagesService
.createPage(workspaceSlug as string, projectId as string, formData)
.then(() => {
.then((res) => {
setToastAlert({
type: "success",
title: "Success!",
message: "Page created successfully.",
});
reset();
mutate(RECENT_PAGES_LIST(projectId as string));
mutate<IPage[]>(
MY_PAGES_LIST(projectId as string),
(prevData) => [res, ...(prevData as IPage[])],
false
);
mutate<IPage[]>(
ALL_PAGES_LIST(projectId as string),
(prevData) => [res, ...(prevData as IPage[])],
false
);
})
.catch(() => {
setToastAlert({
@ -224,7 +234,7 @@ const ProjectPages: NextPage<UserAuth> = (props) => {
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<RecentPagesList pages={recentPages} viewType={viewType} />
<RecentPagesList viewType={viewType} />
</Tab.Panel>
<Tab.Panel>
<AllPagesList viewType={viewType} />