feat: project user preference for pages (#1673)

* feat: project user preference for pages

* feat: page block description improvement

* fix: create block input box
This commit is contained in:
Anmol Singh Bhatia 2023-07-31 11:47:22 +05:30 committed by GitHub
parent 89e7975821
commit 6769d1139e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 144 additions and 13 deletions

View File

@ -96,16 +96,16 @@ export const CreateBlock: React.FC<Props> = ({ user }) => {
return ( return (
<div className="relative"> <div className="relative">
<form <form
className="flex flex-col items-center justify-between rounded border-2 border-custom-border-200 p-2" className="relative flex flex-col items-center justify-between h-32 rounded border-2 border-custom-border-200 p-2"
onSubmit={handleSubmit(createPageBlock)} onSubmit={handleSubmit(createPageBlock)}
> >
<div className="flex min-h-[75px] w-full"> <div className="flex min-h-full w-full">
<TextArea <TextArea
id="name" id="name"
name="name" name="name"
placeholder="Title" placeholder="Title"
register={register} register={register}
className="min-h-[20px] block max-h-24 w-full resize-none overflow-hidden border-none bg-transparent px-1 py-1 text-sm font-medium" className="min-h-full block w-full resize-none overflow-hidden border-none bg-transparent px-1 py-1 text-sm font-medium"
role="textbox" role="textbox"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
maxLength={255} maxLength={255}
@ -113,7 +113,7 @@ export const CreateBlock: React.FC<Props> = ({ user }) => {
/> />
</div> </div>
<div className="flex w-full items-center justify-end gap-2 p-1"> <div className="absolute right-2 bottom-2 flex items-center p-1">
<button type="submit"> <button type="submit">
<PaperAirplaneIcon className="h-5 w-5 text-custom-text-100" /> <PaperAirplaneIcon className="h-5 w-5 text-custom-text-100" />
</button> </button>

View File

@ -1,4 +1,4 @@
import { useEffect, useState, useRef } from "react"; import React, { useEffect, useState, useRef } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
@ -19,6 +19,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components // components
import { GptAssistantModal } from "components/core"; import { GptAssistantModal } from "components/core";
import { CreateUpdateBlockInline } from "components/pages"; import { CreateUpdateBlockInline } from "components/pages";
import RemirrorRichTextEditor, { IRemirrorRichTextEditor } from "components/rich-text-editor";
// ui // ui
import { CustomMenu, TextArea } from "components/ui"; import { CustomMenu, TextArea } from "components/ui";
// icons // icons
@ -42,11 +43,25 @@ import { PAGE_BLOCKS_LIST } from "constants/fetch-keys";
type Props = { type Props = {
block: IPageBlock; block: IPageBlock;
projectDetails: IProject | undefined; projectDetails: IProject | undefined;
showBlockDetails: boolean;
index: number; index: number;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };
export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index, user }) => { const WrappedRemirrorRichTextEditor = React.forwardRef<
IRemirrorRichTextEditor,
IRemirrorRichTextEditor
>((props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />);
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
export const SinglePageBlock: React.FC<Props> = ({
block,
projectDetails,
showBlockDetails,
index,
user,
}) => {
const [isSyncing, setIsSyncing] = useState(false); const [isSyncing, setIsSyncing] = useState(false);
const [createBlockForm, setCreateBlockForm] = useState(false); const [createBlockForm, setCreateBlockForm] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
@ -440,11 +455,21 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index,
noPadding noPadding
/> />
</div> </div>
{block?.description_stripped.length > 0 && (
<p className="mt-3 h-5 truncate text-sm font-normal text-custom-text-200"> {showBlockDetails
{block.description_stripped} ? block.description_html.length > 7 && (
</p> <WrappedRemirrorRichTextEditor
)} value={block.description_html}
customClassName="text-sm"
noBorder
borderOnFocus={false}
/>
)
: block.description_stripped.length > 0 && (
<p className="mt-3 text-sm font-normal text-custom-text-200 h-5 truncate">
{block.description_stripped}
</p>
)}
</div> </div>
</div> </div>
<GptAssistantModal <GptAssistantModal

View File

@ -28,7 +28,7 @@ import { CreateLabelModal } from "components/labels";
import { CreateBlock } from "components/pages/create-block"; import { CreateBlock } from "components/pages/create-block";
// ui // ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { CustomSearchSelect, Loader, PrimaryButton, TextArea, Tooltip } from "components/ui"; import { CustomSearchSelect, Loader, TextArea, ToggleSwitch, Tooltip } from "components/ui";
// icons // icons
import { import {
ArrowLeftIcon, ArrowLeftIcon,
@ -38,6 +38,7 @@ import {
StarIcon, StarIcon,
LinkIcon, LinkIcon,
XMarkIcon, XMarkIcon,
ChevronDownIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { ColorPalletteIcon, ClipboardIcon } from "components/icons"; import { ColorPalletteIcon, ClipboardIcon } from "components/icons";
// helpers // helpers
@ -46,18 +47,20 @@ import { copyTextToClipboard } from "helpers/string.helper";
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
import { IIssueLabels, IPage, IPageBlock } from "types"; import { IIssueLabels, IPage, IPageBlock, IProjectMember } from "types";
// fetch-keys // fetch-keys
import { import {
PAGE_BLOCKS_LIST, PAGE_BLOCKS_LIST,
PAGE_DETAILS, PAGE_DETAILS,
PROJECT_DETAILS, PROJECT_DETAILS,
PROJECT_ISSUE_LABELS, PROJECT_ISSUE_LABELS,
USER_PROJECT_VIEW,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
const SinglePage: NextPage = () => { const SinglePage: NextPage = () => {
const [createBlockForm, setCreateBlockForm] = useState(false); const [createBlockForm, setCreateBlockForm] = useState(false);
const [labelModal, setLabelModal] = useState(false); const [labelModal, setLabelModal] = useState(false);
const [showBlock, setShowBlock] = useState(false);
const scrollToRef = useRef<HTMLDivElement>(null); const scrollToRef = useRef<HTMLDivElement>(null);
@ -110,6 +113,13 @@ const SinglePage: NextPage = () => {
: null : null
); );
const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
workspaceSlug && projectId
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
: null
);
const updatePage = async (formData: IPage) => { const updatePage = async (formData: IPage) => {
if (!workspaceSlug || !projectId || !pageId) return; if (!workspaceSlug || !projectId || !pageId) return;
@ -264,6 +274,43 @@ const SinglePage: NextPage = () => {
}); });
}, [setCreateBlockForm, scrollToRef]); }, [setCreateBlockForm, scrollToRef]);
const handleShowBlockToggle = async () => {
if (!workspaceSlug || !projectId) return;
const payload: Partial<IProjectMember> = {
preferences: {
pages: {
block_display: !showBlock,
},
},
};
mutate<IProjectMember>(
(workspaceSlug as string) && (projectId as string)
? USER_PROJECT_VIEW(projectId as string)
: null,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
...payload,
};
},
false
);
await projectService
.setProjectView(workspaceSlug as string, projectId as string, payload)
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again.",
});
});
};
const options = const options =
labels?.map((label) => ({ labels?.map((label) => ({
value: label.id, value: label.id,
@ -289,6 +336,11 @@ const SinglePage: NextPage = () => {
}); });
}, [reset, pageDetails]); }, [reset, pageDetails]);
useEffect(() => {
if (!memberDetails) return;
setShowBlock(memberDetails.preferences.pages.block_display);
}, [memberDetails]);
return ( return (
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
breadcrumbs={ breadcrumbs={
@ -403,6 +455,49 @@ const SinglePage: NextPage = () => {
> >
<p className="text-sm">{render24HourFormatTime(pageDetails.updated_at)}</p> <p className="text-sm">{render24HourFormatTime(pageDetails.updated_at)}</p>
</Tooltip> </Tooltip>
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center gap-2 rounded-md border border-custom-sidebar-border-200 bg-transparent px-2 py-1 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
open
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200"
}`}
>
View
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
<div className="relative divide-y-2 divide-custom-border-200">
<div className="flex items-center justify-between">
<span className="text-sm text-custom-text-200">
Show full block content
</span>
<ToggleSwitch
value={showBlock}
onChange={(value) => {
setShowBlock(value);
handleShowBlockToggle();
}}
/>
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<button className="flex items-center gap-2" onClick={handleCopyText}> <button className="flex items-center gap-2" onClick={handleCopyText}>
<LinkIcon className="h-4 w-4" /> <LinkIcon className="h-4 w-4" />
</button> </button>
@ -528,6 +623,7 @@ const SinglePage: NextPage = () => {
key={block.id} key={block.id}
block={block} block={block}
projectDetails={projectDetails} projectDetails={projectDetails}
showBlockDetails={showBlock}
index={index} index={index}
user={user} user={user}
/> />

View File

@ -11,6 +11,7 @@ import type {
IProjectMember, IProjectMember,
IProjectMemberInvitation, IProjectMemberInvitation,
ISearchIssueResponse, ISearchIssueResponse,
ProjectPreferences,
ProjectViewTheme, ProjectViewTheme,
TProjectIssuesSearchParams, TProjectIssuesSearchParams,
} from "types"; } from "types";
@ -252,6 +253,7 @@ class ProjectServices extends APIService {
data: { data: {
view_props?: ProjectViewTheme; view_props?: ProjectViewTheme;
default_props?: ProjectViewTheme; default_props?: ProjectViewTheme;
preferences?: ProjectPreferences;
} }
): Promise<any> { ): Promise<any> {
await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`, data) await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`, data)

View File

@ -69,6 +69,12 @@ type ProjectViewTheme = {
filters: IIssueFilterOptions; filters: IIssueFilterOptions;
}; };
type ProjectPreferences = {
pages: {
block_display: boolean;
};
};
export interface IProjectMember { export interface IProjectMember {
id: string; id: string;
member: IUserLite; member: IUserLite;
@ -77,6 +83,8 @@ export interface IProjectMember {
comment: string; comment: string;
role: 5 | 10 | 15 | 20; role: 5 | 10 | 15 | 20;
preferences: ProjectPreferences;
view_props: ProjectViewTheme; view_props: ProjectViewTheme;
default_props: ProjectViewTheme; default_props: ProjectViewTheme;