forked from github/plane
chore: minor fixes on pages (#557)
* feat: block sync * chore: minor fixes on pages * fix: remove dangerously set inner html * fix: pages crud operations mutation * fix: favorites mutation for recent pages * fix: remove dangerously set inner html
This commit is contained in:
parent
c0a471e916
commit
b654d30aeb
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@ -16,6 +17,7 @@ type Props = {
|
|||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
inset?: string;
|
inset?: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
htmlContent?: string;
|
||||||
onResponse: (response: string) => void;
|
onResponse: (response: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,11 +26,16 @@ type FormData = {
|
|||||||
task: string;
|
task: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
export const GptAssistantModal: React.FC<Props> = ({
|
export const GptAssistantModal: React.FC<Props> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
handleClose,
|
handleClose,
|
||||||
inset = "top-0 left-0",
|
inset = "top-0 left-0",
|
||||||
content,
|
content,
|
||||||
|
htmlContent,
|
||||||
onResponse,
|
onResponse,
|
||||||
}) => {
|
}) => {
|
||||||
const [response, setResponse] = useState("");
|
const [response, setResponse] = useState("");
|
||||||
@ -62,15 +69,6 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
const handleResponse = async (formData: FormData) => {
|
const handleResponse = async (formData: FormData) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
if (!content || content === "") {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Please enter some description to get AI assistance.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.task === "") {
|
if (formData.task === "") {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -82,11 +80,11 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
await aiService
|
await aiService
|
||||||
.createGptTask(workspaceSlug as string, projectId as string, {
|
.createGptTask(workspaceSlug as string, projectId as string, {
|
||||||
prompt: content,
|
prompt: content && content !== "" ? content : "",
|
||||||
task: formData.task,
|
task: formData.task,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setResponse(res.response);
|
setResponse(res.response_html);
|
||||||
setFocus("task");
|
setFocus("task");
|
||||||
|
|
||||||
if (res.response === "") setInvalidResponse(true);
|
if (res.response === "") setInvalidResponse(true);
|
||||||
@ -105,12 +103,28 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(handleResponse)} className="space-y-4">
|
<form onSubmit={handleSubmit(handleResponse)} className="space-y-4">
|
||||||
|
{content && content !== "" && (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
Content: <p className="text-gray-500">{content}</p>
|
Content:
|
||||||
|
<RemirrorRichTextEditor
|
||||||
|
value={htmlContent ?? <p>{content}</p>}
|
||||||
|
customClassName="-mx-3 -my-3"
|
||||||
|
noBorder
|
||||||
|
borderOnFocus={false}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{response !== "" && (
|
{response !== "" && (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
Response: <p className="text-gray-500">{response}</p>
|
Response:
|
||||||
|
<RemirrorRichTextEditor
|
||||||
|
value={`<p>${response}</p>`}
|
||||||
|
customClassName="-mx-3 -my-3"
|
||||||
|
noBorder
|
||||||
|
borderOnFocus={false}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{invalidResponse && (
|
{invalidResponse && (
|
||||||
@ -123,7 +137,11 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
name="task"
|
name="task"
|
||||||
register={register}
|
register={register}
|
||||||
placeholder="Tell OpenAI what action to perform on this content..."
|
placeholder={`${
|
||||||
|
content && content !== ""
|
||||||
|
? "Tell AI what action to perform on this content..."
|
||||||
|
: "Ask AI anything..."
|
||||||
|
}`}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
|
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
|
||||||
|
@ -239,7 +239,11 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<RemirrorRichTextEditor
|
<RemirrorRichTextEditor
|
||||||
value={value}
|
value={
|
||||||
|
!value || (typeof value === "object" && Object.keys(value).length === 0)
|
||||||
|
? watch("description_html")
|
||||||
|
: value
|
||||||
|
}
|
||||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
|
@ -45,12 +45,20 @@ export const CreateUpdatePageModal: React.FC<Props> = ({ isOpen, handleClose, da
|
|||||||
mutate(RECENT_PAGES_LIST(projectId as string));
|
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||||
mutate<IPage[]>(
|
mutate<IPage[]>(
|
||||||
MY_PAGES_LIST(projectId as string),
|
MY_PAGES_LIST(projectId as string),
|
||||||
(prevData) => [res, ...(prevData as IPage[])],
|
(prevData) => {
|
||||||
|
if (!prevData) return undefined;
|
||||||
|
|
||||||
|
return [res, ...(prevData as IPage[])];
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
mutate<IPage[]>(
|
mutate<IPage[]>(
|
||||||
ALL_PAGES_LIST(projectId as string),
|
ALL_PAGES_LIST(projectId as string),
|
||||||
(prevData) => [res, ...(prevData as IPage[])],
|
(prevData) => {
|
||||||
|
if (!prevData) return undefined;
|
||||||
|
|
||||||
|
return [res, ...(prevData as IPage[])];
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
onClose();
|
onClose();
|
||||||
|
@ -2,6 +2,8 @@ import React, { useState } from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
@ -14,6 +16,13 @@ import { DangerButton, SecondaryButton } from "components/ui";
|
|||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import type { IPage } from "types";
|
import type { IPage } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import {
|
||||||
|
ALL_PAGES_LIST,
|
||||||
|
FAVORITE_PAGES_LIST,
|
||||||
|
MY_PAGES_LIST,
|
||||||
|
RECENT_PAGES_LIST,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
type TConfirmPageDeletionProps = {
|
type TConfirmPageDeletionProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -45,6 +54,22 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = ({
|
|||||||
await pagesService
|
await pagesService
|
||||||
.deletePage(workspaceSlug as string, data.project, data.id)
|
.deletePage(workspaceSlug as string, data.project, data.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||||
|
mutate<IPage[]>(
|
||||||
|
MY_PAGES_LIST(projectId as string),
|
||||||
|
(prevData) => (prevData ?? []).filter((page) => page.id !== data?.id),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
mutate<IPage[]>(
|
||||||
|
ALL_PAGES_LIST(projectId as string),
|
||||||
|
(prevData) => (prevData ?? []).filter((page) => page.id !== data?.id),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
mutate<IPage[]>(
|
||||||
|
FAVORITE_PAGES_LIST(projectId as string),
|
||||||
|
(prevData) => (prevData ?? []).filter((page) => page.id !== data?.id),
|
||||||
|
false
|
||||||
|
);
|
||||||
handleClose();
|
handleClose();
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
|
@ -57,7 +57,6 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
|||||||
const handleAddToFavorites = (page: IPage) => {
|
const handleAddToFavorites = (page: IPage) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
mutate(RECENT_PAGES_LIST(projectId as string));
|
|
||||||
mutate<IPage[]>(
|
mutate<IPage[]>(
|
||||||
ALL_PAGES_LIST(projectId as string),
|
ALL_PAGES_LIST(projectId as string),
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
@ -89,6 +88,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
|||||||
page: page.id,
|
page: page.id,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -107,7 +107,6 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
|||||||
const handleRemoveFromFavorites = (page: IPage) => {
|
const handleRemoveFromFavorites = (page: IPage) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
mutate(RECENT_PAGES_LIST(projectId as string));
|
|
||||||
mutate<IPage[]>(
|
mutate<IPage[]>(
|
||||||
ALL_PAGES_LIST(projectId as string),
|
ALL_PAGES_LIST(projectId as string),
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
@ -137,6 +136,7 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
|
|||||||
pagesService
|
pagesService
|
||||||
.removePageFromFavorites(workspaceSlug as string, projectId as string, page.id)
|
.removePageFromFavorites(workspaceSlug as string, projectId as string, page.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
|
@ -17,11 +17,16 @@ import useToast from "hooks/use-toast";
|
|||||||
import { CreateUpdateIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal } from "components/issues";
|
||||||
import { GptAssistantModal } from "components/core";
|
import { GptAssistantModal } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, Loader, TextArea } from "components/ui";
|
import { CustomMenu, Input, Loader, TextArea } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { LayerDiagonalIcon, WaterDropIcon } from "components/icons";
|
import { LayerDiagonalIcon } from "components/icons";
|
||||||
import { ArrowPathIcon } from "@heroicons/react/20/solid";
|
import { ArrowPathIcon } from "@heroicons/react/20/solid";
|
||||||
import { CheckIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
|
BoltIcon,
|
||||||
|
CheckIcon,
|
||||||
|
CursorArrowRaysIcon,
|
||||||
|
SparklesIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
@ -163,21 +168,8 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
|||||||
const handleAiAssistance = async (response: string) => {
|
const handleAiAssistance = async (response: string) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
setValue("description", {
|
setValue("description", {});
|
||||||
type: "doc",
|
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
text: response,
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
setValue("description_html", `<p>${response}</p>`);
|
|
||||||
handleSubmit(updatePageBlock)()
|
handleSubmit(updatePageBlock)()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -253,7 +245,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="-mx-3 -mt-2 flex items-center justify-between gap-2">
|
<div className="-mx-3 -mt-2 flex items-center justify-between gap-2">
|
||||||
<TextArea
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
placeholder="Block title"
|
placeholder="Block title"
|
||||||
@ -261,11 +253,11 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
|||||||
onBlur={handleSubmit(updatePageBlock)}
|
onBlur={handleSubmit(updatePageBlock)}
|
||||||
onChange={(e) => setValue("name", e.target.value)}
|
onChange={(e) => setValue("name", e.target.value)}
|
||||||
required={true}
|
required={true}
|
||||||
className="min-h-10 block w-full resize-none overflow-hidden border-none bg-transparent text-base font-medium"
|
className="min-h-10 block w-full resize-none overflow-hidden border-none bg-transparent py-1 text-base font-medium ring-0 focus:ring-1 focus:ring-gray-200"
|
||||||
role="textbox"
|
role="textbox"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-shrink-0 items-center gap-2">
|
<div className="flex flex-shrink-0 items-center gap-2">
|
||||||
{block.sync && (
|
{block.issue && block.sync && (
|
||||||
<div className="flex flex-shrink-0 cursor-default items-center gap-1 rounded bg-gray-100 py-1 px-1.5 text-xs">
|
<div className="flex flex-shrink-0 cursor-default items-center gap-1 rounded bg-gray-100 py-1 px-1.5 text-xs">
|
||||||
{isSyncing ? (
|
{isSyncing ? (
|
||||||
<ArrowPathIcon className="h-3 w-3 animate-spin" />
|
<ArrowPathIcon className="h-3 w-3 animate-spin" />
|
||||||
@ -285,12 +277,13 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="-mr-2 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
|
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
|
||||||
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
||||||
>
|
>
|
||||||
|
<SparklesIcon className="h-4 w-4" />
|
||||||
AI
|
AI
|
||||||
</button>
|
</button>
|
||||||
<CustomMenu label={<WaterDropIcon width={14} height={15} />} noBorder noChevron>
|
<CustomMenu label={<BoltIcon className="h-4.5 w-3.5" />} noBorder noChevron>
|
||||||
{block.issue ? (
|
{block.issue ? (
|
||||||
<>
|
<>
|
||||||
<CustomMenu.MenuItem onClick={handleBlockSync}>
|
<CustomMenu.MenuItem onClick={handleBlockSync}>
|
||||||
@ -312,7 +305,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-block-section relative -mx-3 -mt-5">
|
<div className="page-block-section font relative -mx-3 -mt-3">
|
||||||
<Controller
|
<Controller
|
||||||
name="description"
|
name="description"
|
||||||
control={control}
|
control={control}
|
||||||
@ -327,8 +320,9 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
|||||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||||
placeholder="Block description..."
|
placeholder="Block description..."
|
||||||
customClassName="text-gray-500"
|
customClassName="border border-transparent"
|
||||||
noBorder
|
noBorder
|
||||||
|
borderOnFocus
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -337,6 +331,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
|
|||||||
handleClose={() => setGptAssistantModal(false)}
|
handleClose={() => setGptAssistantModal(false)}
|
||||||
inset="top-2 left-0"
|
inset="top-2 left-0"
|
||||||
content={block.description_stripped}
|
content={block.description_stripped}
|
||||||
|
htmlContent={block.description_html}
|
||||||
onResponse={handleAiAssistance}
|
onResponse={handleAiAssistance}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,13 +6,12 @@ import { useRouter } from "next/router";
|
|||||||
// ui
|
// ui
|
||||||
import { CustomMenu, Tooltip } from "components/ui";
|
import { CustomMenu, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { DocumentTextIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
import { renderShortDate, renderShortTime } from "helpers/date-time.helper";
|
import { renderShortDate, renderShortTime } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { IPage } from "types";
|
import { IPage } from "types";
|
||||||
import { PencilScribbleIcon } from "components/icons";
|
|
||||||
|
|
||||||
type TSingleStatProps = {
|
type TSingleStatProps = {
|
||||||
page: IPage;
|
page: IPage;
|
||||||
@ -39,7 +38,7 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
|
|||||||
<div className="relative rounded p-4 hover:bg-gray-100">
|
<div className="relative rounded p-4 hover:bg-gray-100">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<PencilScribbleIcon />
|
<DocumentTextIcon className="h-4 w-4" />
|
||||||
<p className="mr-2 truncate text-sm font-medium">{truncateText(page.name, 75)}</p>
|
<p className="mr-2 truncate text-sm font-medium">{truncateText(page.name, 75)}</p>
|
||||||
{page.label_details.length > 0 &&
|
{page.label_details.length > 0 &&
|
||||||
page.label_details.map((label) => (
|
page.label_details.map((label) => (
|
||||||
|
@ -6,7 +6,7 @@ import { Disclosure, Transition } from "@headlessui/react";
|
|||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { ChevronDownIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
|
||||||
import {
|
import {
|
||||||
ContrastIcon,
|
ContrastIcon,
|
||||||
LayerDiagonalIcon,
|
LayerDiagonalIcon,
|
||||||
@ -53,7 +53,7 @@ const navigation = (workspaceSlug: string, projectId: string) => [
|
|||||||
{
|
{
|
||||||
name: "Pages",
|
name: "Pages",
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/pages`,
|
href: `/${workspaceSlug}/projects/${projectId}/pages`,
|
||||||
icon: PencilScribbleIcon,
|
icon: DocumentTextIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
|
@ -50,6 +50,7 @@ export interface IRemirrorRichTextEditor {
|
|||||||
customClassName?: string;
|
customClassName?: string;
|
||||||
gptOption?: boolean;
|
gptOption?: boolean;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
|
borderOnFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-duplicate-imports
|
// eslint-disable-next-line no-duplicate-imports
|
||||||
@ -69,6 +70,7 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
|||||||
customClassName,
|
customClassName,
|
||||||
gptOption = false,
|
gptOption = false,
|
||||||
noBorder = false,
|
noBorder = false,
|
||||||
|
borderOnFocus = true,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [imageLoader, setImageLoader] = useState(false);
|
const [imageLoader, setImageLoader] = useState(false);
|
||||||
@ -188,9 +190,9 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
|||||||
manager={manager}
|
manager={manager}
|
||||||
initialContent={state}
|
initialContent={state}
|
||||||
classNames={[
|
classNames={[
|
||||||
`p-4 relative focus:outline-none rounded-md focus:border-theme ${
|
`p-4 relative focus:outline-none rounded-md focus:border-gray-200 ${
|
||||||
noBorder ? "" : "border"
|
noBorder ? "" : "border"
|
||||||
} ${customClassName}`,
|
} ${borderOnFocus ? "focus:border" : ""} ${customClassName}`,
|
||||||
]}
|
]}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
|
@ -8,14 +8,12 @@ import { Transition } from "@headlessui/react";
|
|||||||
import useTheme from "hooks/use-theme";
|
import useTheme from "hooks/use-theme";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowLongLeftIcon, ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
|
|
||||||
import {
|
import {
|
||||||
QuestionMarkCircleIcon,
|
ArrowLongLeftIcon,
|
||||||
BoltIcon,
|
ChatBubbleOvalLeftEllipsisIcon,
|
||||||
DocumentIcon,
|
RocketLaunchIcon,
|
||||||
DiscordIcon,
|
} from "@heroicons/react/24/outline";
|
||||||
GithubIcon,
|
import { QuestionMarkCircleIcon, DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
|
||||||
} from "components/icons";
|
|
||||||
|
|
||||||
const helpOptions = [
|
const helpOptions = [
|
||||||
{
|
{
|
||||||
@ -77,7 +75,7 @@ export const WorkspaceHelpSection: FC<WorkspaceHelpSectionProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
title="Shortcuts"
|
title="Shortcuts"
|
||||||
>
|
>
|
||||||
<BoltIcon className="h-4 w-4 text-gray-500" />
|
<RocketLaunchIcon className="h-4 w-4 text-gray-500" />
|
||||||
{!sidebarCollapse && <span>Shortcuts</span>}
|
{!sidebarCollapse && <span>Shortcuts</span>}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
@ -24,7 +24,7 @@ import AppLayout from "layouts/app-layout";
|
|||||||
import { SinglePageBlock } from "components/pages";
|
import { SinglePageBlock } from "components/pages";
|
||||||
// ui
|
// ui
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
import { CustomSearchSelect, Loader, PrimaryButton, TextArea } from "components/ui";
|
import { CustomSearchSelect, Loader, PrimaryButton, TextArea, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowLeftIcon, PlusIcon, ShareIcon, StarIcon } from "@heroicons/react/24/outline";
|
import { ArrowLeftIcon, PlusIcon, ShareIcon, StarIcon } from "@heroicons/react/24/outline";
|
||||||
import { ColorPalletteIcon } from "components/icons";
|
import { ColorPalletteIcon } from "components/icons";
|
||||||
@ -324,9 +324,14 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-sm text-gray-500">
|
<Tooltip
|
||||||
{renderShortTime(pageDetails.created_at)}
|
tooltipContent={`Page last updated at ${renderShortTime(pageDetails.updated_at)}`}
|
||||||
|
theme="dark"
|
||||||
|
>
|
||||||
|
<span className="cursor-default text-sm text-gray-500">
|
||||||
|
{renderShortTime(pageDetails.updated_at)}
|
||||||
</span>
|
</span>
|
||||||
|
</Tooltip>
|
||||||
<PrimaryButton className="flex items-center gap-2" onClick={handleCopyText}>
|
<PrimaryButton className="flex items-center gap-2" onClick={handleCopyText}>
|
||||||
<ShareIcon className="h-4 w-4" />
|
<ShareIcon className="h-4 w-4" />
|
||||||
Share
|
Share
|
||||||
@ -393,7 +398,7 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
onBlur={handleSubmit(updatePage)}
|
onBlur={handleSubmit(updatePage)}
|
||||||
onChange={(e) => setValue("name", e.target.value)}
|
onChange={(e) => setValue("name", e.target.value)}
|
||||||
required={true}
|
required={true}
|
||||||
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-semibold outline-none ring-0 focus:ring-1 focus:ring-theme"
|
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-semibold outline-none ring-0 focus:ring-1 focus:ring-gray-200"
|
||||||
role="textbox"
|
role="textbox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -413,7 +418,7 @@ const SinglePage: NextPage<UserAuth> = (props) => {
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-1 rounded px-2.5 py-1 text-xs hover:bg-gray-100"
|
className="flex items-center gap-1 rounded bg-gray-100 px-2.5 py-1 text-xs hover:bg-gray-200"
|
||||||
onClick={createPageBlock}
|
onClick={createPageBlock}
|
||||||
disabled={isAddingBlock}
|
disabled={isAddingBlock}
|
||||||
>
|
>
|
||||||
|
@ -120,12 +120,20 @@ const ProjectPages: NextPage<UserAuth> = (props) => {
|
|||||||
mutate(RECENT_PAGES_LIST(projectId as string));
|
mutate(RECENT_PAGES_LIST(projectId as string));
|
||||||
mutate<IPage[]>(
|
mutate<IPage[]>(
|
||||||
MY_PAGES_LIST(projectId as string),
|
MY_PAGES_LIST(projectId as string),
|
||||||
(prevData) => [res, ...(prevData as IPage[])],
|
(prevData) => {
|
||||||
|
if (!prevData) return undefined;
|
||||||
|
|
||||||
|
return [res, ...(prevData as IPage[])];
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
mutate<IPage[]>(
|
mutate<IPage[]>(
|
||||||
ALL_PAGES_LIST(projectId as string),
|
ALL_PAGES_LIST(projectId as string),
|
||||||
(prevData) => [res, ...(prevData as IPage[])],
|
(prevData) => {
|
||||||
|
if (!prevData) return undefined;
|
||||||
|
|
||||||
|
return [res, ...(prevData as IPage[])];
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
// types
|
||||||
|
import { IGptResponse } from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
@ -12,7 +14,7 @@ class AiServices extends APIService {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
data: { prompt: string; task: string }
|
data: { prompt: string; task: string }
|
||||||
): Promise<any> {
|
): Promise<IGptResponse> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
4
apps/app/types/ai.d.ts
vendored
Normal file
4
apps/app/types/ai.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface IGptResponse {
|
||||||
|
response: string;
|
||||||
|
response_html: string;
|
||||||
|
}
|
1
apps/app/types/index.d.ts
vendored
1
apps/app/types/index.d.ts
vendored
@ -9,6 +9,7 @@ export * from "./modules";
|
|||||||
export * from "./views";
|
export * from "./views";
|
||||||
export * from "./integration";
|
export * from "./integration";
|
||||||
export * from "./pages";
|
export * from "./pages";
|
||||||
|
export * from "./ai";
|
||||||
|
|
||||||
export type NestedKeyOf<ObjectType extends object> = {
|
export type NestedKeyOf<ObjectType extends object> = {
|
||||||
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
||||||
|
Loading…
Reference in New Issue
Block a user