forked from github/plane
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:
parent
89e7975821
commit
6769d1139e
@ -96,16 +96,16 @@ export const CreateBlock: React.FC<Props> = ({ user }) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<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)}
|
||||
>
|
||||
<div className="flex min-h-[75px] w-full">
|
||||
<div className="flex min-h-full w-full">
|
||||
<TextArea
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Title"
|
||||
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"
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength={255}
|
||||
@ -113,7 +113,7 @@ export const CreateBlock: React.FC<Props> = ({ user }) => {
|
||||
/>
|
||||
</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">
|
||||
<PaperAirplaneIcon className="h-5 w-5 text-custom-text-100" />
|
||||
</button>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
@ -19,6 +19,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { GptAssistantModal } from "components/core";
|
||||
import { CreateUpdateBlockInline } from "components/pages";
|
||||
import RemirrorRichTextEditor, { IRemirrorRichTextEditor } from "components/rich-text-editor";
|
||||
// ui
|
||||
import { CustomMenu, TextArea } from "components/ui";
|
||||
// icons
|
||||
@ -42,11 +43,25 @@ import { PAGE_BLOCKS_LIST } from "constants/fetch-keys";
|
||||
type Props = {
|
||||
block: IPageBlock;
|
||||
projectDetails: IProject | undefined;
|
||||
showBlockDetails: boolean;
|
||||
index: number;
|
||||
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 [createBlockForm, setCreateBlockForm] = useState(false);
|
||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||
@ -440,11 +455,21 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index,
|
||||
noPadding
|
||||
/>
|
||||
</div>
|
||||
{block?.description_stripped.length > 0 && (
|
||||
<p className="mt-3 h-5 truncate text-sm font-normal text-custom-text-200">
|
||||
{block.description_stripped}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{showBlockDetails
|
||||
? block.description_html.length > 7 && (
|
||||
<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>
|
||||
<GptAssistantModal
|
||||
|
@ -28,7 +28,7 @@ import { CreateLabelModal } from "components/labels";
|
||||
import { CreateBlock } from "components/pages/create-block";
|
||||
// ui
|
||||
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
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
@ -38,6 +38,7 @@ import {
|
||||
StarIcon,
|
||||
LinkIcon,
|
||||
XMarkIcon,
|
||||
ChevronDownIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ColorPalletteIcon, ClipboardIcon } from "components/icons";
|
||||
// helpers
|
||||
@ -46,18 +47,20 @@ import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
import { IIssueLabels, IPage, IPageBlock } from "types";
|
||||
import { IIssueLabels, IPage, IPageBlock, IProjectMember } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
PAGE_BLOCKS_LIST,
|
||||
PAGE_DETAILS,
|
||||
PROJECT_DETAILS,
|
||||
PROJECT_ISSUE_LABELS,
|
||||
USER_PROJECT_VIEW,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
const SinglePage: NextPage = () => {
|
||||
const [createBlockForm, setCreateBlockForm] = useState(false);
|
||||
const [labelModal, setLabelModal] = useState(false);
|
||||
const [showBlock, setShowBlock] = useState(false);
|
||||
|
||||
const scrollToRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -110,6 +113,13 @@ const SinglePage: NextPage = () => {
|
||||
: 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) => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
|
||||
@ -264,6 +274,43 @@ const SinglePage: NextPage = () => {
|
||||
});
|
||||
}, [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 =
|
||||
labels?.map((label) => ({
|
||||
value: label.id,
|
||||
@ -289,6 +336,11 @@ const SinglePage: NextPage = () => {
|
||||
});
|
||||
}, [reset, pageDetails]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!memberDetails) return;
|
||||
setShowBlock(memberDetails.preferences.pages.block_display);
|
||||
}, [memberDetails]);
|
||||
|
||||
return (
|
||||
<ProjectAuthorizationWrapper
|
||||
breadcrumbs={
|
||||
@ -403,6 +455,49 @@ const SinglePage: NextPage = () => {
|
||||
>
|
||||
<p className="text-sm">{render24HourFormatTime(pageDetails.updated_at)}</p>
|
||||
</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}>
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</button>
|
||||
@ -528,6 +623,7 @@ const SinglePage: NextPage = () => {
|
||||
key={block.id}
|
||||
block={block}
|
||||
projectDetails={projectDetails}
|
||||
showBlockDetails={showBlock}
|
||||
index={index}
|
||||
user={user}
|
||||
/>
|
||||
|
@ -11,6 +11,7 @@ import type {
|
||||
IProjectMember,
|
||||
IProjectMemberInvitation,
|
||||
ISearchIssueResponse,
|
||||
ProjectPreferences,
|
||||
ProjectViewTheme,
|
||||
TProjectIssuesSearchParams,
|
||||
} from "types";
|
||||
@ -252,6 +253,7 @@ class ProjectServices extends APIService {
|
||||
data: {
|
||||
view_props?: ProjectViewTheme;
|
||||
default_props?: ProjectViewTheme;
|
||||
preferences?: ProjectPreferences;
|
||||
}
|
||||
): Promise<any> {
|
||||
await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`, data)
|
||||
|
8
apps/app/types/projects.d.ts
vendored
8
apps/app/types/projects.d.ts
vendored
@ -69,6 +69,12 @@ type ProjectViewTheme = {
|
||||
filters: IIssueFilterOptions;
|
||||
};
|
||||
|
||||
type ProjectPreferences = {
|
||||
pages: {
|
||||
block_display: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export interface IProjectMember {
|
||||
id: string;
|
||||
member: IUserLite;
|
||||
@ -77,6 +83,8 @@ export interface IProjectMember {
|
||||
comment: string;
|
||||
role: 5 | 10 | 15 | 20;
|
||||
|
||||
preferences: ProjectPreferences;
|
||||
|
||||
view_props: ProjectViewTheme;
|
||||
default_props: ProjectViewTheme;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user