diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 6a1c07c17..96a72c017 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -93,6 +93,7 @@ from plane.api.views import ( CompletedCyclesEndpoint, CycleFavoriteViewSet, DraftCyclesEndpoint, + TransferCycleIssueEndpoint, ## End Cycles # Modules ModuleViewSet, @@ -629,6 +630,11 @@ urlpatterns = [ ), name="user-favorite-cycle", ), + path( + "workspaces//projects//cycles//transfer-issues/", + TransferCycleIssueEndpoint.as_view(), + name="transfer-issues", + ), ## End Cycles # Issue path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 7e7b0337d..784574c4b 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -51,6 +51,7 @@ from .cycle import ( CompletedCyclesEndpoint, CycleFavoriteViewSet, DraftCyclesEndpoint, + TransferCycleIssueEndpoint, ) from .asset import FileAssetEndpoint, UserAssetsEndpoint from .issue import ( diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index f8e8715e1..13ee8b1e3 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -129,6 +129,36 @@ class CycleViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) + def partial_update(self, request, slug, project_id, pk): + try: + cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + + if cycle.end_date < timezone.now().date(): + return Response( + { + "error": "The Cycle has already been completed so it cannot be edited" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = CycleSerializer(cycle, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Cycle.DoesNotExist: + return Response( + {"error": "Cycle does not exist"}, status=status.HTTP_400_BAD_REQUEST + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + class CycleIssueViewSet(BaseViewSet): serializer_class = CycleIssueSerializer @@ -230,6 +260,14 @@ class CycleIssueViewSet(BaseViewSet): workspace__slug=slug, project_id=project_id, pk=cycle_id ) + if cycle.end_date < timezone.now().date(): + return Response( + { + "error": "The Cycle has already been completed so no new issues can be added" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Get all CycleIssues already created cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues)) records_to_update = [] @@ -681,3 +719,60 @@ class CycleFavoriteViewSet(BaseViewSet): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class TransferCycleIssueEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + def post(self, request, slug, project_id, cycle_id): + try: + new_cycle_id = request.data.get("new_cycle_id", False) + + if not new_cycle_id: + return Response( + {"error": "New Cycle Id is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + new_cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=new_cycle_id + ) + + if new_cycle.end_date < timezone.now().date(): + return Response( + { + "error": "The cycle where the issues are transferred is already completed" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + cycle_issues = CycleIssue.objects.filter( + cycle_id=cycle_id, + project_id=project_id, + workspace__slug=slug, + issue__state__group__in=["backlog", "unstarted", "started"], + ) + + updated_cycles = [] + for cycle_issue in cycle_issues: + cycle_issue.cycle_id = new_cycle_id + updated_cycles.append(cycle_issue) + + cycle_issues = CycleIssue.objects.bulk_update( + updated_cycles, ["cycle_id"], batch_size=100 + ) + + return Response({"message": "Success"}, status=status.HTTP_200_OK) + except Cycle.DoesNotExist: + return Response( + {"error": "New Cycle Does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/api/views/gpt.py b/apiserver/plane/api/views/gpt.py index c470593bf..bdc060e4f 100644 --- a/apiserver/plane/api/views/gpt.py +++ b/apiserver/plane/api/views/gpt.py @@ -28,10 +28,9 @@ class GPTIntegrationEndpoint(BaseAPIView): prompt = request.data.get("prompt", False) task = request.data.get("task", False) - if not prompt or not task: + if not task: return Response( - {"error": "Task and prompt are required"}, - status=status.HTTP_400_BAD_REQUEST, + {"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST ) final_text = task + "\n" + prompt @@ -45,7 +44,11 @@ class GPTIntegrationEndpoint(BaseAPIView): ) text = response.choices[0].text.strip() - return Response({"response": text}, status=status.HTTP_200_OK) + text_html = text.replace("\n", "
") + return Response( + {"response": text, "response_html": text_html}, + status=status.HTTP_200_OK, + ) except Exception as e: capture_exception(e) return Response( diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py index 16652d762..ba75eac91 100644 --- a/apiserver/plane/api/views/search.py +++ b/apiserver/plane/api/views/search.py @@ -26,7 +26,7 @@ class GlobalSearchEndpoint(BaseAPIView): q |= Q(**{f"{field}__icontains": query}) return Workspace.objects.filter( q, workspace_member__member=self.request.user - ).values("name", "id", "slug") + ).distinct().values("name", "id", "slug") def filter_projects(self, query, slug, project_id): fields = ["name"] @@ -37,7 +37,7 @@ class GlobalSearchEndpoint(BaseAPIView): q, Q(project_projectmember__member=self.request.user) | Q(network=2), workspace__slug=slug, - ).values("name", "id", "identifier", "workspace__slug") + ).distinct().values("name", "id", "identifier", "workspace__slug") def filter_issues(self, query, slug, project_id): fields = ["name", "sequence_id"] @@ -54,7 +54,7 @@ class GlobalSearchEndpoint(BaseAPIView): project__project_projectmember__member=self.request.user, workspace__slug=slug, project_id=project_id, - ).values( + ).distinct().values( "name", "id", "sequence_id", @@ -73,7 +73,7 @@ class GlobalSearchEndpoint(BaseAPIView): project__project_projectmember__member=self.request.user, workspace__slug=slug, project_id=project_id, - ).values( + ).distinct().values( "name", "id", "project_id", @@ -90,7 +90,7 @@ class GlobalSearchEndpoint(BaseAPIView): project__project_projectmember__member=self.request.user, workspace__slug=slug, project_id=project_id, - ).values( + ).distinct().values( "name", "id", "project_id", @@ -107,7 +107,7 @@ class GlobalSearchEndpoint(BaseAPIView): project__project_projectmember__member=self.request.user, workspace__slug=slug, project_id=project_id, - ).values( + ).distinct().values( "name", "id", "project_id", @@ -124,7 +124,7 @@ class GlobalSearchEndpoint(BaseAPIView): project__project_projectmember__member=self.request.user, workspace__slug=slug, project_id=project_id, - ).values( + ).distinct().values( "name", "id", "project_id", diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index aa834c51b..92a419d22 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -6,9 +6,9 @@ import useSWR, { mutate } from "swr"; import { ArrowRightIcon, ChartBarIcon, + ChatBubbleOvalLeftEllipsisIcon, ClipboardIcon, FolderPlusIcon, - InboxIcon, MagnifyingGlassIcon, Squares2X2Icon, TrashIcon, @@ -27,7 +27,7 @@ import { PeopleGroupIcon, SettingIcon, ViewListIcon, - PencilScribbleIcon + PencilScribbleIcon, } from "components/icons"; // headless ui import { Dialog, Transition } from "@headlessui/react"; @@ -37,6 +37,7 @@ import { Command } from "cmdk"; import useTheme from "hooks/use-theme"; import useToast from "hooks/use-toast"; import useUser from "hooks/use-user"; +import useDebounce from "hooks/use-debounce"; // components import { ShortcutsModal, @@ -50,6 +51,7 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateModuleModal } from "components/modules"; import { CreateProjectModal } from "components/project"; import { CreateUpdateViewModal } from "components/views"; +import { Spinner } from "components/ui"; // helpers import { capitalizeFirstLetter, @@ -58,12 +60,11 @@ import { } from "helpers/string.helper"; // services import issuesService from "services/issues.service"; +import workspaceService from "services/workspace.service"; // types import { IIssue, IWorkspaceSearchResults } from "types"; // fetch keys import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; -import useDebounce from "hooks/use-debounce"; -import workspaceService from "services/workspace.service"; export const CommandPalette: React.FC = () => { const [isPaletteOpen, setIsPaletteOpen] = useState(false); @@ -88,7 +89,9 @@ export const CommandPalette: React.FC = () => { page: [], }, }); - const [isPendingAPIRequest, setIsPendingAPIRequest] = useState(false); + const [resultsCount, setResultsCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [isSearching, setIsSearching] = useState(false); const debouncedSearchTerm = useDebounce(searchTerm, 500); const [placeholder, setPlaceholder] = React.useState("Type a command or search..."); const [pages, setPages] = React.useState([]); @@ -220,18 +223,39 @@ export const CommandPalette: React.FC = () => { () => { if (!workspaceSlug || !projectId) return; - // this is done prevent api request when user is clearing input + setIsLoading(true); + // this is done prevent subsequent api request // or searchTerm has not been updated within last 500ms. if (debouncedSearchTerm) { - setIsPendingAPIRequest(true); + setIsSearching(true); workspaceService .searchWorkspace(workspaceSlug as string, projectId as string, debouncedSearchTerm) .then((results) => { - setIsPendingAPIRequest(false); setResults(results); + const count = Object.keys(results.results).reduce( + (accumulator, key) => (results.results as any)[key].length + accumulator, + 0 + ); + setResultsCount(count); + }) + .finally(() => { + setIsLoading(false); + setIsSearching(false); }); } else { - setIsPendingAPIRequest(false); + setResults({ + results: { + workspace: [], + project: [], + issue: [], + cycle: [], + module: [], + issue_view: [], + page: [], + }, + }); + setIsLoading(false); + setIsSearching(false); } }, [debouncedSearchTerm, workspaceSlug, projectId] // Only call effect if debounced search term changes @@ -369,11 +393,11 @@ export const CommandPalette: React.FC = () => { }} > {issueId && issueDetails && ( -
- +
+

{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "} {issueDetails?.name} - +

)}
@@ -392,9 +416,20 @@ export const CommandPalette: React.FC = () => { />
- - No results found. - + {!isLoading && + resultsCount === 0 && + searchTerm !== "" && + debouncedSearchTerm !== "" && ( +
No results found.
+ )} + + {(isLoading || isSearching) && ( + +
+ +
+
+ )} {debouncedSearchTerm !== "" && ( <> @@ -419,7 +454,8 @@ export const CommandPalette: React.FC = () => { Icon = AssignmentClipboardIcon; } else if (key === "issue") { path = `/${item.workspace__slug}/projects/${item.project_id}/issues/${item.id}`; - value = `${item.project__identifier}-${item.sequence_id} item.name`; + // user can search id-num idnum or issue name + value = `${item.project__identifier}-${item.sequence_id} ${item.project__identifier}${item.sequence_id} ${item.name}`; Icon = LayerDiagonalIcon; } else if (key === "issue_view") { path = `/${item.workspace__slug}/projects/${item.project_id}/views/${item.id}`; @@ -446,9 +482,9 @@ export const CommandPalette: React.FC = () => { className="focus:bg-slate-200 focus:outline-none" tabIndex={0} > -
+
- {item.name} +

{item.name}

); @@ -720,14 +756,14 @@ export const CommandPalette: React.FC = () => { { setIsPaletteOpen(false); - window.open("mailto:hello@plane.so", "_blank"); + (window as any).$crisp.push(["do", "chat:open"]); }} className="focus:bg-slate-200 focus:outline-none" tabIndex={0} >
- - Email us + + Chat with us
diff --git a/apps/app/components/core/gpt-assistant-modal.tsx b/apps/app/components/core/gpt-assistant-modal.tsx index 0afe481a7..e9c7241c1 100644 --- a/apps/app/components/core/gpt-assistant-modal.tsx +++ b/apps/app/components/core/gpt-assistant-modal.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; +import dynamic from "next/dynamic"; // react-hook-form import { useForm } from "react-hook-form"; @@ -16,6 +17,7 @@ type Props = { handleClose: () => void; inset?: string; content: string; + htmlContent?: string; onResponse: (response: string) => void; }; @@ -24,11 +26,16 @@ type FormData = { task: string; }; +const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { + ssr: false, +}); + export const GptAssistantModal: React.FC = ({ isOpen, handleClose, inset = "top-0 left-0", content, + htmlContent, onResponse, }) => { const [response, setResponse] = useState(""); @@ -62,15 +69,6 @@ export const GptAssistantModal: React.FC = ({ const handleResponse = async (formData: FormData) => { 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 === "") { setToastAlert({ type: "error", @@ -82,11 +80,11 @@ export const GptAssistantModal: React.FC = ({ await aiService .createGptTask(workspaceSlug as string, projectId as string, { - prompt: content, + prompt: content && content !== "" ? content : "", task: formData.task, }) .then((res) => { - setResponse(res.response); + setResponse(res.response_html); setFocus("task"); if (res.response === "") setInvalidResponse(true); @@ -105,12 +103,28 @@ export const GptAssistantModal: React.FC = ({ }`} >
-
- Content:

{content}

-
+ {content && content !== "" && ( +
+ Content: + {content}

} + customClassName="-mx-3 -my-3" + noBorder + borderOnFocus={false} + editable={false} + /> +
+ )} {response !== "" && (
- Response:

{response}

+ Response: + ${response}

`} + customClassName="-mx-3 -my-3" + noBorder + borderOnFocus={false} + editable={false} + />
)} {invalidResponse && ( @@ -123,7 +137,11 @@ export const GptAssistantModal: React.FC = ({ type="text" name="task" 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" />
diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index 52567c06a..4a561f1bb 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -30,7 +30,7 @@ import { TrashIcon, XMarkIcon, } from "@heroicons/react/24/outline"; -import { getStateGroupIcon } from "components/icons"; +import { ExclamationIcon, getStateGroupIcon } from "components/icons"; // helpers import { getStatesList } from "helpers/state.helper"; // types @@ -683,6 +683,12 @@ export const IssuesView: React.FC = ({ {groupedByIssues ? ( isNotEmpty ? ( <> + {isCompleted && ( +
+ + Completed cycles are not editable. +
+ )} {issueView === "list" ? ( = ({ groupedIssues, issues, module, - setModuleLinkModal, - handleDeleteLink, userAuth, }) => { const router = useRouter(); @@ -72,14 +67,12 @@ export const SidebarProgressStats: React.FC = ({ const currentValue = (tab: string | null) => { switch (tab) { - case "Links": - return 0; case "Assignees": - return 1; + return 0; case "Labels": - return 2; + return 1; case "States": - return 3; + return 2; default: return 3; @@ -91,12 +84,10 @@ export const SidebarProgressStats: React.FC = ({ onChange={(i) => { switch (i) { case 0: - return setTab("Links"); - case 1: return setTab("Assignees"); - case 2: + case 1: return setTab("Labels"); - case 3: + case 2: return setTab("States"); default: @@ -109,20 +100,6 @@ export const SidebarProgressStats: React.FC = ({ className={`flex w-full items-center justify-between rounded-md bg-gray-100 px-1 py-1.5 ${module ? "text-xs" : "text-sm"} `} > - {module ? ( - - `w-full rounded px-3 py-1 text-gray-900 ${ - selected ? " bg-theme text-white" : " hover:bg-hover-gray" - }` - } - > - Links - - ) : ( - "" - )} - `w-full rounded px-3 py-1 text-gray-900 ${ @@ -152,29 +129,6 @@ export const SidebarProgressStats: React.FC = ({ - {module ? ( - - -
- {userAuth && module.link_module && module.link_module.length > 0 ? ( - - ) : null} -
-
- ) : ( - "" - )} - {members?.map((member, index) => { const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id)); diff --git a/apps/app/components/cycles/completed-cycles-list.tsx b/apps/app/components/cycles/completed-cycles-list.tsx index 8a369e8b9..cb8bb43d7 100644 --- a/apps/app/components/cycles/completed-cycles-list.tsx +++ b/apps/app/components/cycles/completed-cycles-list.tsx @@ -9,7 +9,7 @@ import cyclesService from "services/cycles.service"; // components import { DeleteCycleModal, SingleCycleCard } from "components/cycles"; // icons -import { CompletedCycleIcon } from "components/icons"; +import { CompletedCycleIcon, ExclamationIcon } from "components/icons"; // types import { ICycle, SelectCycleType } from "types"; // fetch-keys @@ -63,16 +63,22 @@ export const CompletedCyclesList: React.FC = ({ /> {completedCycles ? ( completedCycles.completed_cycles.length > 0 ? ( -
- {completedCycles.completed_cycles.map((cycle) => ( - handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - isCompleted - /> - ))} +
+
+ + Completed cycles are not editable. +
+
+ {completedCycles.completed_cycles.map((cycle) => ( + handleDeleteCycle(cycle)} + handleEditCycle={() => handleEditCycle(cycle)} + isCompleted + /> + ))} +
) : ( = ({ control={control} render={({ field: { value } }) => ( setValue("description", jsonValue)} onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)} placeholder="Description" diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx index 7f360a419..839030740 100644 --- a/apps/app/components/modules/sidebar.tsx +++ b/apps/app/components/modules/sidebar.tsx @@ -14,6 +14,7 @@ import { ChevronDownIcon, DocumentDuplicateIcon, DocumentIcon, + PlusIcon, TrashIcon, } from "@heroicons/react/24/outline"; @@ -24,7 +25,7 @@ import modulesService from "services/modules.service"; // hooks import useToast from "hooks/use-toast"; // components -import { LinkModal, SidebarProgressStats } from "components/core"; +import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules"; import ProgressChart from "components/core/sidebar/progress-chart"; import { CustomMenu, CustomSelect, Loader, ProgressBar } from "components/ui"; @@ -414,7 +415,7 @@ export const ModuleDetailsSidebar: React.FC = ({
{isStartValid && isEndValid ? ( - +
{issues.length > 0 ? ( - +
+ +
+
+

Links

+ +
+
+ {userAuth && module.link_module && module.link_module.length > 0 ? ( + + ) : null} +
+
) : ( diff --git a/apps/app/components/pages/create-update-page-modal.tsx b/apps/app/components/pages/create-update-page-modal.tsx index 18b2c9b34..ed12f8f46 100644 --- a/apps/app/components/pages/create-update-page-modal.tsx +++ b/apps/app/components/pages/create-update-page-modal.tsx @@ -45,12 +45,20 @@ export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, da mutate(RECENT_PAGES_LIST(projectId as string)); mutate( MY_PAGES_LIST(projectId as string), - (prevData) => [res, ...(prevData as IPage[])], + (prevData) => { + if (!prevData) return undefined; + + return [res, ...(prevData as IPage[])]; + }, false ); mutate( ALL_PAGES_LIST(projectId as string), - (prevData) => [res, ...(prevData as IPage[])], + (prevData) => { + if (!prevData) return undefined; + + return [res, ...(prevData as IPage[])]; + }, false ); onClose(); diff --git a/apps/app/components/pages/delete-page-modal.tsx b/apps/app/components/pages/delete-page-modal.tsx index 38dff3264..94468f2b8 100644 --- a/apps/app/components/pages/delete-page-modal.tsx +++ b/apps/app/components/pages/delete-page-modal.tsx @@ -2,6 +2,8 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; +import { mutate } from "swr"; + // headless ui import { Dialog, Transition } from "@headlessui/react"; // services @@ -14,6 +16,13 @@ import { DangerButton, SecondaryButton } from "components/ui"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // 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 = { isOpen: boolean; @@ -45,6 +54,22 @@ export const DeletePageModal: React.FC = ({ await pagesService .deletePage(workspaceSlug as string, data.project, data.id) .then(() => { + mutate(RECENT_PAGES_LIST(projectId as string)); + mutate( + MY_PAGES_LIST(projectId as string), + (prevData) => (prevData ?? []).filter((page) => page.id !== data?.id), + false + ); + mutate( + ALL_PAGES_LIST(projectId as string), + (prevData) => (prevData ?? []).filter((page) => page.id !== data?.id), + false + ); + mutate( + FAVORITE_PAGES_LIST(projectId as string), + (prevData) => (prevData ?? []).filter((page) => page.id !== data?.id), + false + ); handleClose(); setToastAlert({ type: "success", diff --git a/apps/app/components/pages/pages-view.tsx b/apps/app/components/pages/pages-view.tsx index dc4e0d19c..e8b483016 100644 --- a/apps/app/components/pages/pages-view.tsx +++ b/apps/app/components/pages/pages-view.tsx @@ -57,7 +57,6 @@ export const PagesView: React.FC = ({ pages, viewType }) => { const handleAddToFavorites = (page: IPage) => { if (!workspaceSlug || !projectId) return; - mutate(RECENT_PAGES_LIST(projectId as string)); mutate( ALL_PAGES_LIST(projectId as string), (prevData) => @@ -89,6 +88,7 @@ export const PagesView: React.FC = ({ pages, viewType }) => { page: page.id, }) .then(() => { + mutate(RECENT_PAGES_LIST(projectId as string)); setToastAlert({ type: "success", title: "Success!", @@ -107,7 +107,6 @@ export const PagesView: React.FC = ({ pages, viewType }) => { const handleRemoveFromFavorites = (page: IPage) => { if (!workspaceSlug || !projectId) return; - mutate(RECENT_PAGES_LIST(projectId as string)); mutate( ALL_PAGES_LIST(projectId as string), (prevData) => @@ -137,6 +136,7 @@ export const PagesView: React.FC = ({ pages, viewType }) => { pagesService .removePageFromFavorites(workspaceSlug as string, projectId as string, page.id) .then(() => { + mutate(RECENT_PAGES_LIST(projectId as string)); setToastAlert({ type: "success", title: "Success!", diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx index 1aa914022..8c9064ad7 100644 --- a/apps/app/components/pages/single-page-block.tsx +++ b/apps/app/components/pages/single-page-block.tsx @@ -17,11 +17,16 @@ import useToast from "hooks/use-toast"; import { CreateUpdateIssueModal } from "components/issues"; import { GptAssistantModal } from "components/core"; // ui -import { CustomMenu, Loader, TextArea } from "components/ui"; +import { CustomMenu, Input, Loader, TextArea } from "components/ui"; // icons -import { LayerDiagonalIcon, WaterDropIcon } from "components/icons"; +import { LayerDiagonalIcon } from "components/icons"; 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 import { copyTextToClipboard } from "helpers/string.helper"; // types @@ -163,21 +168,8 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails }) => { const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId) return; - setValue("description", { - type: "doc", - content: [ - { - type: "paragraph", - content: [ - { - text: response, - type: "text", - }, - ], - }, - ], - }); - setValue("description_html", `

${response}

`); + setValue("description", {}); + setValue("description_html", `${watch("description_html")}

${response}

`); handleSubmit(updatePageBlock)() .then(() => { setToastAlert({ @@ -253,7 +245,7 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails }) => { }} />
-