mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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 (
|
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>
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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)
|
||||||
|
8
apps/app/types/projects.d.ts
vendored
8
apps/app/types/projects.d.ts
vendored
@ -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;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user