From a93dfc1b8d42c1667644496a6f34df171bff0988 Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:17:59 +0530 Subject: [PATCH 01/57] fix: favorite improvements (#5307) --- .../workspace/sidebar/projects-list-item.tsx | 390 +++++++++--------- .../workspace/sidebar/projects-list.tsx | 11 +- web/core/store/favorite.store.ts | 3 + web/core/store/pages/project-page.store.ts | 7 +- 4 files changed, 218 insertions(+), 193 deletions(-) diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index 38e50c556..ea52fc29b 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -111,6 +111,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { const [publishModalOpen, setPublishModal] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false); const [isDragging, setIsDragging] = useState(false); + const [isProjectListOpen, setIsProjectListOpen] = useState(false); const [instruction, setInstruction] = useState<"DRAG_OVER" | "DRAG_BELOW" | undefined>(undefined); // refs const actionSectionRef = useRef(null); @@ -266,200 +267,209 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { if (!project) return null; + useEffect(() => { + if (URLProjectId === project.id) setIsProjectListOpen(true); + }, [URLProjectId]); + return ( <> setPublishModal(false)} /> setLeaveProjectModal(false)} /> - - {({ open }) => ( + +
+
- -
- {!disableDrag && ( - - - - )} - {isSidebarCollapsed ? ( - - -
- -
-
- - ) : ( - <> - - - -
- -
-

{project.name}

-
- -
- setIsMenuActive(!isMenuActive)} - > - - + {!disableDrag && ( + + + + )} + {isSidebarCollapsed ? ( + + setIsProjectListOpen(!isProjectListOpen)} + > +
+ +
+
+ + ) : ( + <> + + + - - - )} -
- + onClick={() => setIsProjectListOpen(!isProjectListOpen)} + > +
+ +
+

{project.name}

+ + + + setIsMenuActive(!isMenuActive)} + > + + + } + className={cn( + "opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto", + { + "opacity-100 pointer-events-auto": isMenuActive, + } + )} + customButtonClassName="grid place-items-center" + placement="bottom-start" + > + + + + {project.is_favorite ? "Remove from favorites" : "Add to favorites"} + + + + {/* publish project settings */} + {isAdmin && ( + setPublishModal(true)}> +
+
+ +
+
{project.anchor ? "Publish settings" : "Publish"}
+
+
+ )} + + +
+ + Draft issues +
+ +
+ + + + Copy link + + + {!isViewerOrGuest && ( + + +
+ + Archives +
+ +
+ )} + + +
+ + Settings +
+ +
+ {/* leave project */} + {isViewerOrGuest && ( + +
+ + Leave project +
+
+ )} +
+ setIsProjectListOpen(!isProjectListOpen)} + > + + + + )} +
+ + {isProjectListOpen && ( {navigation(workspaceSlug?.toString(), project?.id).map((item) => { if ( @@ -498,10 +508,10 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { ); })} - - {isLastChild && } -
- )} + )} + + {isLastChild && } +
); diff --git a/web/core/components/workspace/sidebar/projects-list.tsx b/web/core/components/workspace/sidebar/projects-list.tsx index 8c3a60dad..568f98443 100644 --- a/web/core/components/workspace/sidebar/projects-list.tsx +++ b/web/core/components/workspace/sidebar/projects-list.tsx @@ -4,7 +4,7 @@ import { useState, FC, useRef, useEffect } from "react"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; +import { useParams, usePathname } from "next/navigation"; import { Briefcase, ChevronRight, Plus } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; // types @@ -43,6 +43,8 @@ export const SidebarProjectsList: FC = observer(() => { const { getProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject(); // router params const { workspaceSlug } = useParams(); + const pathname = usePathname(); + // auth const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; @@ -127,7 +129,12 @@ export const SidebarProjectsList: FC = observer(() => { setIsAllProjectsListOpen(isOpen); localStorage.setItem("isAllProjectsListOpen", isOpen.toString()); }; - + useEffect(() => { + if (pathname.includes("projects")) { + setIsAllProjectsListOpen(true); + localStorage.setItem("isAllProjectsListOpen", "true"); + } + }, [pathname]); return ( <> {workspaceSlug && ( diff --git a/web/core/store/favorite.store.ts b/web/core/store/favorite.store.ts index 50eca4b89..30917afcb 100644 --- a/web/core/store/favorite.store.ts +++ b/web/core/store/favorite.store.ts @@ -399,6 +399,9 @@ export class FavoriteStore implements IFavoriteStore { */ fetchFavorite = async (workspaceSlug: string) => { try { + this.favoriteIds = []; + this.favoriteMap = {}; + this.entityMap = {}; const favorites = await this.favoriteService.getFavorites(workspaceSlug); runInAction(() => { favorites.forEach((favorite) => { diff --git a/web/core/store/pages/project-page.store.ts b/web/core/store/pages/project-page.store.ts index 26b532320..18487d3b3 100644 --- a/web/core/store/pages/project-page.store.ts +++ b/web/core/store/pages/project-page.store.ts @@ -53,6 +53,7 @@ export class ProjectPageStore implements IProjectPageStore { }; // service service: ProjectPageService; + rootStore: CoreRootStore; constructor(private store: CoreRootStore) { makeObservable(this, { @@ -70,6 +71,7 @@ export class ProjectPageStore implements IProjectPageStore { createPage: action, removePage: action, }); + this.rootStore = store; // service this.service = new ProjectPageService(); // initialize display filters of the current project @@ -257,7 +259,10 @@ export class ProjectPageStore implements IProjectPageStore { if (!workspaceSlug || !projectId || !pageId) return undefined; await this.service.remove(workspaceSlug, projectId, pageId); - runInAction(() => unset(this.data, [pageId])); + runInAction(() => { + unset(this.data, [pageId]); + this.rootStore.favorite.removeFavoriteFromStore(pageId); + }); } catch (error) { runInAction(() => { this.loader = undefined; From 95641f31af0cc2923ef5d503d2f79afb25438d2b Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 6 Aug 2024 13:08:39 +0530 Subject: [PATCH 02/57] fix: sidebar help section padding. (#5311) --- web/core/components/workspace/sidebar/help-section.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/workspace/sidebar/help-section.tsx b/web/core/components/workspace/sidebar/help-section.tsx index 864a57f3a..e2c94e855 100644 --- a/web/core/components/workspace/sidebar/help-section.tsx +++ b/web/core/components/workspace/sidebar/help-section.tsx @@ -64,7 +64,7 @@ export const SidebarHelpSection: React.FC = observer( <>
Date: Tue, 6 Aug 2024 13:34:21 +0530 Subject: [PATCH 03/57] chore: update cache command to delete the cache entry for the cache key (#5309) --- .../plane/db/management/commands/clear_cache.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apiserver/plane/db/management/commands/clear_cache.py b/apiserver/plane/db/management/commands/clear_cache.py index 4dfbe6c10..c1908eee7 100644 --- a/apiserver/plane/db/management/commands/clear_cache.py +++ b/apiserver/plane/db/management/commands/clear_cache.py @@ -6,8 +6,23 @@ from django.core.management import BaseCommand class Command(BaseCommand): help = "Clear Cache before starting the server to remove stale values" + def add_arguments(self, parser): + # Positional argument + parser.add_argument( + "--key", type=str, nargs="?", help="Key to clear cache" + ) + def handle(self, *args, **options): try: + if options["key"]: + cache.delete(options["key"]) + self.stdout.write( + self.style.SUCCESS( + f"Cache Cleared for key: {options['key']}" + ) + ) + return + cache.clear() self.stdout.write(self.style.SUCCESS("Cache Cleared")) return From 9715922fc10d3afce344e2ae3fe4540a1769dc39 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Tue, 6 Aug 2024 16:02:01 +0530 Subject: [PATCH 04/57] [WEB-2103] chore: intercom trigger updates from sidebar and command palette helper actions (#5314) * chore: handled intercom operations programatically. * fix: app sidebar improvement --------- Co-authored-by: Anmol Singh Bhatia --- .../components/admin-sidebar/help-section.tsx | 2 +- .../[workspaceSlug]/(projects)/sidebar.tsx | 12 +++++--- .../command-palette/actions/help-actions.tsx | 12 ++++---- .../workspace/sidebar/help-section.tsx | 9 +++--- web/core/hooks/store/index.ts | 1 + web/core/hooks/store/use-transient.ts | 11 ++++++++ web/core/lib/intercom-provider.tsx | 17 +++++++++-- web/core/store/root.store.ts | 5 ++++ web/core/store/transient.store.ts | 28 +++++++++++++++++++ 9 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 web/core/hooks/store/use-transient.ts create mode 100644 web/core/store/transient.store.ts diff --git a/admin/core/components/admin-sidebar/help-section.tsx b/admin/core/components/admin-sidebar/help-section.tsx index 4b516dff0..abba68e3e 100644 --- a/admin/core/components/admin-sidebar/help-section.tsx +++ b/admin/core/components/admin-sidebar/help-section.tsx @@ -96,7 +96,7 @@ export const HelpSection: FC = observer(() => { leaveTo="transform opacity-0 scale-95" >
= observer(() => {
-
+
@@ -69,8 +73,8 @@ export const AppSidebar: FC = observer(() => { })} />
diff --git a/web/core/components/command-palette/actions/help-actions.tsx b/web/core/components/command-palette/actions/help-actions.tsx index ad54c542b..49a888798 100644 --- a/web/core/components/command-palette/actions/help-actions.tsx +++ b/web/core/components/command-palette/actions/help-actions.tsx @@ -1,19 +1,21 @@ "use client"; import { Command } from "cmdk"; +import { observer } from "mobx-react"; import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react"; // ui import { DiscordIcon } from "@plane/ui"; // hooks -import { useCommandPalette } from "@/hooks/store"; +import { useCommandPalette, useTransient } from "@/hooks/store"; type Props = { closePalette: () => void; }; -export const CommandPaletteHelpActions: React.FC = (props) => { +export const CommandPaletteHelpActions: React.FC = observer((props) => { const { closePalette } = props; // hooks const { toggleShortcutModal } = useCommandPalette(); + const { toggleIntercom } = useTransient(); return ( @@ -68,9 +70,7 @@ export const CommandPaletteHelpActions: React.FC = (props) => { { closePalette(); - if (window) { - window.$crisp.push(["do", "chat:show"]); - } + toggleIntercom(true); }} className="focus:outline-none" > @@ -81,4 +81,4 @@ export const CommandPaletteHelpActions: React.FC = (props) => { ); -}; +}); diff --git a/web/core/components/workspace/sidebar/help-section.tsx b/web/core/components/workspace/sidebar/help-section.tsx index e2c94e855..43289f02a 100644 --- a/web/core/components/workspace/sidebar/help-section.tsx +++ b/web/core/components/workspace/sidebar/help-section.tsx @@ -10,7 +10,7 @@ import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme, useCommandPalette, useInstance } from "@/hooks/store"; +import { useAppTheme, useCommandPalette, useInstance, useTransient } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import { usePlatformOS } from "@/hooks/use-platform-os"; // components @@ -45,15 +45,14 @@ export const SidebarHelpSection: React.FC = observer( const { toggleShortcutModal } = useCommandPalette(); const { isMobile } = usePlatformOS(); const { config } = useInstance(); + const { isIntercomToggle, toggleIntercom } = useTransient(); // states const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); // refs const helpOptionsRef = useRef(null); const handleCrispWindowShow = () => { - if (window) { - window.$crisp.push(["do", "chat:show"]); - } + toggleIntercom(!isIntercomToggle); }; useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false)); @@ -133,7 +132,7 @@ export const SidebarHelpSection: React.FC = observer( leaveTo="transform opacity-0 scale-95" >
{ + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useTransient must be used within StoreProvider"); + return context.transient; +}; diff --git a/web/core/lib/intercom-provider.tsx b/web/core/lib/intercom-provider.tsx index 0140e4a20..b483c6da2 100644 --- a/web/core/lib/intercom-provider.tsx +++ b/web/core/lib/intercom-provider.tsx @@ -1,10 +1,10 @@ "use client"; import React, { FC, useEffect } from "react"; -import Intercom from "@intercom/messenger-js-sdk"; +import { Intercom, show, hide, onHide } from "@intercom/messenger-js-sdk"; import { observer } from "mobx-react"; // store hooks -import { useUser, useInstance } from "@/hooks/store"; +import { useUser, useInstance, useTransient } from "@/hooks/store"; export type IntercomProviderProps = { children: React.ReactNode; @@ -15,6 +15,16 @@ const IntercomProvider: FC = observer((props) => { // hooks const { data: user } = useUser(); const { config } = useInstance(); + const { isIntercomToggle, toggleIntercom } = useTransient(); + + useEffect(() => { + if (isIntercomToggle) show(); + else hide(); + }, [isIntercomToggle]); + + onHide(() => { + toggleIntercom(false); + }); useEffect(() => { if (user && config?.is_intercom_enabled && config.intercom_app_id) { @@ -23,9 +33,10 @@ const IntercomProvider: FC = observer((props) => { user_id: user.id, name: `${user.first_name} ${user.last_name}`, email: user.email, + hide_default_launcher: true, }); } - }, [user, config]); + }, [user, config, toggleIntercom]); return <>{children}; }); diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index 709076ceb..af38f51b2 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -23,6 +23,7 @@ import { IProjectViewStore, ProjectViewStore } from "./project-view.store"; import { RouterStore, IRouterStore } from "./router.store"; import { IStateStore, StateStore } from "./state.store"; import { ThemeStore, IThemeStore } from "./theme.store"; +import { ITransientStore, TransientStore } from "./transient.store"; import { IUserStore, UserStore } from "./user"; import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace"; @@ -54,6 +55,7 @@ export class CoreRootStore { multipleSelect: IMultipleSelectStore; workspaceNotification: IWorkspaceNotificationStore; favorite: IFavoriteStore; + transient: ITransientStore; constructor() { this.router = new RouterStore(); @@ -81,6 +83,7 @@ export class CoreRootStore { this.projectEstimate = new ProjectEstimateStore(this); this.workspaceNotification = new WorkspaceNotificationStore(this); this.favorite = new FavoriteStore(this); + this.transient = new TransientStore(); } resetOnSignOut() { @@ -110,5 +113,7 @@ export class CoreRootStore { this.multipleSelect = new MultipleSelectStore(); this.projectEstimate = new ProjectEstimateStore(this); this.workspaceNotification = new WorkspaceNotificationStore(this); + this.favorite = new FavoriteStore(this); + this.transient = new TransientStore(); } } diff --git a/web/core/store/transient.store.ts b/web/core/store/transient.store.ts new file mode 100644 index 000000000..c7bacce61 --- /dev/null +++ b/web/core/store/transient.store.ts @@ -0,0 +1,28 @@ +import { action, observable, makeObservable } from "mobx"; + +export interface ITransientStore { + // observables + isIntercomToggle: boolean; + // actions + toggleIntercom: (intercomToggle: boolean) => void; +} + +export class TransientStore implements ITransientStore { + // observables + isIntercomToggle: boolean = false; + + constructor() { + makeObservable(this, { + // observable + isIntercomToggle: observable.ref, + // action + toggleIntercom: action, + }); + } + + /** + * @description Toggle the intercom collapsed state + * @param { boolean } intercomToggle + */ + toggleIntercom = (intercomToggle: boolean) => (this.isIntercomToggle = intercomToggle); +} From 3f9523804b76e66d9332174d60e9e03247bdae30 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:42:13 +0530 Subject: [PATCH 05/57] fix: delete action mutation (#5315) --- web/core/store/cycle.store.ts | 2 +- web/core/store/module.store.ts | 2 +- web/core/store/pages/project-page.store.ts | 2 +- web/core/store/project-view.store.ts | 2 +- web/core/store/project/project.store.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index d8ef84cf6..2845527c2 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -551,9 +551,9 @@ export class CycleStore implements ICycleStore { deleteCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId).then(() => { runInAction(() => { - this.rootStore.favorite.removeFavoriteFromStore(cycleId); delete this.cycleMap[cycleId]; delete this.activeCycleIdMap[cycleId]; + if (this.rootStore.favorite.entityMap[cycleId]) this.rootStore.favorite.removeFavoriteFromStore(cycleId); }); }); diff --git a/web/core/store/module.store.ts b/web/core/store/module.store.ts index e5631eedf..23e5cb012 100644 --- a/web/core/store/module.store.ts +++ b/web/core/store/module.store.ts @@ -405,7 +405,7 @@ export class ModulesStore implements IModuleStore { await this.moduleService.deleteModule(workspaceSlug, projectId, moduleId).then(() => { runInAction(() => { delete this.moduleMap[moduleId]; - this.rootStore.favorite.removeFavoriteFromStore(moduleId); + if (this.rootStore.favorite.entityMap[moduleId]) this.rootStore.favorite.removeFavoriteFromStore(moduleId); }); }); }; diff --git a/web/core/store/pages/project-page.store.ts b/web/core/store/pages/project-page.store.ts index 18487d3b3..0629cc17b 100644 --- a/web/core/store/pages/project-page.store.ts +++ b/web/core/store/pages/project-page.store.ts @@ -261,7 +261,7 @@ export class ProjectPageStore implements IProjectPageStore { await this.service.remove(workspaceSlug, projectId, pageId); runInAction(() => { unset(this.data, [pageId]); - this.rootStore.favorite.removeFavoriteFromStore(pageId); + if (this.rootStore.favorite.entityMap[pageId]) this.rootStore.favorite.removeFavoriteFromStore(pageId); }); } catch (error) { runInAction(() => { diff --git a/web/core/store/project-view.store.ts b/web/core/store/project-view.store.ts index 61721cad7..b9f4e8656 100644 --- a/web/core/store/project-view.store.ts +++ b/web/core/store/project-view.store.ts @@ -271,7 +271,7 @@ export class ProjectViewStore implements IProjectViewStore { await this.viewService.deleteView(workspaceSlug, projectId, viewId).then(() => { runInAction(() => { delete this.viewMap[viewId]; - this.rootStore.favorite.removeFavoriteFromStore(viewId); + if (this.rootStore.favorite.entityMap[viewId]) this.rootStore.favorite.removeFavoriteFromStore(viewId); }); }); }; diff --git a/web/core/store/project/project.store.ts b/web/core/store/project/project.store.ts index fe1f30566..faebabf15 100644 --- a/web/core/store/project/project.store.ts +++ b/web/core/store/project/project.store.ts @@ -401,7 +401,7 @@ export class ProjectStore implements IProjectStore { await this.projectService.deleteProject(workspaceSlug, projectId); runInAction(() => { delete this.projectMap[projectId]; - this.rootStore.favorite.removeFavoriteFromStore(projectId); + if (this.rootStore.favorite.entityMap[projectId]) this.rootStore.favorite.removeFavoriteFromStore(projectId); }); } catch (error) { console.log("Failed to delete project from project store"); From 983769a944ca484d074ddf09f30b13a3dc927d60 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 6 Aug 2024 17:26:20 +0530 Subject: [PATCH 06/57] feat: added endpoint for creating service tokens (#5312) * feat: added endpoint for creating service tokens * fix: removed filtering of APITokens without being a service token --- apiserver/plane/app/urls/api.py | 7 ++++- apiserver/plane/app/views/__init__.py | 6 ++-- apiserver/plane/app/views/api.py | 44 ++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/app/urls/api.py b/apiserver/plane/app/urls/api.py index b77ea8530..592ff53b5 100644 --- a/apiserver/plane/app/urls/api.py +++ b/apiserver/plane/app/urls/api.py @@ -1,5 +1,5 @@ from django.urls import path -from plane.app.views import ApiTokenEndpoint +from plane.app.views import ApiTokenEndpoint, ServiceApiTokenEndpoint urlpatterns = [ # API Tokens @@ -13,5 +13,10 @@ urlpatterns = [ ApiTokenEndpoint.as_view(), name="api-tokens", ), + path( + "workspaces//service-api-tokens/", + ServiceApiTokenEndpoint.as_view(), + name="service-api-tokens", + ), ## End API Tokens ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 0babdf5d8..9d8929fda 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -174,8 +174,10 @@ from .module.archive import ( ModuleArchiveUnarchiveEndpoint, ) -from .api import ApiTokenEndpoint - +from .api import ( + ApiTokenEndpoint, + ServiceApiTokenEndpoint, +) from .page.base import ( PageViewSet, diff --git a/apiserver/plane/app/views/api.py b/apiserver/plane/app/views/api.py index 6cd349b07..fe7259fbb 100644 --- a/apiserver/plane/app/views/api.py +++ b/apiserver/plane/app/views/api.py @@ -45,7 +45,7 @@ class ApiTokenEndpoint(BaseAPIView): def get(self, request, slug, pk=None): if pk is None: api_tokens = APIToken.objects.filter( - user=request.user, workspace__slug=slug + user=request.user, workspace__slug=slug, is_service=False ) serializer = APITokenReadSerializer(api_tokens, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @@ -61,6 +61,7 @@ class ApiTokenEndpoint(BaseAPIView): workspace__slug=slug, user=request.user, pk=pk, + is_service=False, ) api_token.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -78,3 +79,44 @@ class ApiTokenEndpoint(BaseAPIView): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class ServiceApiTokenEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceOwnerPermission, + ] + + def post(self, request, slug): + workspace = Workspace.objects.get(slug=slug) + + api_token = APIToken.objects.filter( + workspace=workspace, + is_service=True, + ).first() + + if api_token: + return Response( + { + "token": str(api_token.token), + }, + status=status.HTTP_200_OK, + ) + else: + # Check the user type + user_type = 1 if request.user.is_bot else 0 + + api_token = APIToken.objects.create( + label=str(uuid4().hex), + description="Service Token", + user=request.user, + workspace=workspace, + user_type=user_type, + is_service=True, + ) + return Response( + { + "token": str(api_token.token), + }, + status=status.HTTP_201_CREATED, + ) + From 976784bc84beac0a363e7ae09fdd691821a31147 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 6 Aug 2024 17:26:40 +0530 Subject: [PATCH 07/57] feat: added `deleted_at` as read-only property for the label serializer (#5306) --- apiserver/plane/api/serializers/issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index fd89a3e05..044aa1581 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -269,6 +269,7 @@ class LabelSerializer(BaseSerializer): "updated_by", "created_at", "updated_at", + "deleted_at", ] @@ -430,4 +431,3 @@ class IssueExpandSerializer(BaseSerializer): "created_at", "updated_at", ] - From 3279bb6ac95267af34a686151c5f55238c957aa2 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:21:53 +0530 Subject: [PATCH 08/57] [WEB-2182] fix: favorite item alignment and redirection (#5316) * fix: favorite item alignment * fix: favorite item redirection * chore: code refactor --- .../sidebar/favorites/favorite-item.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/web/core/components/workspace/sidebar/favorites/favorite-item.tsx b/web/core/components/workspace/sidebar/favorites/favorite-item.tsx index 4ef38c8c2..79cd8dc56 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-item.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-item.tsx @@ -135,7 +135,11 @@ export const FavoriteItem = observer( key={favorite.id} className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`} > -
+
- {getIcon()} - - {!sidebarCollapsed && ( - - {favorite.entity_data ? favorite.entity_data.name : favorite.name} - - )} + + {getIcon()} + {!sidebarCollapsed && ( + + {favorite.entity_data ? favorite.entity_data.name : favorite.name} + + )} + {!sidebarCollapsed && ( Date: Wed, 7 Aug 2024 12:58:24 +0530 Subject: [PATCH 09/57] fix: reloading on favorite action (#5313) --- web/core/store/project/project.store.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web/core/store/project/project.store.ts b/web/core/store/project/project.store.ts index faebabf15..f22a5d47d 100644 --- a/web/core/store/project/project.store.ts +++ b/web/core/store/project/project.store.ts @@ -285,7 +285,6 @@ export class ProjectStore implements IProjectStore { project_id: projectId, entity_data: { name: this.projectMap[projectId].name || "" }, }); - await this.fetchProjects(workspaceSlug); return response; } catch (error) { console.log("Failed to add project to favorite"); @@ -306,12 +305,11 @@ export class ProjectStore implements IProjectStore { try { const currentProject = this.getProjectById(projectId); if (!currentProject.is_favorite) return; - const response = await this.rootStore.favorite.removeFavoriteEntity(workspaceSlug.toString(), projectId); - runInAction(() => { set(this.projectMap, [projectId, "is_favorite"], false); }); - await this.fetchProjects(workspaceSlug); + const response = await this.rootStore.favorite.removeFavoriteEntity(workspaceSlug.toString(), projectId); + return response; } catch (error) { console.log("Failed to add project to favorite"); From 91142659ca31c55667d57da8d984931b7b2fdc1a Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:49:45 +0530 Subject: [PATCH 10/57] [WEB-2192] fix: order of state groups in space app (#5317) * chore: added sequence in the states endpoint * fix state grouping order in space app --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/space/views/state.py | 13 +++++-------- .../core/components/issues/issue-layouts/utils.tsx | 6 +++--- space/core/store/state.store.ts | 13 ++++++++++++- space/helpers/state.helper.ts | 13 +++++++++++++ 4 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 space/helpers/state.helper.ts diff --git a/apiserver/plane/space/views/state.py b/apiserver/plane/space/views/state.py index 853bf022c..7ffcef5b9 100644 --- a/apiserver/plane/space/views/state.py +++ b/apiserver/plane/space/views/state.py @@ -27,14 +27,11 @@ class ProjectStatesEndpoint(BaseAPIView): status=status.HTTP_404_NOT_FOUND, ) - states = ( - State.objects.filter( - ~Q(name="Triage"), - workspace__slug=deploy_board.workspace.slug, - project_id=deploy_board.project_id, - ) - .values("name", "group", "color", "id") - ) + states = State.objects.filter( + ~Q(name="Triage"), + workspace__slug=deploy_board.workspace.slug, + project_id=deploy_board.project_id, + ).values("name", "group", "color", "id", "sequence") return Response( states, diff --git a/space/core/components/issues/issue-layouts/utils.tsx b/space/core/components/issues/issue-layouts/utils.tsx index edb67b4f7..992f6367c 100644 --- a/space/core/components/issues/issue-layouts/utils.tsx +++ b/space/core/components/issues/issue-layouts/utils.tsx @@ -109,10 +109,10 @@ const getModuleColumns = (moduleStore: IIssueModuleStore): IGroupByColumn[] | un }; const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => { - const { states } = projectState; - if (!states) return; + const { sortedStates } = projectState; + if (!sortedStates) return; - return states.map((state) => ({ + return sortedStates.map((state) => ({ id: state.id, name: state.name, icon: ( diff --git a/space/core/store/state.store.ts b/space/core/store/state.store.ts index 36a0ceaf7..aff22a22a 100644 --- a/space/core/store/state.store.ts +++ b/space/core/store/state.store.ts @@ -1,11 +1,15 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; +import clone from "lodash/clone"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { IState } from "@plane/types"; +import { sortStates } from "@/helpers/state.helper"; import { StateService } from "@/services/state.service"; import { CoreRootStore } from "./root.store"; export interface IStateStore { // observables states: IState[] | undefined; + //computed + sortedStates: IState[] | undefined; // computed actions getStateById: (stateId: string | undefined) => IState | undefined; // fetch actions @@ -21,6 +25,8 @@ export class StateStore implements IStateStore { makeObservable(this, { // observables states: observable, + // computed + sortedStates: computed, // fetch action fetchStates: action, }); @@ -28,6 +34,11 @@ export class StateStore implements IStateStore { this.rootStore = _rootStore; } + get sortedStates() { + if (!this.states) return; + return sortStates(clone(this.states)); + } + getStateById = (stateId: string | undefined) => this.states?.find((state) => state.id === stateId); fetchStates = async (anchor: string) => { diff --git a/space/helpers/state.helper.ts b/space/helpers/state.helper.ts new file mode 100644 index 000000000..81bffdef9 --- /dev/null +++ b/space/helpers/state.helper.ts @@ -0,0 +1,13 @@ +import { IState } from "@plane/types"; +import { STATE_GROUPS } from "@/constants/state"; + +export const sortStates = (states: IState[]) => { + if (!states || states.length === 0) return; + + return states.sort((stateA, stateB) => { + if (stateA.group === stateB.group) { + return stateA.sequence - stateB.sequence; + } + return Object.keys(STATE_GROUPS).indexOf(stateA.group) - Object.keys(STATE_GROUPS).indexOf(stateB.group); + }); +}; From 598846adc4282177c16c4fda6243e1493fdc04a8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:28:25 +0530 Subject: [PATCH 11/57] [WEB-2182] chore: user favorites improvement (#5318) * chore: favorite collapsible spacing * chore: favorite collapsible tooltip added * chore: user favorites icon improvement and code refactor * chore: favorites empty state added * chore: project identifier message updated * chore: favorties collapsible improvement * chore: code refactor * fix: build error * fix: app sidebar draft issue z-index --- packages/types/src/favorite/favorite.d.ts | 13 +++ .../project/create-project-form.tsx | 2 +- web/core/components/project/form.tsx | 2 +- .../sidebar/favorites/favorite-item.tsx | 61 ++++++++------ .../sidebar/favorites/favorites-menu.tsx | 82 +++++++++++-------- .../workspace/sidebar/quick-actions.tsx | 2 +- 6 files changed, 99 insertions(+), 63 deletions(-) diff --git a/packages/types/src/favorite/favorite.d.ts b/packages/types/src/favorite/favorite.d.ts index 154d0e266..092a12095 100644 --- a/packages/types/src/favorite/favorite.d.ts +++ b/packages/types/src/favorite/favorite.d.ts @@ -1,9 +1,22 @@ +type TLogoProps = { + in_use: "emoji" | "icon"; + emoji?: { + value?: string; + url?: string; + }; + icon?: { + name?: string; + color?: string; + }; +}; + export type IFavorite = { id: string; name: string; entity_type: string; entity_data: { name: string; + logo_props?: TLogoProps | undefined; }; is_folder: boolean; sort_order: number; diff --git a/web/core/components/project/create-project-form.tsx b/web/core/components/project/create-project-form.tsx index 65c620635..53de72979 100644 --- a/web/core/components/project/create-project-form.tsx +++ b/web/core/components/project/create-project-form.tsx @@ -294,7 +294,7 @@ export const CreateProjectForm: FC = observer((props) => { /> diff --git a/web/core/components/project/form.tsx b/web/core/components/project/form.tsx index 737f81983..332c75284 100644 --- a/web/core/components/project/form.tsx +++ b/web/core/components/project/form.tsx @@ -298,7 +298,7 @@ export const ProjectDetailsForm: FC = (props) => { /> diff --git a/web/core/components/workspace/sidebar/favorites/favorite-item.tsx b/web/core/components/workspace/sidebar/favorites/favorite-item.tsx index 79cd8dc56..f824fc493 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-item.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-item.tsx @@ -9,7 +9,16 @@ import { useParams } from "next/navigation"; import { Briefcase, FileText, Layers, MoreHorizontal, Star } from "lucide-react"; // ui import { IFavorite } from "@plane/types"; -import { ContrastIcon, CustomMenu, DiceIcon, DragHandle, FavoriteFolderIcon, LayersIcon, Tooltip } from "@plane/ui"; +import { + ContrastIcon, + CustomMenu, + DiceIcon, + DragHandle, + FavoriteFolderIcon, + LayersIcon, + Logo, + Tooltip, +} from "@plane/ui"; // components import { SidebarNavItem } from "@/components/sidebar"; @@ -20,6 +29,17 @@ import { useAppTheme } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import { usePlatformOS } from "@/hooks/use-platform-os"; +const iconClassName = `flex-shrink-0 size-4 stroke-[1.5] m-auto`; +const ICONS: Record = { + page: , + project: , + view: , + module: , + cycle: , + issue: , + folder: , +}; + export const FavoriteItem = observer( ({ favoriteMap, @@ -48,28 +68,18 @@ export const FavoriteItem = observer( const dragHandleRef = useRef(null); const actionSectionRef = useRef(null); - const getIcon = () => { - const className = `flex-shrink-0 size-4 stroke-[1.5] m-auto`; - - switch (favorite.entity_type) { - case "page": - return ; - case "project": - return ; - case "view": - return ; - case "module": - return ; - case "cycle": - return ; - case "issue": - return ; - case "folder": - return ; - default: - return ; - } - }; + const getIcon = () => ( + <> +
{ICONS[favorite.entity_type] || }
+
+ {favorite.entity_data?.logo_props?.in_use ? ( + + ) : ( + ICONS[favorite.entity_type] || + )} +
+ + ); const getLink = () => { switch (favorite.entity_type) { @@ -133,7 +143,10 @@ export const FavoriteItem = observer(
{ toggleFavoriteMenu(!isFavoriteMenuOpen)} className="flex-1 text-start"> YOUR FAVORITES - - { - setCreateNewFolder(true); - !isFavoriteMenuOpen && toggleFavoriteMenu(!isFavoriteMenuOpen); - }} - className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform")} - /> + + + { + setCreateNewFolder(true); + !isFavoriteMenuOpen && toggleFavoriteMenu(!isFavoriteMenuOpen); + }} + className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform")} + /> + toggleFavoriteMenu(!isFavoriteMenuOpen)} className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", { @@ -168,34 +170,42 @@ export const SidebarFavoritesMenu = observer(() => { static > {createNewFolder && } - {uniqBy(orderBy(Object.values(favoriteMap), "sequence", "desc"), "id") - .filter((fav) => !fav.parent) - .map((fav, index) => ( - - {fav.is_folder ? ( - - ) : ( - - )} - - ))} + {Object.keys(favoriteMap).length === 0 ? ( + <> + {!sidebarCollapsed && ( + No favorites yet + )} + + ) : ( + uniqBy(orderBy(Object.values(favoriteMap), "sequence", "desc"), "id") + .filter((fav) => !fav.parent) + .map((fav, index) => ( + + {fav.is_folder ? ( + + ) : ( + + )} + + )) + )} )} diff --git a/web/core/components/workspace/sidebar/quick-actions.tsx b/web/core/components/workspace/sidebar/quick-actions.tsx index 30f20e4da..f0eca5cf0 100644 --- a/web/core/components/workspace/sidebar/quick-actions.tsx +++ b/web/core/components/workspace/sidebar/quick-actions.tsx @@ -108,7 +108,7 @@ export const SidebarQuickActions = observer(() => { )} {isDraftButtonOpen && ( -
+
-
+

{favorite.name}

diff --git a/web/core/components/workspace/sidebar/favorites/favorite-item.tsx b/web/core/components/workspace/sidebar/favorites/favorite-item.tsx index f824fc493..348b5c76e 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-item.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-item.tsx @@ -9,17 +9,9 @@ import { useParams } from "next/navigation"; import { Briefcase, FileText, Layers, MoreHorizontal, Star } from "lucide-react"; // ui import { IFavorite } from "@plane/types"; -import { - ContrastIcon, - CustomMenu, - DiceIcon, - DragHandle, - FavoriteFolderIcon, - LayersIcon, - Logo, - Tooltip, -} from "@plane/ui"; +import { ContrastIcon, CustomMenu, DiceIcon, DragHandle, FavoriteFolderIcon, LayersIcon, Tooltip } from "@plane/ui"; // components +import { Logo } from "@/components/common"; import { SidebarNavItem } from "@/components/sidebar"; // helpers @@ -70,10 +62,16 @@ export const FavoriteItem = observer( const getIcon = () => ( <> -
{ICONS[favorite.entity_type] || }
-
+
+ {ICONS[favorite.entity_type] || } +
+
{favorite.entity_data?.logo_props?.in_use ? ( - + ) : ( ICONS[favorite.entity_type] || )} @@ -140,18 +138,27 @@ export const FavoriteItem = observer( useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); return ( -
- + <> + {sidebarCollapsed ? ( +
+ + {getIcon()} + +
+ ) : (
- - - {getIcon()} - {!sidebarCollapsed && ( - - {favorite.entity_data ? favorite.entity_data.name : favorite.name} - - )} + +
{getIcon()}
+ + {favorite.entity_data ? favorite.entity_data.name : favorite.name} + - - {!sidebarCollapsed && ( - setIsMenuActive(!isMenuActive)} - > - - + setIsMenuActive(!isMenuActive)} + > + + + } + className={cn( + "opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto", + { + "opacity-100 pointer-events-auto": isMenuActive, } - className={cn( - "opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto", - { - "opacity-100 pointer-events-auto": isMenuActive, - } - )} - customButtonClassName="grid place-items-center" - placement="bottom-start" - > - handleRemoveFromFavorites(favorite)}> - - - Remove from favorites - - - - )} + )} + customButtonClassName="grid place-items-center" + placement="bottom-start" + > + handleRemoveFromFavorites(favorite)}> + + + Remove from favorites + + +
-
-
+ )} + ); } ); From 520938ab5c59b6736327be93dfb099026eff82d5 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 7 Aug 2024 19:35:00 +0530 Subject: [PATCH 13/57] chore: add rate limiting in magic generate endpoint (#5322) --- apiserver/plane/authentication/views/app/magic.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apiserver/plane/authentication/views/app/magic.py b/apiserver/plane/authentication/views/app/magic.py index 12f8a0c0b..980eb4e7c 100644 --- a/apiserver/plane/authentication/views/app/magic.py +++ b/apiserver/plane/authentication/views/app/magic.py @@ -29,6 +29,7 @@ from plane.authentication.adapter.error import ( AuthenticationException, AUTHENTICATION_ERROR_CODES, ) +from plane.authentication.rate_limit import AuthenticationThrottle class MagicGenerateEndpoint(APIView): @@ -37,6 +38,10 @@ class MagicGenerateEndpoint(APIView): AllowAny, ] + throttle_classes = [ + AuthenticationThrottle, + ] + def post(self, request): # Check if instance is configured instance = Instance.objects.first() From 943dd593fa1c90fa2d59430d293a3a368c4d9133 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 7 Aug 2024 20:06:15 +0530 Subject: [PATCH 14/57] dev: editor extensions feature flagging (#5279) --- .../src/ce/extensions/document-extensions.tsx | 6 +++++- .../components/editors/document/editor.tsx | 5 ++++- .../components/editors/rich-text/editor.tsx | 6 +----- .../src/core/hooks/use-document-editor.ts | 10 ++++++++-- packages/editor/src/core/types/extensions.ts | 1 + packages/editor/src/core/types/index.ts | 1 + .../core/types/slash-commands-suggestion.ts | 3 ++- web/ce/hooks/use-editor-flagging.ts | 20 +++++++++++++++++++ web/ce/hooks/use-issue-embed.tsx | 7 +------ .../components/pages/editor/editor-body.tsx | 14 +++++++------ 10 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 packages/editor/src/core/types/extensions.ts create mode 100644 web/ce/hooks/use-editor-flagging.ts diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx index 57583e60a..0ea842ff4 100644 --- a/packages/editor/src/ce/extensions/document-extensions.tsx +++ b/packages/editor/src/ce/extensions/document-extensions.tsx @@ -1,10 +1,14 @@ +import { Extensions } from "@tiptap/core"; import { SlashCommand } from "@/extensions"; // hooks import { TFileHandler } from "@/hooks/use-editor"; // plane editor types import { TIssueEmbedConfig } from "@/plane-editor/types"; +// types +import { TExtensions } from "@/types"; type Props = { + disabledExtensions?: TExtensions[]; fileHandler: TFileHandler; issueEmbedConfig: TIssueEmbedConfig | undefined; }; @@ -12,7 +16,7 @@ type Props = { export const DocumentEditorAdditionalExtensions = (props: Props) => { const { fileHandler } = props; - const extensions = [SlashCommand(fileHandler.upload)]; + const extensions: Extensions = [SlashCommand(fileHandler.upload)]; return extensions; }; diff --git a/packages/editor/src/core/components/editors/document/editor.tsx b/packages/editor/src/core/components/editors/document/editor.tsx index 6576dacba..503dc76e0 100644 --- a/packages/editor/src/core/components/editors/document/editor.tsx +++ b/packages/editor/src/core/components/editors/document/editor.tsx @@ -9,10 +9,11 @@ import { TFileHandler } from "@/hooks/use-editor"; // plane editor types import { TEmbedConfig } from "@/plane-editor/types"; // types -import { EditorRefApi, IMentionHighlight, IMentionSuggestion } from "@/types"; +import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TExtensions } from "@/types"; interface IDocumentEditor { containerClassName?: string; + disabledExtensions?: TExtensions[]; editorClassName?: string; embedHandler: TEmbedConfig; fileHandler: TFileHandler; @@ -32,6 +33,7 @@ interface IDocumentEditor { const DocumentEditor = (props: IDocumentEditor) => { const { containerClassName, + disabledExtensions, editorClassName = "", embedHandler, fileHandler, @@ -54,6 +56,7 @@ const DocumentEditor = (props: IDocumentEditor) => { // use document editor const { editor, isIndexedDbSynced } = useDocumentEditor({ + disabledExtensions, id, editorClassName, embedHandler, diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index 069681dab..ead73d99d 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -19,11 +19,7 @@ const RichTextEditor = (props: IRichTextEditor) => { }; const getExtensions = useCallback(() => { - const extensions = [ - SlashCommand(fileHandler.upload), - // TODO; add the extension conditionally for forms that don't require it - // EnterKeyExtension(onEnterKeyPress), - ]; + const extensions = [SlashCommand(fileHandler.upload)]; if (dragDropEnabled) extensions.push(DragAndDrop(setHideDragHandleFunction)); diff --git a/packages/editor/src/core/hooks/use-document-editor.ts b/packages/editor/src/core/hooks/use-document-editor.ts index 4aa8c12f7..58a45b747 100644 --- a/packages/editor/src/core/hooks/use-document-editor.ts +++ b/packages/editor/src/core/hooks/use-document-editor.ts @@ -13,9 +13,10 @@ import { CollaborationProvider } from "@/plane-editor/providers"; // plane editor types import { TEmbedConfig } from "@/plane-editor/types"; // types -import { EditorRefApi, IMentionHighlight, IMentionSuggestion } from "@/types"; +import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TExtensions } from "@/types"; type DocumentEditorProps = { + disabledExtensions?: TExtensions[]; editorClassName: string; editorProps?: EditorProps; embedHandler?: TEmbedConfig; @@ -36,6 +37,7 @@ type DocumentEditorProps = { export const useDocumentEditor = (props: DocumentEditorProps) => { const { + disabledExtensions, editorClassName, editorProps = {}, embedHandler, @@ -102,6 +104,7 @@ export const useDocumentEditor = (props: DocumentEditorProps) => { document: provider.document, }), ...DocumentEditorAdditionalExtensions({ + disabledExtensions, fileHandler, issueEmbedConfig: embedHandler?.issue, }), @@ -111,5 +114,8 @@ export const useDocumentEditor = (props: DocumentEditorProps) => { tabIndex, }); - return { editor, isIndexedDbSynced }; + return { + editor, + isIndexedDbSynced, + }; }; diff --git a/packages/editor/src/core/types/extensions.ts b/packages/editor/src/core/types/extensions.ts new file mode 100644 index 000000000..d4a26e803 --- /dev/null +++ b/packages/editor/src/core/types/extensions.ts @@ -0,0 +1 @@ +export type TExtensions = "issue-embed"; diff --git a/packages/editor/src/core/types/index.ts b/packages/editor/src/core/types/index.ts index 9e5980531..891a86286 100644 --- a/packages/editor/src/core/types/index.ts +++ b/packages/editor/src/core/types/index.ts @@ -1,5 +1,6 @@ export * from "./editor"; export * from "./embed"; +export * from "./extensions"; export * from "./image"; export * from "./mention-suggestion"; export * from "./slash-commands-suggestion"; diff --git a/packages/editor/src/core/types/slash-commands-suggestion.ts b/packages/editor/src/core/types/slash-commands-suggestion.ts index f7696f45d..3cb9d76b0 100644 --- a/packages/editor/src/core/types/slash-commands-suggestion.ts +++ b/packages/editor/src/core/types/slash-commands-suggestion.ts @@ -20,7 +20,8 @@ export type TEditorCommands = | "code" | "table" | "image" - | "divider"; + | "divider" + | "issue-embed"; export type CommandProps = { editor: Editor; diff --git a/web/ce/hooks/use-editor-flagging.ts b/web/ce/hooks/use-editor-flagging.ts new file mode 100644 index 000000000..9019db94d --- /dev/null +++ b/web/ce/hooks/use-editor-flagging.ts @@ -0,0 +1,20 @@ +// editor +import { TExtensions } from "@plane/editor"; + +/** + * @description extensions disabled in various editors + * @returns + * ```ts + * { + * documentEditor: TExtensions[] + * richTextEditor: TExtensions[] + * } + * ``` + */ +export const useEditorFlagging = (): { + documentEditor: TExtensions[]; + richTextEditor: TExtensions[]; +} => ({ + documentEditor: [], + richTextEditor: [], +}); diff --git a/web/ce/hooks/use-issue-embed.tsx b/web/ce/hooks/use-issue-embed.tsx index 5ca4d4b02..5d02d978f 100644 --- a/web/ce/hooks/use-issue-embed.tsx +++ b/web/ce/hooks/use-issue-embed.tsx @@ -1,5 +1,5 @@ // editor -import { TEmbedConfig, TReadOnlyEmbedConfig } from "@plane/editor"; +import { TEmbedConfig } from "@plane/editor"; // types import { TPageEmbedType } from "@plane/types"; // plane web components @@ -13,12 +13,7 @@ export const useIssueEmbed = (workspaceSlug: string, projectId: string, queryTyp widgetCallback, }; - const issueEmbedReadOnlyProps: TReadOnlyEmbedConfig["issue"] = { - widgetCallback, - }; - return { issueEmbedProps, - issueEmbedReadOnlyProps, }; }; diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index ee96252fa..4b4c80a3a 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -19,6 +19,7 @@ import { cn } from "@/helpers/common.helper"; import { useMember, useMention, useUser, useWorkspace } from "@/hooks/store"; import { usePageFilters } from "@/hooks/use-page-filters"; // plane web hooks +import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed"; // services import { FileService } from "@/services/file.service"; @@ -72,7 +73,6 @@ export const PageEditorBody: React.FC = observer((props) => { const { isContentEditable, updateTitle, setIsSubmitting } = page; const projectMemberIds = projectId ? getProjectMemberIds(projectId.toString()) : []; const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite); - // use-mention const { mentionHighlights, mentionSuggestions } = useMention({ workspaceSlug: workspaceSlug?.toString() ?? "", @@ -80,14 +80,13 @@ export const PageEditorBody: React.FC = observer((props) => { members: projectMemberDetails, user: currentUser ?? undefined, }); + // editor flaggings + const { documentEditor } = useEditorFlagging(); // page filters const { isFullWidth } = usePageFilters(); // issue-embed - const { issueEmbedProps, issueEmbedReadOnlyProps } = useIssueEmbed( - workspaceSlug?.toString() ?? "", - projectId?.toString() ?? "" - ); + const { issueEmbedProps } = useIssueEmbed(workspaceSlug?.toString() ?? "", projectId?.toString() ?? ""); useEffect(() => { updateMarkings(pageDescription ?? "

"); @@ -149,6 +148,7 @@ export const PageEditorBody: React.FC = observer((props) => { embedHandler={{ issue: issueEmbedProps, }} + disabledExtensions={documentEditor} /> ) : ( = observer((props) => { highlights: mentionHighlights, }} embedHandler={{ - issue: issueEmbedReadOnlyProps, + issue: { + widgetCallback: issueEmbedProps.widgetCallback, + }, }} /> )} From e805c49e692dfb4c3e3f45fd66dc106ab9b286e0 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 8 Aug 2024 14:48:05 +0530 Subject: [PATCH 15/57] [WEB-2047] refactor: editor side menu (#5329) * refactor: editor side menu * chore: change editor side menu selector to be id based --- .../src/ce/extensions/ai-features/handle.ts | 13 + .../src/ce/extensions/ai-features/index.ts | 1 + packages/editor/src/ce/extensions/index.ts | 1 + .../components/editors/document/editor.tsx | 18 +- .../editors/document/page-renderer.tsx | 12 +- .../components/editors/editor-container.tsx | 12 +- .../components/editors/editor-wrapper.tsx | 9 +- .../components/editors/lite-text/editor.tsx | 2 +- .../components/editors/rich-text/editor.tsx | 21 +- packages/editor/src/core/extensions/index.ts | 2 +- .../editor/src/core/extensions/side-menu.tsx | 199 ++++++++++++ .../src/core/hooks/use-document-editor.ts | 9 +- .../drag-drop.tsx => plugins/drag-handle.ts} | 287 +++++++----------- packages/editor/src/core/types/extensions.ts | 2 +- .../src/ee/extensions/ai-features/index.ts | 1 + packages/editor/src/styles/drag-drop.css | 60 +--- 16 files changed, 356 insertions(+), 293 deletions(-) create mode 100644 packages/editor/src/ce/extensions/ai-features/handle.ts create mode 100644 packages/editor/src/ce/extensions/ai-features/index.ts create mode 100644 packages/editor/src/core/extensions/side-menu.tsx rename packages/editor/src/core/{extensions/drag-drop.tsx => plugins/drag-handle.ts} (52%) create mode 100644 packages/editor/src/ee/extensions/ai-features/index.ts diff --git a/packages/editor/src/ce/extensions/ai-features/handle.ts b/packages/editor/src/ce/extensions/ai-features/handle.ts new file mode 100644 index 000000000..d477d228a --- /dev/null +++ b/packages/editor/src/ce/extensions/ai-features/handle.ts @@ -0,0 +1,13 @@ +// extensions +import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const AIHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => { + const view = () => {}; + const domEvents = {}; + + return { + view, + domEvents, + }; +}; diff --git a/packages/editor/src/ce/extensions/ai-features/index.ts b/packages/editor/src/ce/extensions/ai-features/index.ts new file mode 100644 index 000000000..af0faafca --- /dev/null +++ b/packages/editor/src/ce/extensions/ai-features/index.ts @@ -0,0 +1 @@ +export * from "./handle"; diff --git a/packages/editor/src/ce/extensions/index.ts b/packages/editor/src/ce/extensions/index.ts index 4a975b8c5..172d9ee1a 100644 --- a/packages/editor/src/ce/extensions/index.ts +++ b/packages/editor/src/ce/extensions/index.ts @@ -1 +1,2 @@ +export * from "./ai-features"; export * from "./document-extensions"; diff --git a/packages/editor/src/core/components/editors/document/editor.tsx b/packages/editor/src/core/components/editors/document/editor.tsx index 503dc76e0..2ec49acc0 100644 --- a/packages/editor/src/core/components/editors/document/editor.tsx +++ b/packages/editor/src/core/components/editors/document/editor.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; // components import { PageRenderer } from "@/components/editors"; // helpers @@ -46,13 +46,6 @@ const DocumentEditor = (props: IDocumentEditor) => { tabIndex, value, } = props; - // states - const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = useState<() => void>(() => {}); - // this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin - // loads such that we can invoke it from react when the cursor leaves the container - const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => { - setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop); - }; // use document editor const { editor, isIndexedDbSynced } = useDocumentEditor({ @@ -67,7 +60,6 @@ const DocumentEditor = (props: IDocumentEditor) => { forwardedRef, mentionHandler, placeholder, - setHideDragHandleFunction, tabIndex, }); @@ -80,13 +72,7 @@ const DocumentEditor = (props: IDocumentEditor) => { if (!editor || !isIndexedDbSynced) return null; return ( - + ); }; diff --git a/packages/editor/src/core/components/editors/document/page-renderer.tsx b/packages/editor/src/core/components/editors/document/page-renderer.tsx index 9254d52c6..2de5f6c57 100644 --- a/packages/editor/src/core/components/editors/document/page-renderer.tsx +++ b/packages/editor/src/core/components/editors/document/page-renderer.tsx @@ -20,13 +20,12 @@ import { BlockMenu } from "@/components/menus"; type IPageRenderer = { editor: Editor; editorContainerClassName: string; - hideDragHandle?: () => void; id: string; tabIndex?: number; }; export const PageRenderer = (props: IPageRenderer) => { - const { editor, editorContainerClassName, hideDragHandle, id, tabIndex } = props; + const { editor, editorContainerClassName, id, tabIndex } = props; // states const [linkViewProps, setLinkViewProps] = useState(); const [isOpen, setIsOpen] = useState(false); @@ -129,14 +128,9 @@ export const PageRenderer = (props: IPageRenderer) => { return ( <>
- + - {editor && editor.isEditable && } + {editor.isEditable && }
{isOpen && linkViewProps && coordinates && ( diff --git a/packages/editor/src/core/components/editors/editor-container.tsx b/packages/editor/src/core/components/editors/editor-container.tsx index 5c09f42e5..5102cf9ac 100644 --- a/packages/editor/src/core/components/editors/editor-container.tsx +++ b/packages/editor/src/core/components/editors/editor-container.tsx @@ -7,12 +7,11 @@ interface EditorContainerProps { children: ReactNode; editor: Editor | null; editorContainerClassName: string; - hideDragHandle?: () => void; id: string; } export const EditorContainer: FC = (props) => { - const { children, editor, editorContainerClassName, hideDragHandle, id } = props; + const { children, editor, editorContainerClassName, id } = props; const handleContainerClick = () => { if (!editor) return; @@ -53,11 +52,18 @@ export const EditorContainer: FC = (props) => { } }; + const handleContainerMouseLeave = () => { + const dragHandleElement = document.querySelector("#editor-side-menu"); + if (!dragHandleElement?.classList.contains("side-menu-hidden")) { + dragHandleElement?.classList.add("side-menu-hidden"); + } + }; + return (
React.ReactNode; extensions: Extension[]; - hideDragHandleOnMouseLeave: () => void; }; export const EditorWrapper: React.FC = (props) => { @@ -20,7 +19,6 @@ export const EditorWrapper: React.FC = (props) => { containerClassName, editorClassName = "", extensions, - hideDragHandleOnMouseLeave, id, initialValue, fileHandler, @@ -56,12 +54,7 @@ export const EditorWrapper: React.FC = (props) => { if (!editor) return null; return ( - + {children?.(editor)}
diff --git a/packages/editor/src/core/components/editors/lite-text/editor.tsx b/packages/editor/src/core/components/editors/lite-text/editor.tsx index 0ef708022..924706aae 100644 --- a/packages/editor/src/core/components/editors/lite-text/editor.tsx +++ b/packages/editor/src/core/components/editors/lite-text/editor.tsx @@ -11,7 +11,7 @@ const LiteTextEditor = (props: ILiteTextEditor) => { const extensions = [EnterKeyExtension(onEnterKeyPress)]; - return {}} />; + return ; }; const LiteTextEditorWithRef = forwardRef((props, ref) => ( diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index ead73d99d..282042372 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -1,33 +1,30 @@ -import { forwardRef, useCallback, useState } from "react"; +import { forwardRef, useCallback } from "react"; // components import { EditorWrapper } from "@/components/editors"; import { EditorBubbleMenu } from "@/components/menus"; // extensions -import { DragAndDrop, SlashCommand } from "@/extensions"; +import { SideMenuExtension, SlashCommand } from "@/extensions"; // types import { EditorRefApi, IRichTextEditor } from "@/types"; const RichTextEditor = (props: IRichTextEditor) => { const { dragDropEnabled, fileHandler } = props; - // states - const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = useState<() => void>(() => {}); - - // this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin - // loads such that we can invoke it from react when the cursor leaves the container - const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => { - setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop); - }; const getExtensions = useCallback(() => { const extensions = [SlashCommand(fileHandler.upload)]; - if (dragDropEnabled) extensions.push(DragAndDrop(setHideDragHandleFunction)); + extensions.push( + SideMenuExtension({ + aiEnabled: false, + dragDropEnabled: !!dragDropEnabled, + }) + ); return extensions; }, [dragDropEnabled, fileHandler.upload]); return ( - + {(editor) => <>{editor && }} ); diff --git a/packages/editor/src/core/extensions/index.ts b/packages/editor/src/core/extensions/index.ts index 220a11757..9c9e74ff9 100644 --- a/packages/editor/src/core/extensions/index.ts +++ b/packages/editor/src/core/extensions/index.ts @@ -10,7 +10,6 @@ export * from "./typography"; export * from "./core-without-props"; export * from "./document-without-props"; export * from "./custom-code-inline"; -export * from "./drag-drop"; export * from "./drop"; export * from "./enter-key-extension"; export * from "./extensions"; @@ -18,4 +17,5 @@ export * from "./horizontal-rule"; export * from "./keymap"; export * from "./quote"; export * from "./read-only-extensions"; +export * from "./side-menu"; export * from "./slash-commands"; diff --git a/packages/editor/src/core/extensions/side-menu.tsx b/packages/editor/src/core/extensions/side-menu.tsx new file mode 100644 index 000000000..c382469a3 --- /dev/null +++ b/packages/editor/src/core/extensions/side-menu.tsx @@ -0,0 +1,199 @@ +import { Extension } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { EditorView } from "@tiptap/pm/view"; +// plane editor extensions +import { AIHandlePlugin } from "@/plane-editor/extensions"; +import { DragHandlePlugin } from "@/plugins/drag-handle"; + +type Props = { + aiEnabled: boolean; + dragDropEnabled: boolean; +}; + +export type SideMenuPluginProps = { + dragHandleWidth: number; + handlesConfig: { + ai: boolean; + dragDrop: boolean; + }; + scrollThreshold: { + up: number; + down: number; + }; +}; + +export type SideMenuHandleOptions = { + view: (view: EditorView, sideMenu: HTMLDivElement | null) => void; + domEvents?: { + [key: string]: (...args: any) => void; + }; +}; + +export const SideMenuExtension = (props: Props) => { + const { aiEnabled, dragDropEnabled } = props; + + return Extension.create({ + name: "editorSideMenu", + addProseMirrorPlugins() { + return [ + SideMenu({ + dragHandleWidth: 24, + handlesConfig: { + ai: aiEnabled, + dragDrop: dragDropEnabled, + }, + scrollThreshold: { up: 300, down: 100 }, + }), + ]; + }, + }); +}; + +const absoluteRect = (node: Element) => { + const data = node.getBoundingClientRect(); + + return { + top: data.top, + left: data.left, + width: data.width, + }; +}; + +const nodeDOMAtCoords = (coords: { x: number; y: number }) => { + const elements = document.elementsFromPoint(coords.x, coords.y); + const generalSelectors = [ + "li", + "p:not(:first-child)", + ".code-block", + "blockquote", + "img", + "h1, h2, h3, h4, h5, h6", + "[data-type=horizontalRule]", + ".table-wrapper", + ".issue-embed", + ].join(", "); + + for (const elem of elements) { + if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) { + return elem; + } + + // if the element is a

tag that is the first child of a td or th + if ( + (elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) && + elem?.textContent?.trim() !== "" + ) { + return elem; // Return only if p tag is not empty in td or th + } + + // apply general selector + if (elem.matches(generalSelectors)) { + return elem; + } + } + return null; +}; + +const SideMenu = (options: SideMenuPluginProps) => { + const { handlesConfig } = options; + const editorSideMenu: HTMLDivElement | null = document.createElement("div"); + editorSideMenu.id = "editor-side-menu"; + // side menu view actions + const hideSideMenu = () => { + if (!editorSideMenu?.classList.contains("side-menu-hidden")) editorSideMenu?.classList.add("side-menu-hidden"); + }; + const showSideMenu = () => editorSideMenu?.classList.remove("side-menu-hidden"); + // side menu elements + const { view: dragHandleView, domEvents: dragHandleDOMEvents } = DragHandlePlugin(options); + const { view: aiHandleView } = AIHandlePlugin(options); + + return new Plugin({ + key: new PluginKey("sideMenu"), + view: (view) => { + hideSideMenu(); + view?.dom.parentElement?.appendChild(editorSideMenu); + // side menu elements' initialization + if (handlesConfig.dragDrop) { + dragHandleView(view, editorSideMenu); + } + if (handlesConfig.ai) { + aiHandleView(view, editorSideMenu); + } + + return { + destroy: () => hideSideMenu(), + }; + }, + props: { + handleDOMEvents: { + mousemove: (view, event) => { + if (!view.editable) return; + + const node = nodeDOMAtCoords({ + x: event.clientX + 50 + options.dragHandleWidth, + y: event.clientY, + }); + + if (!(node instanceof Element) || node.matches("ul, ol")) { + hideSideMenu(); + return; + } + + const compStyle = window.getComputedStyle(node); + const lineHeight = parseInt(compStyle.lineHeight, 10); + const paddingTop = parseInt(compStyle.paddingTop, 10); + + const rect = absoluteRect(node); + + rect.top += (lineHeight - 20) / 2; + rect.top += paddingTop; + + if (node.parentElement?.parentElement?.matches("td") || node.parentElement?.parentElement?.matches("th")) { + if (node.matches("ul:not([data-type=taskList]) li, ol li")) { + rect.left -= 5; + } + } else { + // Li markers + if (node.matches("ul:not([data-type=taskList]) li, ol li")) { + rect.left -= 18; + } + } + + if (node.matches(".table-wrapper")) { + rect.top += 8; + rect.left -= 8; + } + + if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) { + rect.left += 8; + } + + rect.width = options.dragHandleWidth; + + if (!editorSideMenu) return; + + editorSideMenu.style.left = `${rect.left - rect.width}px`; + editorSideMenu.style.top = `${rect.top}px`; + showSideMenu(); + }, + keydown: () => hideSideMenu(), + mousewheel: () => hideSideMenu(), + dragenter: (view) => { + if (handlesConfig.dragDrop) { + dragHandleDOMEvents?.dragenter?.(view); + } + }, + drop: (view, event) => { + if (handlesConfig.dragDrop) { + dragHandleDOMEvents?.drop?.(view, event); + } + }, + dragend: (view) => { + if (handlesConfig.dragDrop) { + dragHandleDOMEvents?.dragend?.(view); + } + }, + }, + }, + }); +}; diff --git a/packages/editor/src/core/hooks/use-document-editor.ts b/packages/editor/src/core/hooks/use-document-editor.ts index 58a45b747..21b224b9d 100644 --- a/packages/editor/src/core/hooks/use-document-editor.ts +++ b/packages/editor/src/core/hooks/use-document-editor.ts @@ -3,7 +3,7 @@ import Collaboration from "@tiptap/extension-collaboration"; import { EditorProps } from "@tiptap/pm/view"; import * as Y from "yjs"; // extensions -import { DragAndDrop, IssueWidget } from "@/extensions"; +import { IssueWidget, SideMenuExtension } from "@/extensions"; // hooks import { TFileHandler, useEditor } from "@/hooks/use-editor"; // plane editor extensions @@ -30,7 +30,6 @@ type DocumentEditorProps = { }; onChange: (updates: Uint8Array) => void; placeholder?: string | ((isFocused: boolean, value: string) => string); - setHideDragHandleFunction: (hideDragHandlerFromDragDrop: () => void) => void; tabIndex?: number; value: Uint8Array; }; @@ -48,7 +47,6 @@ export const useDocumentEditor = (props: DocumentEditorProps) => { mentionHandler, onChange, placeholder, - setHideDragHandleFunction, tabIndex, value, } = props; @@ -95,7 +93,10 @@ export const useDocumentEditor = (props: DocumentEditorProps) => { forwardedRef, mentionHandler, extensions: [ - DragAndDrop(setHideDragHandleFunction), + SideMenuExtension({ + aiEnabled: !disabledExtensions?.includes("ai"), + dragDropEnabled: true, + }), embedHandler?.issue && IssueWidget({ widgetCallback: embedHandler.issue.widgetCallback, diff --git a/packages/editor/src/core/extensions/drag-drop.tsx b/packages/editor/src/core/plugins/drag-handle.ts similarity index 52% rename from packages/editor/src/core/extensions/drag-drop.tsx rename to packages/editor/src/core/plugins/drag-handle.ts index 458ccbf70..32b6301d5 100644 --- a/packages/editor/src/core/extensions/drag-drop.tsx +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -1,50 +1,33 @@ -import { Extension } from "@tiptap/core"; import { Fragment, Slice, Node } from "@tiptap/pm/model"; -import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state"; +import { NodeSelection, TextSelection } from "@tiptap/pm/state"; // @ts-expect-error __serializeForClipboard's is not exported import { __serializeForClipboard, EditorView } from "@tiptap/pm/view"; +// extensions +import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions"; -export interface DragHandleOptions { - dragHandleWidth: number; - setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void; - scrollThreshold: { - up: number; - down: number; - }; -} - -export const DragAndDrop = (setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void) => - Extension.create({ - name: "dragAndDrop", - - addProseMirrorPlugins() { - return [ - DragHandle({ - dragHandleWidth: 24, - scrollThreshold: { up: 300, down: 100 }, - setHideDragHandle, - }), - ]; - }, - }); +const dragHandleClassName = + "hidden sm:grid place-items-center size-5 aspect-square rounded-sm cursor-grab outline-none hover:bg-custom-background-80 active:bg-custom-background-80 active:cursor-grabbing transition-colors duration-200 ease-linear"; +const dragHandleContainerClassName = "size-[15px] grid place-items-center"; +const dragHandleDotsClassName = "h-full w-3 grid grid-cols-2 place-items-center"; +const dragHandleDotClassName = "size-[2.5px] bg-custom-text-300 rounded-[50%]"; const createDragHandleElement = (): HTMLElement => { const dragHandleElement = document.createElement("button"); dragHandleElement.type = "button"; dragHandleElement.draggable = true; dragHandleElement.dataset.dragHandle = ""; - dragHandleElement.classList.add("drag-handle"); + dragHandleElement.classList.value = dragHandleClassName; const dragHandleContainer = document.createElement("span"); - dragHandleContainer.classList.add("drag-handle-container"); + dragHandleContainer.classList.value = dragHandleContainerClassName; dragHandleElement.appendChild(dragHandleContainer); const dotsContainer = document.createElement("span"); - dotsContainer.classList.add("drag-handle-dots"); + dotsContainer.classList.value = dragHandleDotsClassName; for (let i = 0; i < 6; i++) { const spanElement = document.createElement("span"); - spanElement.classList.add("drag-handle-dot"); + spanElement.classList.value = dragHandleDotClassName; dotsContainer.appendChild(spanElement); } @@ -53,16 +36,6 @@ const createDragHandleElement = (): HTMLElement => { return dragHandleElement; }; -const absoluteRect = (node: Element) => { - const data = node.getBoundingClientRect(); - - return { - top: data.top, - left: data.left, - width: data.width, - }; -}; - const nodeDOMAtCoords = (coords: { x: number; y: number }) => { const elements = document.elementsFromPoint(coords.x, coords.y); const generalSelectors = [ @@ -98,7 +71,7 @@ const nodeDOMAtCoords = (coords: { x: number; y: number }) => { return null; }; -const nodePosAtDOM = (node: Element, view: EditorView, options: DragHandleOptions) => { +const nodePosAtDOM = (node: Element, view: EditorView, options: SideMenuPluginProps) => { const boundingRect = node.getBoundingClientRect(); return view.posAtCoords({ @@ -132,7 +105,7 @@ const calcNodePos = (pos: number, view: EditorView, node: Element) => { return safePos; }; -const DragHandle = (options: DragHandleOptions) => { +export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => { let listType = ""; const handleDragStart = (event: DragEvent, view: EditorView) => { view.focus(); @@ -222,15 +195,15 @@ const DragHandle = (options: DragHandleOptions) => { if (!(node instanceof Element)) return; if (node.matches("blockquote")) { - let nodePosForBlockquotes = nodePosAtDOMForBlockQuotes(node, view); - if (nodePosForBlockquotes === null || nodePosForBlockquotes === undefined) return; + let nodePosForBlockQuotes = nodePosAtDOMForBlockQuotes(node, view); + if (nodePosForBlockQuotes === null || nodePosForBlockQuotes === undefined) return; const docSize = view.state.doc.content.size; - nodePosForBlockquotes = Math.max(0, Math.min(nodePosForBlockquotes, docSize)); + nodePosForBlockQuotes = Math.max(0, Math.min(nodePosForBlockQuotes, docSize)); - if (nodePosForBlockquotes >= 0 && nodePosForBlockquotes <= docSize) { + if (nodePosForBlockQuotes >= 0 && nodePosForBlockQuotes <= docSize) { // TODO FIX ERROR - const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockquotes); + const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockQuotes); view.dispatch(view.state.tr.setSelection(nodeSelection)); } return; @@ -253,152 +226,96 @@ const DragHandle = (options: DragHandleOptions) => { let dragHandleElement: HTMLElement | null = null; // drag handle view actions - const hideDragHandle = () => dragHandleElement?.classList.add("drag-handle-hidden"); - const showDragHandle = () => dragHandleElement?.classList.remove("drag-handle-hidden"); + const hideDragHandle = () => { + if (!dragHandleElement?.classList.contains("drag-handle-hidden")) + dragHandleElement?.classList.add("drag-handle-hidden"); + }; - options.setHideDragHandle?.(hideDragHandle); + const view = (view: EditorView, sideMenu: HTMLDivElement | null) => { + dragHandleElement = createDragHandleElement(); + dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view)); + dragHandleElement.addEventListener("click", (e) => handleClick(e, view)); + dragHandleElement.addEventListener("contextmenu", (e) => handleClick(e, view)); - return new Plugin({ - key: new PluginKey("dragHandle"), - view: (view) => { - dragHandleElement = createDragHandleElement(); - dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view)); - dragHandleElement.addEventListener("click", (e) => handleClick(e, view)); - dragHandleElement.addEventListener("contextmenu", (e) => handleClick(e, view)); + dragHandleElement.addEventListener("drag", (e) => { + hideDragHandle(); + const frameRenderer = document.querySelector(".frame-renderer"); + if (!frameRenderer) return; + if (e.clientY < options.scrollThreshold.up) { + frameRenderer.scrollBy({ top: -70, behavior: "smooth" }); + } else if (window.innerHeight - e.clientY < options.scrollThreshold.down) { + frameRenderer.scrollBy({ top: 70, behavior: "smooth" }); + } + }); - dragHandleElement.addEventListener("drag", (e) => { - hideDragHandle(); - const frameRenderer = document.querySelector(".frame-renderer"); - if (!frameRenderer) return; - if (e.clientY < options.scrollThreshold.up) { - frameRenderer.scrollBy({ top: -70, behavior: "smooth" }); - } else if (window.innerHeight - e.clientY < options.scrollThreshold.down) { - frameRenderer.scrollBy({ top: 70, behavior: "smooth" }); - } + hideDragHandle(); + + sideMenu?.appendChild(dragHandleElement); + + return { + destroy: () => { + dragHandleElement?.remove?.(); + dragHandleElement = null; + }, + }; + }; + const domEvents = { + dragenter: (view: EditorView) => { + view.dom.classList.add("dragging"); + hideDragHandle(); + }, + drop: (view: EditorView, event: DragEvent) => { + view.dom.classList.remove("dragging"); + hideDragHandle(); + let droppedNode: Node | null = null; + const dropPos = view.posAtCoords({ + left: event.clientX, + top: event.clientY, }); - hideDragHandle(); + if (!dropPos) return; - view?.dom?.parentElement?.appendChild(dragHandleElement); + if (view.state.selection instanceof NodeSelection) { + droppedNode = view.state.selection.node; + } - return { - destroy: () => { - dragHandleElement?.remove?.(); - dragHandleElement = null; - }, - }; + if (!droppedNode) return; + + const resolvedPos = view.state.doc.resolve(dropPos.pos); + let isDroppedInsideList = false; + + // Traverse up the document tree to find if we're inside a list item + for (let i = resolvedPos.depth; i > 0; i--) { + if (resolvedPos.node(i).type.name === "listItem") { + isDroppedInsideList = true; + break; + } + } + + // If the selected node is a list item and is not dropped inside a list, we need to wrap it inside

    tag otherwise ol list items will be transformed into ul list item when dropped + if ( + view.state.selection instanceof NodeSelection && + view.state.selection.node.type.name === "listItem" && + !isDroppedInsideList && + listType == "OL" + ) { + const text = droppedNode.textContent; + if (!text) return; + const paragraph = view.state.schema.nodes.paragraph?.createAndFill({}, view.state.schema.text(text)); + const listItem = view.state.schema.nodes.listItem?.createAndFill({}, paragraph); + + const newList = view.state.schema.nodes.orderedList?.createAndFill(null, listItem); + const slice = new Slice(Fragment.from(newList), 0, 0); + view.dragging = { slice, move: event.ctrlKey }; + } }, - props: { - handleDOMEvents: { - mousemove: (view, event) => { - if (!view.editable) return; - - const node = nodeDOMAtCoords({ - x: event.clientX + 50 + options.dragHandleWidth, - y: event.clientY, - }); - - if (!(node instanceof Element) || node.matches("ul, ol")) { - hideDragHandle(); - return; - } - - const compStyle = window.getComputedStyle(node); - const lineHeight = parseInt(compStyle.lineHeight, 10); - const paddingTop = parseInt(compStyle.paddingTop, 10); - - const rect = absoluteRect(node); - - rect.top += (lineHeight - 20) / 2; - rect.top += paddingTop; - - if (node.parentElement?.parentElement?.matches("td") || node.parentElement?.parentElement?.matches("th")) { - if (node.matches("ul:not([data-type=taskList]) li, ol li")) { - rect.left -= 5; - } - } else { - // Li markers - if (node.matches("ul:not([data-type=taskList]) li, ol li")) { - rect.left -= 18; - } - } - - if (node.matches(".table-wrapper")) { - rect.top += 8; - rect.left -= 8; - } - - if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) { - rect.left += 8; - } - - rect.width = options.dragHandleWidth; - - if (!dragHandleElement) return; - - dragHandleElement.style.left = `${rect.left - rect.width}px`; - dragHandleElement.style.top = `${rect.top}px`; - showDragHandle(); - }, - keydown: () => { - hideDragHandle(); - }, - mousewheel: () => { - hideDragHandle(); - }, - dragenter: (view) => { - view.dom.classList.add("dragging"); - hideDragHandle(); - }, - drop: (view, event) => { - view.dom.classList.remove("dragging"); - hideDragHandle(); - let droppedNode: Node | null = null; - const dropPos = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - - if (!dropPos) return; - - if (view.state.selection instanceof NodeSelection) { - droppedNode = view.state.selection.node; - } - - if (!droppedNode) return; - - const resolvedPos = view.state.doc.resolve(dropPos.pos); - let isDroppedInsideList = false; - - // Traverse up the document tree to find if we're inside a list item - for (let i = resolvedPos.depth; i > 0; i--) { - if (resolvedPos.node(i).type.name === "listItem") { - isDroppedInsideList = true; - break; - } - } - - // If the selected node is a list item and is not dropped inside a list, we need to wrap it inside
      tag otherwise ol list items will be transformed into ul list item when dropped - if ( - view.state.selection instanceof NodeSelection && - view.state.selection.node.type.name === "listItem" && - !isDroppedInsideList && - listType == "OL" - ) { - const text = droppedNode.textContent; - if (!text) return; - const paragraph = view.state.schema.nodes.paragraph?.createAndFill({}, view.state.schema.text(text)); - const listItem = view.state.schema.nodes.listItem?.createAndFill({}, paragraph); - - const newList = view.state.schema.nodes.orderedList?.createAndFill(null, listItem); - const slice = new Slice(Fragment.from(newList), 0, 0); - view.dragging = { slice, move: event.ctrlKey }; - } - }, - dragend: (view) => { - view.dom.classList.remove("dragging"); - }, - }, + dragend: (view: EditorView) => { + view.dom.classList.remove("dragging"); }, - }); + }; + + return { + view, + domEvents, + }; }; diff --git a/packages/editor/src/core/types/extensions.ts b/packages/editor/src/core/types/extensions.ts index d4a26e803..4f86b4120 100644 --- a/packages/editor/src/core/types/extensions.ts +++ b/packages/editor/src/core/types/extensions.ts @@ -1 +1 @@ -export type TExtensions = "issue-embed"; +export type TExtensions = "ai" | "issue-embed"; diff --git a/packages/editor/src/ee/extensions/ai-features/index.ts b/packages/editor/src/ee/extensions/ai-features/index.ts new file mode 100644 index 000000000..5e2283046 --- /dev/null +++ b/packages/editor/src/ee/extensions/ai-features/index.ts @@ -0,0 +1 @@ +export * from "src/ce/extensions/ai-features"; diff --git a/packages/editor/src/styles/drag-drop.css b/packages/editor/src/styles/drag-drop.css index c0970630b..c205c0699 100644 --- a/packages/editor/src/styles/drag-drop.css +++ b/packages/editor/src/styles/drag-drop.css @@ -1,66 +1,20 @@ -/* drag handle */ -.drag-handle { +/* side menu */ +#editor-side-menu { position: fixed; - opacity: 1; - height: 20px; - width: 20px; - aspect-ratio: 1 / 1; - display: grid; - place-items: center; - z-index: 5; - cursor: grab; - border-radius: 2px; - outline: none !important; + display: flex; + align-items: center; + opacity: 100; transition: opacity 0.2s ease 0.2s, - background-color 0.2s ease, top 0.2s ease, left 0.2s ease; - &:hover { - background-color: rgba(var(--color-background-80)); - } - - &:active { - background-color: rgba(var(--color-background-80)); - cursor: grabbing; - } - - &.drag-handle-hidden { + &.side-menu-hidden { opacity: 0; pointer-events: none; } } - -@media screen and (max-width: 600px) { - .drag-handle { - display: none; - pointer-events: none; - } -} - -.drag-handle-container { - height: 15px; - width: 15px; - display: grid; - place-items: center; -} - -.drag-handle-dots { - height: 100%; - width: 12px; - display: grid; - grid-template-columns: repeat(2, 1fr); - place-items: center; -} - -.drag-handle-dot { - height: 2.5px; - width: 2.5px; - background-color: rgba(var(--color-text-300)); - border-radius: 50%; -} -/* end drag handle */ +/* end side menu */ .ProseMirror:not(.dragging) .ProseMirror-selectednode { position: relative; From be82cbb8e818408b0f2bc9bbb9fd122e8a210ad7 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:41:49 +0530 Subject: [PATCH 16/57] [WEB-2047] chore: add missing exports (#5334) * chore: add missing exports * chore: delete unnecessary files --- packages/editor/src/ee/extensions/ai-features/index.ts | 1 - web/ee/hooks/use-editor-flagging.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 packages/editor/src/ee/extensions/ai-features/index.ts create mode 100644 web/ee/hooks/use-editor-flagging.ts diff --git a/packages/editor/src/ee/extensions/ai-features/index.ts b/packages/editor/src/ee/extensions/ai-features/index.ts deleted file mode 100644 index 5e2283046..000000000 --- a/packages/editor/src/ee/extensions/ai-features/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "src/ce/extensions/ai-features"; diff --git a/web/ee/hooks/use-editor-flagging.ts b/web/ee/hooks/use-editor-flagging.ts new file mode 100644 index 000000000..13509de63 --- /dev/null +++ b/web/ee/hooks/use-editor-flagging.ts @@ -0,0 +1 @@ +export * from "ce/hooks/use-editor-flagging"; From 1b624ef3ac54125e2f1524b8a0c820593d473f69 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:43:45 +0530 Subject: [PATCH 17/57] fix: work log activity validation (#5332) --- .../components/inbox/content/issue-root.tsx | 2 +- .../issues/issue-detail/issue-activity/root.tsx | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx index 3db2f9fa0..a95be412d 100644 --- a/web/core/components/inbox/content/issue-root.tsx +++ b/web/core/components/inbox/content/issue-root.tsx @@ -160,7 +160,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => { />
      - +
      ); diff --git a/web/core/components/issues/issue-detail/issue-activity/root.tsx b/web/core/components/issues/issue-detail/issue-activity/root.tsx index dad83881d..3af87e770 100644 --- a/web/core/components/issues/issue-detail/issue-activity/root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/root.tsx @@ -21,6 +21,7 @@ type TIssueActivity = { projectId: string; issueId: string; disabled?: boolean; + isIntakeIssue?: boolean; }; export type TActivityOperations = { @@ -30,7 +31,7 @@ export type TActivityOperations = { }; export const IssueActivity: FC = observer((props) => { - const { workspaceSlug, projectId, issueId, disabled = false } = props; + const { workspaceSlug, projectId, issueId, disabled = false, isIntakeIssue = false } = props; // hooks const { createComment, updateComment, removeComment } = useIssueDetail(); const { getProjectById } = useProject(); @@ -114,12 +115,14 @@ export const IssueActivity: FC = observer((props) => {
      Activity
      - + {!isIntakeIssue && ( + + )}
      From 3b2101815433b13a045551d56a925a8f39fdbf95 Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:00:15 +0530 Subject: [PATCH 18/57] fix issue description in space app's peek overview (#5328) --- space/core/components/issues/peek-overview/layout.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/space/core/components/issues/peek-overview/layout.tsx b/space/core/components/issues/peek-overview/layout.tsx index e649a3279..0e4d0b85f 100644 --- a/space/core/components/issues/peek-overview/layout.tsx +++ b/space/core/components/issues/peek-overview/layout.tsx @@ -7,7 +7,7 @@ import { Dialog, Transition } from "@headlessui/react"; // components import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overview"; // hooks -import { useIssue, useIssueDetails } from "@/hooks/store"; +import { useIssueDetails } from "@/hooks/store"; type TIssuePeekOverview = { anchor: string; @@ -29,15 +29,14 @@ export const IssuePeekOverview: FC = observer((props) => { const [isModalPeekOpen, setIsModalPeekOpen] = useState(false); // store const issueDetailStore = useIssueDetails(); - const issueStore = useIssue(); const issueDetails = issueDetailStore.peekId && peekId ? issueDetailStore.details[peekId.toString()] : undefined; useEffect(() => { - if (anchor && peekId && issueStore.groupedIssueIds) { + if (anchor && peekId) { issueDetailStore.fetchIssueDetails(anchor, peekId.toString()); } - }, [anchor, issueDetailStore, peekId, issueStore.groupedIssueIds]); + }, [anchor, issueDetailStore, peekId]); const handleClose = () => { // if close logic is passed down, call that instead of the below logic From a2098ffb5e50b5f01f2117697c462edf52778fc5 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 8 Aug 2024 17:13:52 +0530 Subject: [PATCH 19/57] chore: made cursor update on created_by in issue poprities pane in issue deatil, and issue peekoverview (#5331) --- web/core/components/issues/issue-detail/sidebar.tsx | 2 +- web/core/components/issues/peek-overview/properties.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index 1cde1e17e..076cc5a03 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -131,7 +131,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { Created by
-
+
{createdByDetails?.display_name}
diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index ad489b055..13d252cee 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -133,7 +133,7 @@ export const PeekOverviewProperties: FC = observer((pro Created by
-
+
{createdByDetails?.display_name}
From 48cb0f5afc644dacd6fb7310f7464128a41e6c9c Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:11:18 +0530 Subject: [PATCH 20/57] [WEB-2202] chore: user favorites mutation and code refactor (#5330) * chore: fav item drag and drop improvement * chore: user favorite type updated * chore: user favorites helper function added * dev: favorite item common component added * dev: favorite item component added and code refactor * fix: build error * chore: code refactor * chore: code refactor * chore: code refactor --- packages/types/src/favorite/favorite.d.ts | 1 + .../sidebar/favorites/favorite-folder.tsx | 5 +- .../sidebar/favorites/favorite-item.tsx | 220 ------------------ .../common/favorite-item-drag-handle.tsx | 41 ++++ .../common/favorite-item-quick-action.tsx | 48 ++++ .../common/favorite-item-title.tsx | 25 ++ .../common/favorite-item-wrapper.tsx | 34 +++ .../favorite-items/common/helper.tsx | 49 ++++ .../favorites/favorite-items/common/index.ts | 5 + .../sidebar/favorites/favorite-items/index.ts | 2 + .../sidebar/favorites/favorite-items/root.tsx | 106 +++++++++ .../sidebar/favorites/favorites-menu.tsx | 5 +- .../workspace/sidebar/favorites/index.ts | 5 + .../components/workspace/sidebar/index.ts | 1 + web/core/hooks/use-favorite-item-details.tsx | 58 +++++ 15 files changed, 381 insertions(+), 224 deletions(-) delete mode 100644 web/core/components/workspace/sidebar/favorites/favorite-item.tsx create mode 100644 web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx create mode 100644 web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx create mode 100644 web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx create mode 100644 web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx create mode 100644 web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx create mode 100644 web/core/components/workspace/sidebar/favorites/favorite-items/common/index.ts create mode 100644 web/core/components/workspace/sidebar/favorites/favorite-items/index.ts create mode 100644 web/core/components/workspace/sidebar/favorites/favorite-items/root.tsx create mode 100644 web/core/components/workspace/sidebar/favorites/index.ts create mode 100644 web/core/hooks/use-favorite-item-details.tsx diff --git a/packages/types/src/favorite/favorite.d.ts b/packages/types/src/favorite/favorite.d.ts index 092a12095..c8ec2509d 100644 --- a/packages/types/src/favorite/favorite.d.ts +++ b/packages/types/src/favorite/favorite.d.ts @@ -15,6 +15,7 @@ export type IFavorite = { name: string; entity_type: string; entity_data: { + id?: string; name: string; logo_props?: TLogoProps | undefined; }; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx index 046bd4f81..63a683c5f 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx @@ -20,7 +20,7 @@ import { useFavorite } from "@/hooks/store/use-favorite"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import { usePlatformOS } from "@/hooks/use-platform-os"; // constants -import { FavoriteItem } from "./favorite-item"; +import { FavoriteRoot } from "./favorite-items"; import { getDestinationStateSequence } from "./favorites.helpers"; import { NewFavoriteFolder } from "./new-fav-folder"; @@ -314,8 +314,9 @@ export const FavoriteFolder: React.FC = (props) => { })} > {favorite.children.map((child) => ( - = { - page: , - project: , - view: , - module: , - cycle: , - issue: , - folder: , -}; - -export const FavoriteItem = observer( - ({ - favoriteMap, - favorite, - handleRemoveFromFavorites, - handleRemoveFromFavoritesFolder, - }: { - favorite: IFavorite; - favoriteMap: Record; - handleRemoveFromFavorites: (favorite: IFavorite) => void; - handleRemoveFromFavoritesFolder: (favoriteId: string) => void; - }) => { - // store hooks - const { sidebarCollapsed } = useAppTheme(); - const { isMobile } = usePlatformOS(); - //state - const [isDragging, setIsDragging] = useState(false); - const [isMenuActive, setIsMenuActive] = useState(false); - - // router params - const { workspaceSlug } = useParams(); - // derived values - - //ref - const elementRef = useRef(null); - const dragHandleRef = useRef(null); - const actionSectionRef = useRef(null); - - const getIcon = () => ( - <> -
- {ICONS[favorite.entity_type] || } -
-
- {favorite.entity_data?.logo_props?.in_use ? ( - - ) : ( - ICONS[favorite.entity_type] || - )} -
- - ); - - const getLink = () => { - switch (favorite.entity_type) { - case "project": - return `/${workspaceSlug}/projects/${favorite.project_id}/issues`; - case "cycle": - return `/${workspaceSlug}/projects/${favorite.project_id}/cycles/${favorite.entity_identifier}`; - case "module": - return `/${workspaceSlug}/projects/${favorite.project_id}/modules/${favorite.entity_identifier}`; - case "view": - return `/${workspaceSlug}/projects/${favorite.project_id}/views/${favorite.entity_identifier}`; - case "page": - return `/${workspaceSlug}/projects/${favorite.project_id}/pages/${favorite.entity_identifier}`; - default: - return `/${workspaceSlug}`; - } - }; - - useEffect(() => { - const element = elementRef.current; - - if (!element) return; - - return combine( - draggable({ - element, - // dragHandle: element, - canDrag: () => true, - getInitialData: () => ({ id: favorite.id, type: "CHILD" }), - onDragStart: () => { - setIsDragging(true); - }, - onDrop: () => { - setIsDragging(false); - }, - }), - dropTargetForElements({ - element, - onDragStart: () => { - setIsDragging(true); - }, - onDragEnter: () => { - setIsDragging(true); - }, - onDragLeave: () => { - setIsDragging(false); - }, - onDrop: ({ source }) => { - setIsDragging(false); - const sourceId = source?.data?.id as string | undefined; - if (!sourceId || !favoriteMap[sourceId].parent) return; - handleRemoveFromFavoritesFolder(sourceId); - }, - }) - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [elementRef?.current, isDragging]); - useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); - - return ( - <> - {sidebarCollapsed ? ( -
- - {getIcon()} - -
- ) : ( -
- - - - -
{getIcon()}
- - {favorite.entity_data ? favorite.entity_data.name : favorite.name} - - - setIsMenuActive(!isMenuActive)} - > - - - } - className={cn( - "opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto", - { - "opacity-100 pointer-events-auto": isMenuActive, - } - )} - customButtonClassName="grid place-items-center" - placement="bottom-start" - > - handleRemoveFromFavorites(favorite)}> - - - Remove from favorites - - - -
- )} - - ); - } -); diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx new file mode 100644 index 000000000..6c8e8666e --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx @@ -0,0 +1,41 @@ +"use client"; +import React, { FC } from "react"; +import { observer } from "mobx-react"; +// ui +import { DragHandle, Tooltip } from "@plane/ui"; +// helper +import { cn } from "@/helpers/common.helper"; +// hooks +import { usePlatformOS } from "@/hooks/use-platform-os"; + +type Props = { + sort_order: number | null; + isDragging: boolean; +}; + +export const FavoriteItemDragHandle: FC = observer((props) => { + const { sort_order, isDragging } = props; + // store hooks + const { isMobile } = usePlatformOS(); + + return ( + +
+ +
+
+ ); +}); diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx new file mode 100644 index 000000000..eadaedb34 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx @@ -0,0 +1,48 @@ +"use client"; +import React, { FC } from "react"; +import { MoreHorizontal, Star } from "lucide-react"; +import { IFavorite } from "@plane/types"; +// ui +import { CustomMenu } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type Props = { + ref: React.MutableRefObject; + isMenuActive: boolean; + favorite: IFavorite; + onChange: (value: boolean) => void; + handleRemoveFromFavorites: (favorite: IFavorite) => void; +}; + +export const FavoriteItemQuickAction: FC = (props) => { + const { ref, isMenuActive, onChange, handleRemoveFromFavorites, favorite } = props; + return ( + onChange(!isMenuActive)} + > + + + } + className={cn( + "opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto", + { + "opacity-100 pointer-events-auto": isMenuActive, + } + )} + customButtonClassName="grid place-items-center" + placement="bottom-start" + > + handleRemoveFromFavorites(favorite)}> + + + Remove from favorites + + + + ); +}; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx new file mode 100644 index 000000000..d8fc0d0b3 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx @@ -0,0 +1,25 @@ +"use client"; +import React, { FC } from "react"; +import Link from "next/link"; + +type Props = { + href: string; + title: string; + icon: JSX.Element; + isSidebarCollapsed: boolean; +}; + +export const FavoriteItemTitle: FC = (props) => { + const { href, title, icon, isSidebarCollapsed } = props; + + const linkClass = "flex items-center gap-1.5 truncate w-full"; + const collapsedClass = + "group/project-item cursor-pointer relative group w-full flex items-center justify-center gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90 truncate p-0 size-8 aspect-square mx-auto"; + + return ( + + {icon} + {!isSidebarCollapsed && {title}} + + ); +}; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx new file mode 100644 index 000000000..f1ffde408 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx @@ -0,0 +1,34 @@ +"use client"; +import React, { FC } from "react"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type Props = { + children: React.ReactNode; + elementRef: React.RefObject; + isMenuActive?: boolean; + sidebarCollapsed?: boolean; +}; + +export const FavoriteItemWrapper: FC = (props) => { + const { children, elementRef, isMenuActive = false, sidebarCollapsed = false } = props; + return ( + <> + {sidebarCollapsed ? ( +
{children}
+ ) : ( +
+ {children} +
+ )} + + ); +}; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx new file mode 100644 index 000000000..8f7fb9859 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx @@ -0,0 +1,49 @@ +"use client"; +// lucide +import { Briefcase, FileText, Layers } from "lucide-react"; +// types +import { IFavorite, TLogoProps } from "@plane/types"; +// ui +import { ContrastIcon, DiceIcon, FavoriteFolderIcon } from "@plane/ui"; +import { Logo } from "@/components/common"; + +const iconClassName = `flex-shrink-0 size-4 stroke-[1.5] m-auto`; + +export const FAVORITE_ITEM_ICON: Record = { + page: , + project: , + view: , + module: , + cycle: , + folder: , +}; + +export const getFavoriteItemIcon = (type: string, logo?: TLogoProps | undefined) => ( + <> +
+ {FAVORITE_ITEM_ICON[type] || } +
+
+ {logo?.in_use ? ( + + ) : ( + FAVORITE_ITEM_ICON[type] || + )} +
+ +); + +const entityPaths: Record = { + project: "issues", + cycle: "cycles", + module: "modules", + view: "views", + page: "pages", +}; + +export const generateFavoriteItemLink = (workspaceSlug: string, favorite: IFavorite) => { + const entityPath = entityPaths[favorite.entity_type]; + return entityPath + ? `/${workspaceSlug}/projects/${favorite.project_id}/${entityPath}/${favorite.entity_identifier || ""}` + : `/${workspaceSlug}`; +}; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/index.ts b/web/core/components/workspace/sidebar/favorites/favorite-items/common/index.ts new file mode 100644 index 000000000..5e03ce1c7 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/index.ts @@ -0,0 +1,5 @@ +export * from "./favorite-item-drag-handle"; +export * from "./favorite-item-quick-action"; +export * from "./favorite-item-wrapper"; +export * from "./favorite-item-title"; +export * from "./helper"; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/index.ts b/web/core/components/workspace/sidebar/favorites/favorite-items/index.ts new file mode 100644 index 000000000..1037372f3 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/index.ts @@ -0,0 +1,2 @@ +export * from "./common"; +export * from "./root"; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/root.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/root.tsx new file mode 100644 index 000000000..b001b7f69 --- /dev/null +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/root.tsx @@ -0,0 +1,106 @@ +"use client"; + +import React, { FC, useEffect, useRef, useState } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { observer } from "mobx-react"; +// ui +import { IFavorite } from "@plane/types"; +// components +import { + FavoriteItemDragHandle, + FavoriteItemQuickAction, + FavoriteItemWrapper, + FavoriteItemTitle, +} from "@/components/workspace/sidebar/favorites"; +// hooks +import { useAppTheme } from "@/hooks/store"; +import { useFavoriteItemDetails } from "@/hooks/use-favorite-item-details"; +import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; + +type Props = { + workspaceSlug: string; + favorite: IFavorite; + favoriteMap: Record; + handleRemoveFromFavorites: (favorite: IFavorite) => void; + handleRemoveFromFavoritesFolder: (favoriteId: string) => void; +}; + +export const FavoriteRoot: FC = observer((props) => { + // props + const { workspaceSlug, favorite, favoriteMap, handleRemoveFromFavorites, handleRemoveFromFavoritesFolder } = props; + // store hooks + const { sidebarCollapsed } = useAppTheme(); + + //state + const [isDragging, setIsDragging] = useState(false); + const [isMenuActive, setIsMenuActive] = useState(false); + //ref + const elementRef = useRef(null); + const actionSectionRef = useRef(null); + + const handleQuickAction = (value: boolean) => setIsMenuActive(value); + + const { itemLink, itemIcon, itemTitle } = useFavoriteItemDetails(workspaceSlug, favorite); + + // drag and drop + useEffect(() => { + const element = elementRef.current; + + if (!element) return; + + return combine( + draggable({ + element, + dragHandle: elementRef.current, + canDrag: () => true, + getInitialData: () => ({ id: favorite.id, type: "CHILD" }), + onDragStart: () => { + setIsDragging(true); + }, + onDrop: () => { + setIsDragging(false); + }, + }), + dropTargetForElements({ + element, + onDragStart: () => { + setIsDragging(true); + }, + onDragEnter: () => { + setIsDragging(true); + }, + onDragLeave: () => { + setIsDragging(false); + }, + onDrop: ({ source }) => { + setIsDragging(false); + const sourceId = source?.data?.id as string | undefined; + if (!sourceId || !favoriteMap[sourceId].parent) return; + handleRemoveFromFavoritesFolder(sourceId); + }, + }) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [elementRef?.current, isDragging]); + + useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); + + return ( + <> + + {!sidebarCollapsed && } + + {!sidebarCollapsed && ( + + )} + + + ); +}); diff --git a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx index d4a18fc40..17380b079 100644 --- a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx @@ -22,7 +22,7 @@ import useLocalStorage from "@/hooks/use-local-storage"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { FavoriteFolder } from "./favorite-folder"; -import { FavoriteItem } from "./favorite-item"; +import { FavoriteRoot } from "./favorite-items"; import { NewFavoriteFolder } from "./new-fav-folder"; export const SidebarFavoritesMenu = observer(() => { @@ -196,7 +196,8 @@ export const SidebarFavoritesMenu = observer(() => { handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder} /> ) : ( - { + const favoriteItemId = favorite.entity_data.id; + const favoriteItemLogoProps = favorite?.entity_data?.logo_props; + const favoriteItemName = favorite?.entity_data.name || favorite?.name; + const favoriteItemEntityType = favorite?.entity_type; + + // store hooks + const { getViewById } = useProjectView(); + const { currentProjectDetails } = useProject(); + const { getCycleById } = useCycle(); + const { getModuleById } = useModule(); + + // derived values + const pageDetail = usePage(favoriteItemId ?? ""); + const viewDetails = getViewById(favoriteItemId ?? ""); + const cycleDetail = getCycleById(favoriteItemId ?? ""); + const moduleDetail = getModuleById(favoriteItemId ?? ""); + + let itemIcon; + let itemTitle; + const itemLink = generateFavoriteItemLink(workspaceSlug.toString(), favorite); + + switch (favoriteItemEntityType) { + case "project": + itemTitle = currentProjectDetails?.name || favoriteItemName; + itemIcon = getFavoriteItemIcon("project", currentProjectDetails?.logo_props || favoriteItemLogoProps); + break; + case "page": + itemTitle = pageDetail.name || favoriteItemName; + itemIcon = getFavoriteItemIcon("page", pageDetail?.logo_props || favoriteItemLogoProps); + break; + case "view": + itemTitle = viewDetails?.name || favoriteItemName; + itemIcon = getFavoriteItemIcon("view", viewDetails?.logo_props || favoriteItemLogoProps); + break; + case "cycle": + itemTitle = cycleDetail?.name || favoriteItemName; + itemIcon = getFavoriteItemIcon("cycle"); + break; + case "module": + itemTitle = moduleDetail?.name || favoriteItemName; + itemIcon = getFavoriteItemIcon("module"); + break; + default: + itemTitle = favoriteItemName; + itemIcon = getFavoriteItemIcon(favoriteItemEntityType); + break; + } + + return { itemIcon, itemTitle, itemLink }; +}; From b05d72e29af326b7a5a01060c2e8d7b09d5f2992 Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:13:01 +0530 Subject: [PATCH 21/57] fixed setup.sh for macos support (#5336) * fixed setup.sh for macos support * updated as per coderabbit suggestions --- deploy/selfhost/install.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/deploy/selfhost/install.sh b/deploy/selfhost/install.sh index 8fd2e3e2f..6ad3b077f 100755 --- a/deploy/selfhost/install.sh +++ b/deploy/selfhost/install.sh @@ -9,11 +9,20 @@ export DOCKERHUB_USER=makeplane export PULL_POLICY=${PULL_POLICY:-if_not_present} CPU_ARCH=$(uname -m) +OS_NAME=$(uname) +UPPER_CPU_ARCH=$(tr '[:lower:]' '[:upper:]' <<< "$CPU_ARCH") mkdir -p $PLANE_INSTALL_DIR/archive DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env +SED_PREFIX=() +if [ "$OS_NAME" == "Darwin" ]; then + SED_PREFIX=("-i" "") +else + SED_PREFIX=("-i") +fi + function print_header() { clear @@ -51,12 +60,12 @@ function spinner() { } function initialize(){ - printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${CPU_ARCH^^} support." >&2 + printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${UPPER_CPU_ARCH} support." >&2 if [ "$CUSTOM_BUILD" == "true" ]; then echo "" >&2 echo "" >&2 - echo "${CPU_ARCH^^} images are not available for selected release ($APP_RELEASE)." >&2 + echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2 echo "build" return 1 fi @@ -78,7 +87,7 @@ function initialize(){ else echo "" >&2 echo "" >&2 - echo "${CPU_ARCH^^} images are not available for selected release ($APP_RELEASE)." >&2 + echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2 echo "" >&2 echo "build" return 1 @@ -122,7 +131,7 @@ function updateEnvFile() { return else # if key exists, update the value - sed -i "s/^$key=.*/$key=$value/g" "$file" + sed "${SED_PREFIX[@]}" "s/^$key=.*/$key=$value/g" "$file" fi else echo "File not found: $file" From 644d1db44cc36816bed223aadb9b990446891a92 Mon Sep 17 00:00:00 2001 From: timf34 <66926418+timf34@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:43:09 +0100 Subject: [PATCH 22/57] Fixed typo in manifest.json (#5310) --- web/public/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/manifest.json b/web/public/manifest.json index 59b721989..c7e2fc5af 100644 --- a/web/public/manifest.json +++ b/web/public/manifest.json @@ -1,6 +1,6 @@ { "name": "Plane", - "shot_name": "Plane", + "short_name": "Plane", "icons": [ { "src": "/icons/icon-192x192.png", From f54e1b922d67f1e3054bad612e51467818359a76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:18:05 +0530 Subject: [PATCH 23/57] chore(deps): bump django in /apiserver/requirements (#5337) Bumps [django](https://github.com/django/django) from 4.2.14 to 4.2.15. - [Commits](https://github.com/django/django/compare/4.2.14...4.2.15) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apiserver/requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 48941954e..2a8d35219 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -1,7 +1,7 @@ # base requirements # django -Django==4.2.14 +Django==4.2.15 # rest framework djangorestframework==3.15.2 # postgres From fc205efd6da5c8dacfdee760ae61860f6f8e7ea3 Mon Sep 17 00:00:00 2001 From: vamsi Date: Fri, 9 Aug 2024 16:23:53 +0530 Subject: [PATCH 24/57] fix: remove user count from instance settings --- apiserver/plane/license/api/serializers/instance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/license/api/serializers/instance.py b/apiserver/plane/license/api/serializers/instance.py index 3b905e64d..606bb643f 100644 --- a/apiserver/plane/license/api/serializers/instance.py +++ b/apiserver/plane/license/api/serializers/instance.py @@ -13,6 +13,7 @@ class InstanceSerializer(BaseSerializer): model = Instance exclude = [ "license_key", + "user_count" ] read_only_fields = [ "id", From 0b72bd373befa69f9eb729b6b485b175b1d1e9f1 Mon Sep 17 00:00:00 2001 From: vamsi Date: Fri, 9 Aug 2024 16:35:52 +0530 Subject: [PATCH 25/57] fix: adding signup enabled flag in instance settings endpoint --- apiserver/plane/license/api/views/instance.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 9df727607..9aac3fb18 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -54,6 +54,7 @@ class InstanceEndpoint(BaseAPIView): data["is_activated"] = True # Get all the configuration ( + ENABLE_SIGNUP, IS_GOOGLE_ENABLED, IS_GITHUB_ENABLED, GITHUB_APP_NAME, @@ -70,6 +71,10 @@ class InstanceEndpoint(BaseAPIView): INTERCOM_APP_ID, ) = get_configuration_value( [ + { + "key": "ENABLE_SIGNUP", + "default": os.environ.get("ENABLE_SIGNUP", "0"), + }, { "key": "IS_GOOGLE_ENABLED", "default": os.environ.get("IS_GOOGLE_ENABLED", "0"), @@ -132,6 +137,7 @@ class InstanceEndpoint(BaseAPIView): data = {} # Authentication + data["enable_signup"] = ENABLE_SIGNUP == "1" data["is_google_enabled"] = IS_GOOGLE_ENABLED == "1" data["is_github_enabled"] = IS_GITHUB_ENABLED == "1" data["is_gitlab_enabled"] = IS_GITLAB_ENABLED == "1" From 24b1e71cbff85c0104e812745da9caf96b5401ae Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:42:31 +0530 Subject: [PATCH 26/57] [WEB-2211] fix: input autoComplete (#5333) * fix: input autoComplete * chore: code refactor * chore: set autoComplete on for email, password and name --- admin/app/general/form.tsx | 1 + admin/core/components/instance/setup-form.tsx | 4 ++++ admin/core/components/login/sign-in-form.tsx | 2 ++ packages/ui/src/form-fields/input.tsx | 14 +++++++++++++- space/core/components/account/auth-forms/email.tsx | 1 + .../components/account/auth-forms/password.tsx | 1 + web/app/accounts/forgot-password/page.tsx | 1 + web/app/accounts/reset-password/page.tsx | 2 ++ web/app/accounts/set-password/page.tsx | 2 ++ web/app/profile/page.tsx | 3 +++ web/core/components/account/auth-forms/email.tsx | 1 + .../components/account/auth-forms/password.tsx | 1 + .../components/account/auth-forms/unique-code.tsx | 1 + web/core/components/onboarding/profile-setup.tsx | 3 +++ 14 files changed, 36 insertions(+), 1 deletion(-) diff --git a/admin/app/general/form.tsx b/admin/app/general/form.tsx index 380f39357..4422ee91f 100644 --- a/admin/app/general/form.tsx +++ b/admin/app/general/form.tsx @@ -86,6 +86,7 @@ export const GeneralConfigurationForm: FC = observer( value={instanceAdmins[0]?.user_detail?.email ?? ""} placeholder="Admin email" className="w-full cursor-not-allowed !text-custom-text-400" + autoComplete="on" disabled />
diff --git a/admin/core/components/instance/setup-form.tsx b/admin/core/components/instance/setup-form.tsx index ec3919896..7e987dbdf 100644 --- a/admin/core/components/instance/setup-form.tsx +++ b/admin/core/components/instance/setup-form.tsx @@ -174,6 +174,7 @@ export const InstanceSetupForm: FC = (props) => { placeholder="Wilber" value={formData.first_name} onChange={(e) => handleFormChange("first_name", e.target.value)} + autoComplete="on" autoFocus />
@@ -190,6 +191,7 @@ export const InstanceSetupForm: FC = (props) => { placeholder="Wright" value={formData.last_name} onChange={(e) => handleFormChange("last_name", e.target.value)} + autoComplete="on" />
@@ -208,6 +210,7 @@ export const InstanceSetupForm: FC = (props) => { value={formData.email} onChange={(e) => handleFormChange("email", e.target.value)} hasError={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false} + autoComplete="on" /> {errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (

{errorData.message}

@@ -247,6 +250,7 @@ export const InstanceSetupForm: FC = (props) => { hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false} onFocus={() => setIsPasswordInputFocused(true)} onBlur={() => setIsPasswordInputFocused(false)} + autoComplete="on" /> {showPassword.password ? (
@@ -145,6 +146,7 @@ export const InstanceSignInForm: FC = (props) => { placeholder="Enter your password" value={formData.password} onChange={(e) => handleFormChange("password", e.target.value)} + autoComplete="on" /> {showPassword ? (
@@ -173,6 +174,7 @@ export default function ResetPasswordPage() { minLength={8} onFocus={() => setIsPasswordInputFocused(true)} onBlur={() => setIsPasswordInputFocused(false)} + autoComplete="on" autoFocus /> {showPassword.password ? ( diff --git a/web/app/accounts/set-password/page.tsx b/web/app/accounts/set-password/page.tsx index df1b5d567..f3ac35b76 100644 --- a/web/app/accounts/set-password/page.tsx +++ b/web/app/accounts/set-password/page.tsx @@ -147,6 +147,7 @@ const SetPasswordPage = observer(() => { //hasError={Boolean(errors.email)} placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed" + autoComplete="on" disabled />
@@ -167,6 +168,7 @@ const SetPasswordPage = observer(() => { minLength={8} onFocus={() => setIsPasswordInputFocused(true)} onBlur={() => setIsPasswordInputFocused(false)} + autoComplete="on" autoFocus /> {showPassword.password ? ( diff --git a/web/app/profile/page.tsx b/web/app/profile/page.tsx index ebad3bc58..a50a70c5a 100644 --- a/web/app/profile/page.tsx +++ b/web/app/profile/page.tsx @@ -245,6 +245,7 @@ const ProfileSettingsPage = observer(() => { placeholder="Enter your first name" className={`w-full rounded-md ${errors.first_name ? "border-red-500" : ""}`} maxLength={24} + autoComplete="on" /> )} /> @@ -269,6 +270,7 @@ const ProfileSettingsPage = observer(() => { placeholder="Enter your last name" className="w-full rounded-md" maxLength={24} + autoComplete="on" /> )} /> @@ -296,6 +298,7 @@ const ProfileSettingsPage = observer(() => { className={`w-full cursor-not-allowed rounded-md !bg-custom-background-80 ${ errors.email ? "border-red-500" : "" }`} + autoComplete="on" disabled /> )} diff --git a/web/core/components/account/auth-forms/email.tsx b/web/core/components/account/auth-forms/email.tsx index 9acfcc5cc..ab328a7e1 100644 --- a/web/core/components/account/auth-forms/email.tsx +++ b/web/core/components/account/auth-forms/email.tsx @@ -63,6 +63,7 @@ export const AuthEmailForm: FC = observer((props) => { className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`} onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} + autoComplete="on" autoFocus /> {email.length > 0 && ( diff --git a/web/core/components/account/auth-forms/password.tsx b/web/core/components/account/auth-forms/password.tsx index 618c578b0..088dc3194 100644 --- a/web/core/components/account/auth-forms/password.tsx +++ b/web/core/components/account/auth-forms/password.tsx @@ -208,6 +208,7 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" onFocus={() => setIsPasswordInputFocused(true)} onBlur={() => setIsPasswordInputFocused(false)} + autoComplete="on" autoFocus /> {showPassword?.password ? ( diff --git a/web/core/components/account/auth-forms/unique-code.tsx b/web/core/components/account/auth-forms/unique-code.tsx index 8c7b8a60f..530874eb9 100644 --- a/web/core/components/account/auth-forms/unique-code.tsx +++ b/web/core/components/account/auth-forms/unique-code.tsx @@ -107,6 +107,7 @@ export const AuthUniqueCodeForm: React.FC = (props) => { onChange={(e) => handleFormChange("email", e.target.value)} placeholder="name@company.com" className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0`} + autoComplete="on" disabled /> {uniqueCodeFormData.email.length > 0 && ( diff --git a/web/core/components/onboarding/profile-setup.tsx b/web/core/components/onboarding/profile-setup.tsx index 134f6e7a6..cc89748cf 100644 --- a/web/core/components/onboarding/profile-setup.tsx +++ b/web/core/components/onboarding/profile-setup.tsx @@ -372,6 +372,7 @@ export const ProfileSetup: React.FC = observer((props) => { hasError={Boolean(errors.first_name)} placeholder="Wilbur" className="w-full border-onboarding-border-100" + autoComplete="on" /> )} /> @@ -405,6 +406,7 @@ export const ProfileSetup: React.FC = observer((props) => { hasError={Boolean(errors.last_name)} placeholder="Wright" className="w-full border-onboarding-border-100" + autoComplete="on" /> )} /> @@ -438,6 +440,7 @@ export const ProfileSetup: React.FC = observer((props) => { className="w-full border-[0.5px] border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" onFocus={() => setIsPasswordInputFocused(true)} onBlur={() => setIsPasswordInputFocused(false)} + autoComplete="on" /> {showPassword.password ? ( Date: Fri, 9 Aug 2024 17:16:37 +0530 Subject: [PATCH 27/57] chore: handling the archived module ids in the issue list and issue detail endpoints (#5343) --- apiserver/plane/app/views/inbox/base.py | 3 ++- apiserver/plane/app/views/issue/base.py | 5 ++--- apiserver/plane/app/views/view/base.py | 3 ++- apiserver/plane/utils/grouper.py | 7 ++++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py index bda64f36b..7a1d77d0a 100644 --- a/apiserver/plane/app/views/inbox/base.py +++ b/apiserver/plane/app/views/inbox/base.py @@ -160,7 +160,8 @@ class InboxIssueViewSet(BaseViewSet): ArrayAgg( "issue_module__module_id", distinct=True, - filter=~Q(issue_module__module_id__isnull=True), + filter=~Q(issue_module__module_id__isnull=True) + & Q(issue_module__module__archived_at__isnull=True), ), Value([], output_field=ArrayField(UUIDField())), ), diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index e0ec01936..49c7b5b1e 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -64,7 +64,6 @@ from plane.utils.user_timezone_converter import user_timezone_converter class IssueListEndpoint(BaseAPIView): - permission_classes = [ ProjectEntityPermission, ] @@ -438,7 +437,8 @@ class IssueViewSet(BaseViewSet): ArrayAgg( "issue_module__module_id", distinct=True, - filter=~Q(issue_module__module_id__isnull=True), + filter=~Q(issue_module__module_id__isnull=True) + & Q(issue_module__module__archived_at__isnull=True), ), Value([], output_field=ArrayField(UUIDField())), ), @@ -626,7 +626,6 @@ class BulkDeleteIssuesEndpoint(BaseAPIView): project_id=project_id, is_active=True, ).exists(): - return Response( {"error": "Only admin can perform this action"}, status=status.HTTP_403_FORBIDDEN, diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index 297fe3f69..7a9130951 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -223,7 +223,8 @@ class WorkspaceViewIssuesViewSet(BaseViewSet): ArrayAgg( "issue_module__module_id", distinct=True, - filter=~Q(issue_module__module_id__isnull=True), + filter=~Q(issue_module__module_id__isnull=True) + & Q(issue_module__module__archived_at__isnull=True), ), Value([], output_field=ArrayField(UUIDField())), ), diff --git a/apiserver/plane/utils/grouper.py b/apiserver/plane/utils/grouper.py index a3ac2420e..ba52bca03 100644 --- a/apiserver/plane/utils/grouper.py +++ b/apiserver/plane/utils/grouper.py @@ -18,7 +18,6 @@ from plane.db.models import ( def issue_queryset_grouper(queryset, group_by, sub_group_by): - FIELD_MAPPER = { "label_ids": "labels__id", "assignee_ids": "assignees__id", @@ -30,7 +29,10 @@ def issue_queryset_grouper(queryset, group_by, sub_group_by): "label_ids": ("labels__id", ~Q(labels__id__isnull=True)), "module_ids": ( "issue_module__module_id", - ~Q(issue_module__module_id__isnull=True), + ( + ~Q(issue_module__module_id__isnull=True) + & Q(issue_module__module__archived_at__isnull=True) + ), ), } default_annotations = { @@ -51,7 +53,6 @@ def issue_queryset_grouper(queryset, group_by, sub_group_by): def issue_on_results(issues, group_by, sub_group_by): - FIELD_MAPPER = { "labels__id": "label_ids", "assignees__id": "assignee_ids", From 421bf2abc7574e9a3a4e7dcccda2a62d8e5bd1d6 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:03:25 +0530 Subject: [PATCH 28/57] [WEB-2178] fix: empty folder title (#5344) * fix: empty folder title * fix: collapsible overflow issue --- packages/ui/src/collapsible/collapsible.tsx | 1 - .../sidebar/favorites/new-fav-folder.tsx | 40 +++++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/collapsible/collapsible.tsx b/packages/ui/src/collapsible/collapsible.tsx index a069be3ed..a02330861 100644 --- a/packages/ui/src/collapsible/collapsible.tsx +++ b/packages/ui/src/collapsible/collapsible.tsx @@ -38,7 +38,6 @@ export const Collapsible: FC = (props) => { { formData = { entity_type: "folder", is_folder: true, - name: formData.name, + name: formData.name.trim(), parent: null, project_id: null, }; + + if (formData.name === "") + return setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Folder name cannot be empty", + }); + addFavorite(workspaceSlug.toString(), formData) .then(() => { setToast({ @@ -77,15 +85,31 @@ export const NewFavoriteFolder = observer((props: TProps) => { message: "Folder already exists", }); const payload = { - name: formData.name, + name: formData.name.trim(), }; - updateFavorite(workspaceSlug.toString(), favoriteId, payload).then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: "Favorite updated successfully.", + + if (formData.name.trim() === "") + return setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Folder name cannot be empty", + }); + + updateFavorite(workspaceSlug.toString(), favoriteId, payload) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Favorite updated successfully.", + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Something went wrong!", + }); }); - }); setCreateNewFolder(false); setValue("name", ""); }; From 679b0b6465f1cc91eeab670c2270057af1f71080 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:09:25 +0530 Subject: [PATCH 29/57] [WEB-2189] fix: issue peek overview and issue detail unauthorised delete action (#5341) * fix: issue peek overview and issue detail delete action * chore: code refactor * chore: code refactor --- .../components/issues/delete-issue-modal.tsx | 15 +++++++++++- .../issue-detail-widgets/relations/helper.tsx | 11 +++++---- .../sub-issues/helper.tsx | 13 ++++++----- .../issue-detail-quick-actions.tsx | 20 +++++++++------- .../components/issues/peek-overview/root.tsx | 23 +++++++++++-------- .../components/issues/peek-overview/view.tsx | 2 +- 6 files changed, 53 insertions(+), 31 deletions(-) diff --git a/web/core/components/issues/delete-issue-modal.tsx b/web/core/components/issues/delete-issue-modal.tsx index 06fa4cafa..e6109a028 100644 --- a/web/core/components/issues/delete-issue-modal.tsx +++ b/web/core/components/issues/delete-issue-modal.tsx @@ -8,7 +8,7 @@ import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PROJECT_ERROR_MESSAGES } from "@/constants/project"; // hooks -import { useIssues, useProject } from "@/hooks/store"; +import { useIssues, useProject, useUser } from "@/hooks/store"; type Props = { isOpen: boolean; @@ -26,6 +26,7 @@ export const DeleteIssueModal: React.FC = (props) => { // store hooks const { issueMap } = useIssues(); const { getProjectById } = useProject(); + const { data: currentUser, canPerformProjectAdminActions } = useUser(); useEffect(() => { setIsDeleting(false); @@ -36,6 +37,8 @@ export const DeleteIssueModal: React.FC = (props) => { // derived values const issue = data ? data : issueMap[dataId!]; const projectDetails = getProjectById(issue?.project_id); + const isIssueCreator = issue?.created_by === currentUser?.id; + const authorized = isIssueCreator || canPerformProjectAdminActions; const onClose = () => { setIsDeleting(false); @@ -44,6 +47,16 @@ export const DeleteIssueModal: React.FC = (props) => { const handleIssueDelete = async () => { setIsDeleting(true); + + if (!authorized) { + setToast({ + title: PROJECT_ERROR_MESSAGES.permissionError.title, + type: TOAST_TYPE.ERROR, + message: PROJECT_ERROR_MESSAGES.permissionError.message, + }); + onClose(); + return; + } if (onSubmit) await onSubmit() .then(() => { diff --git a/web/core/components/issues/issue-detail-widgets/relations/helper.tsx b/web/core/components/issues/issue-detail-widgets/relations/helper.tsx index f2366f7d4..fe3be8ca4 100644 --- a/web/core/components/issues/issue-detail-widgets/relations/helper.tsx +++ b/web/core/components/issues/issue-detail-widgets/relations/helper.tsx @@ -70,11 +70,12 @@ export const useRelationOperations = (): TRelationIssueOperations => { }, remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { - await removeIssue(workspaceSlug, projectId, issueId); - captureIssueEvent({ - eventName: ISSUE_DELETED, - payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, - path: pathname, + return removeIssue(workspaceSlug, projectId, issueId).then(() => { + captureIssueEvent({ + eventName: ISSUE_DELETED, + payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, + path: pathname, + }); }); } catch (error) { captureIssueEvent({ diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx index dbf295a00..7df432d5d 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx @@ -150,13 +150,14 @@ export const useSubIssueOperations = (): TSubIssueOperations => { deleteSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => { try { setSubIssueHelpers(parentIssueId, "issue_loader", issueId); - await deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId); - captureIssueEvent({ - eventName: "Sub-issue deleted", - payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, - path: pathname, + return deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId).then(() => { + captureIssueEvent({ + eventName: "Sub-issue deleted", + payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, + path: pathname, + }); + setSubIssueHelpers(parentIssueId, "issue_loader", issueId); }); - setSubIssueHelpers(parentIssueId, "issue_loader", issueId); } catch (error) { captureIssueEvent({ eventName: "Sub-issue removed", diff --git a/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx b/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx index 8c720261e..54cce976f 100644 --- a/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx +++ b/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx @@ -78,14 +78,18 @@ export const IssueDetailQuickActions: FC = observer((props) => { const handleDeleteIssue = async () => { try { - if (issue?.archived_at) await removeArchivedIssue(workspaceSlug, projectId, issueId); - else await removeIssue(workspaceSlug, projectId, issueId); - router.push(`/${workspaceSlug}/projects/${projectId}/issues`); - captureIssueEvent({ - eventName: ISSUE_DELETED, - payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, - path: pathname, - }); + if (issue?.archived_at) { + return removeArchivedIssue(workspaceSlug, projectId, issueId).then(() => { + router.push(`/${workspaceSlug}/projects/${projectId}/issues`); + captureIssueEvent({ + eventName: ISSUE_DELETED, + payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, + path: pathname, + }); + }); + } else { + return removeIssue(workspaceSlug, projectId, issueId); + } } catch (error) { setToast({ title: "Error!", diff --git a/web/core/components/issues/peek-overview/root.tsx b/web/core/components/issues/peek-overview/root.tsx index 0f46f8f68..a55b0d546 100644 --- a/web/core/components/issues/peek-overview/root.tsx +++ b/web/core/components/issues/peek-overview/root.tsx @@ -34,6 +34,7 @@ export const IssuePeekOverview: FC = observer((props) => { } = useIssues(EIssuesStoreType.ARCHIVED); const { peekIssue, + setPeekIssue, issue: { fetchIssue }, fetchActivities, } = useIssueDetail(); @@ -44,6 +45,11 @@ export const IssuePeekOverview: FC = observer((props) => { const [loader, setLoader] = useState(true); const [error, setError] = useState(false); + const removeRoutePeekId = () => { + setPeekIssue(undefined); + if (embedIssue) embedRemoveCurrentNotification && embedRemoveCurrentNotification(); + }; + const issueOperations: TIssueOperations = useMemo( () => ({ fetch: async (workspaceSlug: string, projectId: string, issueId: string, loader = true) => { @@ -95,16 +101,13 @@ export const IssuePeekOverview: FC = observer((props) => { }, remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { - issues?.removeIssue(workspaceSlug, projectId, issueId); - setToast({ - title: "Success!", - type: TOAST_TYPE.SUCCESS, - message: "Issue deleted successfully", - }); - captureIssueEvent({ - eventName: ISSUE_DELETED, - payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, - path: pathname, + return issues?.removeIssue(workspaceSlug, projectId, issueId).then(() => { + captureIssueEvent({ + eventName: ISSUE_DELETED, + payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, + path: pathname, + }); + removeRoutePeekId(); }); } catch (error) { setToast({ diff --git a/web/core/components/issues/peek-overview/view.tsx b/web/core/components/issues/peek-overview/view.tsx index 865e93fbf..617c36dfa 100644 --- a/web/core/components/issues/peek-overview/view.tsx +++ b/web/core/components/issues/peek-overview/view.tsx @@ -131,7 +131,7 @@ export const IssueView: FC = observer((props) => { toggleDeleteIssueModal(null); }} data={issue} - onSubmit={() => issueOperations.remove(workspaceSlug, projectId, issueId).then(() => removeRoutePeekId())} + onSubmit={async () => issueOperations.remove(workspaceSlug, projectId, issueId)} /> )} From 6d0cf1b4e99f52ca5fb65941aaa6f6dfe0e15913 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:14:38 +0530 Subject: [PATCH 30/57] [WEB-2190] fix: unauthorised delete and redirections (#5342) * fix: cycle unauthorised delete action redirection * fix: intake unauthorised delete action redirection --- web/core/components/cycles/delete-modal.tsx | 3 +- .../inbox/content/inbox-issue-header.tsx | 2 +- web/core/store/inbox/project-inbox.store.ts | 30 +++++++++---------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/web/core/components/cycles/delete-modal.tsx b/web/core/components/cycles/delete-modal.tsx index b9b1fdbc5..e31a785b5 100644 --- a/web/core/components/cycles/delete-modal.tsx +++ b/web/core/components/cycles/delete-modal.tsx @@ -42,6 +42,7 @@ export const CycleDeleteModal: React.FC = observer((props) => { try { await deleteCycle(workspaceSlug, projectId, cycle.id) .then(() => { + if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", @@ -68,8 +69,6 @@ export const CycleDeleteModal: React.FC = observer((props) => { }); }) .finally(() => handleClose()); - - if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); } catch (error) { setToast({ type: TOAST_TYPE.ERROR, diff --git a/web/core/components/inbox/content/inbox-issue-header.tsx b/web/core/components/inbox/content/inbox-issue-header.tsx index 1b18f063a..8d4561fc1 100644 --- a/web/core/components/inbox/content/inbox-issue-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-header.tsx @@ -140,7 +140,7 @@ export const InboxIssueActionsHeader: FC = observer((p const handleInboxIssueDelete = async () => { if (!inboxIssue || !currentInboxIssueId) return; - await deleteInboxIssue(workspaceSlug, projectId, currentInboxIssueId).finally(() => { + await deleteInboxIssue(workspaceSlug, projectId, currentInboxIssueId).then(() => { if (!isNotificationEmbed) router.push(`/${workspaceSlug}/projects/${projectId}/inbox`); }); }; diff --git a/web/core/store/inbox/project-inbox.store.ts b/web/core/store/inbox/project-inbox.store.ts index d270ab068..bf0a48576 100644 --- a/web/core/store/inbox/project-inbox.store.ts +++ b/web/core/store/inbox/project-inbox.store.ts @@ -484,25 +484,23 @@ export class ProjectInboxStore implements IProjectInboxStore { const currentIssue = this.inboxIssues?.[inboxIssueId]; try { if (!currentIssue) return; - runInAction(() => { - set( - this, - ["inboxIssuePaginationInfo", "total_results"], - (this.inboxIssuePaginationInfo?.total_results || 0) - 1 - ); - set(this, "inboxIssues", omit(this.inboxIssues, inboxIssueId)); - set( - this, - ["inboxIssueIds"], - this.inboxIssueIds.filter((id) => id !== inboxIssueId) - ); + await this.inboxIssueService.destroy(workspaceSlug, projectId, inboxIssueId).then(() => { + runInAction(() => { + set( + this, + ["inboxIssuePaginationInfo", "total_results"], + (this.inboxIssuePaginationInfo?.total_results || 0) - 1 + ); + set(this, "inboxIssues", omit(this.inboxIssues, inboxIssueId)); + set( + this, + ["inboxIssueIds"], + this.inboxIssueIds.filter((id) => id !== inboxIssueId) + ); + }); }); - await this.inboxIssueService.destroy(workspaceSlug, projectId, inboxIssueId); } catch (error) { console.error("Error removing the intake issue"); - set(this.inboxIssues, [inboxIssueId], currentIssue); - set(this, ["inboxIssuePaginationInfo", "total_results"], (this.inboxIssuePaginationInfo?.total_results || 0) + 1); - set(this, ["inboxIssueIds"], [...this.inboxIssueIds, inboxIssueId]); throw error; } }; From 85f8fe9247549dd9f8af79afeb2dc8fb8c1eec36 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:22:47 +0530 Subject: [PATCH 31/57] [WEB-2045] dev: editor variable font sizes and styles support (#5340) * chore: added variable font size and font style support * chore: remove font style switcher * chore: update typography --- .../components/editors/document/editor.tsx | 22 ++- .../editors/document/page-renderer.tsx | 12 +- .../editors/document/read-only-editor.tsx | 22 ++- .../components/editors/editor-container.tsx | 11 +- .../components/editors/editor-wrapper.tsx | 10 +- .../editors/read-only-editor-wrapper.tsx | 21 ++- packages/editor/src/core/constants/config.ts | 7 + .../extensions/code/code-block-node-view.tsx | 2 +- .../src/core/hooks/use-document-editor.ts | 4 +- packages/editor/src/core/hooks/use-editor.ts | 77 ++++------ .../src/core/hooks/use-read-only-editor.ts | 4 +- packages/editor/src/core/props/props.tsx | 10 +- packages/editor/src/core/props/read-only.tsx | 22 +-- packages/editor/src/core/types/config.ts | 17 +++ packages/editor/src/core/types/editor.ts | 6 +- packages/editor/src/core/types/index.ts | 1 + packages/editor/src/index.ts | 2 +- packages/editor/src/styles/editor.css | 132 ++++++++++++------ packages/editor/src/styles/table.css | 5 - packages/ui/src/icons/index.ts | 3 + packages/ui/src/icons/monospace-icon.tsx | 16 +++ packages/ui/src/icons/sans-serif-icon.tsx | 16 +++ packages/ui/src/icons/serif-icon.tsx | 16 +++ .../components/pages/editor/editor-body.tsx | 11 +- .../pages/editor/header/options-dropdown.tsx | 1 + web/core/components/pages/editor/title.tsx | 27 ++-- web/core/constants/editor.ts | 26 +++- web/core/hooks/use-page-filters.ts | 61 +++++++- 28 files changed, 419 insertions(+), 145 deletions(-) create mode 100644 packages/editor/src/core/constants/config.ts create mode 100644 packages/editor/src/core/types/config.ts create mode 100644 packages/ui/src/icons/monospace-icon.tsx create mode 100644 packages/ui/src/icons/sans-serif-icon.tsx create mode 100644 packages/ui/src/icons/serif-icon.tsx diff --git a/packages/editor/src/core/components/editors/document/editor.tsx b/packages/editor/src/core/components/editors/document/editor.tsx index 2ec49acc0..968bc4572 100644 --- a/packages/editor/src/core/components/editors/document/editor.tsx +++ b/packages/editor/src/core/components/editors/document/editor.tsx @@ -1,19 +1,28 @@ import React from "react"; // components import { PageRenderer } from "@/components/editors"; +// constants +import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; // helpers import { getEditorClassNames } from "@/helpers/common"; // hooks import { useDocumentEditor } from "@/hooks/use-document-editor"; -import { TFileHandler } from "@/hooks/use-editor"; // plane editor types import { TEmbedConfig } from "@/plane-editor/types"; // types -import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TExtensions } from "@/types"; +import { + EditorRefApi, + IMentionHighlight, + IMentionSuggestion, + TDisplayConfig, + TExtensions, + TFileHandler, +} from "@/types"; interface IDocumentEditor { containerClassName?: string; disabledExtensions?: TExtensions[]; + displayConfig?: TDisplayConfig; editorClassName?: string; embedHandler: TEmbedConfig; fileHandler: TFileHandler; @@ -34,6 +43,7 @@ const DocumentEditor = (props: IDocumentEditor) => { const { containerClassName, disabledExtensions, + displayConfig = DEFAULT_DISPLAY_CONFIG, editorClassName = "", embedHandler, fileHandler, @@ -72,7 +82,13 @@ const DocumentEditor = (props: IDocumentEditor) => { if (!editor || !isIndexedDbSynced) return null; return ( - + ); }; diff --git a/packages/editor/src/core/components/editors/document/page-renderer.tsx b/packages/editor/src/core/components/editors/document/page-renderer.tsx index 2de5f6c57..66cde240e 100644 --- a/packages/editor/src/core/components/editors/document/page-renderer.tsx +++ b/packages/editor/src/core/components/editors/document/page-renderer.tsx @@ -16,8 +16,11 @@ import { Editor, ReactRenderer } from "@tiptap/react"; import { EditorContainer, EditorContentWrapper } from "@/components/editors"; import { LinkView, LinkViewProps } from "@/components/links"; import { BlockMenu } from "@/components/menus"; +// types +import { TDisplayConfig } from "@/types"; type IPageRenderer = { + displayConfig: TDisplayConfig; editor: Editor; editorContainerClassName: string; id: string; @@ -25,7 +28,7 @@ type IPageRenderer = { }; export const PageRenderer = (props: IPageRenderer) => { - const { editor, editorContainerClassName, id, tabIndex } = props; + const { displayConfig, editor, editorContainerClassName, id, tabIndex } = props; // states const [linkViewProps, setLinkViewProps] = useState(); const [isOpen, setIsOpen] = useState(false); @@ -128,7 +131,12 @@ export const PageRenderer = (props: IPageRenderer) => { return ( <>
- + {editor.isEditable && } diff --git a/packages/editor/src/core/components/editors/document/read-only-editor.tsx b/packages/editor/src/core/components/editors/document/read-only-editor.tsx index b259574e4..d7e56f50a 100644 --- a/packages/editor/src/core/components/editors/document/read-only-editor.tsx +++ b/packages/editor/src/core/components/editors/document/read-only-editor.tsx @@ -1,6 +1,8 @@ import { forwardRef, MutableRefObject } from "react"; // components import { PageRenderer } from "@/components/editors"; +// constants +import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; // extensions import { IssueWidget } from "@/extensions"; // helpers @@ -10,12 +12,13 @@ import { useReadOnlyEditor } from "@/hooks/use-read-only-editor"; // plane web types import { TEmbedConfig } from "@/plane-editor/types"; // types -import { EditorReadOnlyRefApi, IMentionHighlight } from "@/types"; +import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig } from "@/types"; interface IDocumentReadOnlyEditor { id: string; initialValue: string; containerClassName: string; + displayConfig?: TDisplayConfig; editorClassName?: string; embedHandler: TEmbedConfig; tabIndex?: number; @@ -29,6 +32,7 @@ interface IDocumentReadOnlyEditor { const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => { const { containerClassName, + displayConfig = DEFAULT_DISPLAY_CONFIG, editorClassName = "", embedHandler, id, @@ -39,17 +43,17 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => { mentionHandler, } = props; const editor = useReadOnlyEditor({ - initialValue, editorClassName, - mentionHandler, - forwardedRef, - handleEditorReady, extensions: [ embedHandler?.issue && IssueWidget({ widgetCallback: embedHandler?.issue.widgetCallback, }), ], + forwardedRef, + handleEditorReady, + initialValue, + mentionHandler, }); if (!editor) { @@ -61,7 +65,13 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => { }); return ( - + ); }; diff --git a/packages/editor/src/core/components/editors/editor-container.tsx b/packages/editor/src/core/components/editors/editor-container.tsx index 5102cf9ac..2e6ad5250 100644 --- a/packages/editor/src/core/components/editors/editor-container.tsx +++ b/packages/editor/src/core/components/editors/editor-container.tsx @@ -1,17 +1,22 @@ import { FC, ReactNode } from "react"; import { Editor } from "@tiptap/react"; +// constants +import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; // helpers import { cn } from "@/helpers/common"; +// types +import { TDisplayConfig } from "@/types"; interface EditorContainerProps { children: ReactNode; + displayConfig: TDisplayConfig; editor: Editor | null; editorContainerClassName: string; id: string; } export const EditorContainer: FC = (props) => { - const { children, editor, editorContainerClassName, id } = props; + const { children, displayConfig, editor, editorContainerClassName, id } = props; const handleContainerClick = () => { if (!editor) return; @@ -65,10 +70,12 @@ export const EditorContainer: FC = (props) => { onClick={handleContainerClick} onMouseLeave={handleContainerMouseLeave} className={cn( - "cursor-text relative", + "editor-container cursor-text relative", { "active-editor": editor?.isFocused && editor?.isEditable, }, + displayConfig.fontSize ?? DEFAULT_DISPLAY_CONFIG.fontSize, + displayConfig.fontStyle ?? DEFAULT_DISPLAY_CONFIG.fontStyle, editorContainerClassName )} > diff --git a/packages/editor/src/core/components/editors/editor-wrapper.tsx b/packages/editor/src/core/components/editors/editor-wrapper.tsx index 9d2d6b2a7..3e00dc2af 100644 --- a/packages/editor/src/core/components/editors/editor-wrapper.tsx +++ b/packages/editor/src/core/components/editors/editor-wrapper.tsx @@ -1,6 +1,8 @@ import { Editor, Extension } from "@tiptap/core"; // components import { EditorContainer } from "@/components/editors"; +// constants +import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; // hooks import { getEditorClassNames } from "@/helpers/common"; import { useEditor } from "@/hooks/use-editor"; @@ -17,6 +19,7 @@ export const EditorWrapper: React.FC = (props) => { const { children, containerClassName, + displayConfig = DEFAULT_DISPLAY_CONFIG, editorClassName = "", extensions, id, @@ -54,7 +57,12 @@ export const EditorWrapper: React.FC = (props) => { if (!editor) return null; return ( - + {children?.(editor)}
diff --git a/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx b/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx index d75a5d530..fc0911bee 100644 --- a/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx +++ b/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx @@ -1,5 +1,7 @@ // components import { EditorContainer, EditorContentWrapper } from "@/components/editors"; +// constants +import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; // helpers import { getEditorClassNames } from "@/helpers/common"; // hooks @@ -8,12 +10,20 @@ import { useReadOnlyEditor } from "@/hooks/use-read-only-editor"; import { IReadOnlyEditorProps } from "@/types"; export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => { - const { containerClassName, editorClassName = "", id, initialValue, forwardedRef, mentionHandler } = props; + const { + containerClassName, + displayConfig = DEFAULT_DISPLAY_CONFIG, + editorClassName = "", + id, + initialValue, + forwardedRef, + mentionHandler, + } = props; const editor = useReadOnlyEditor({ - initialValue, editorClassName, forwardedRef, + initialValue, mentionHandler, }); @@ -24,7 +34,12 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => { if (!editor) return null; return ( - +
diff --git a/packages/editor/src/core/constants/config.ts b/packages/editor/src/core/constants/config.ts new file mode 100644 index 000000000..5a9577044 --- /dev/null +++ b/packages/editor/src/core/constants/config.ts @@ -0,0 +1,7 @@ +// types +import { TDisplayConfig } from "@/types"; + +export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = { + fontSize: "large-font", + fontStyle: "sans-serif", +}; diff --git a/packages/editor/src/core/extensions/code/code-block-node-view.tsx b/packages/editor/src/core/extensions/code/code-block-node-view.tsx index b383b4c81..8dbdb044f 100644 --- a/packages/editor/src/core/extensions/code/code-block-node-view.tsx +++ b/packages/editor/src/core/extensions/code/code-block-node-view.tsx @@ -56,7 +56,7 @@ export const CodeBlockComponent: React.FC = ({ node })
-        
+        
       
); diff --git a/packages/editor/src/core/hooks/use-document-editor.ts b/packages/editor/src/core/hooks/use-document-editor.ts index 21b224b9d..e21b7f1b8 100644 --- a/packages/editor/src/core/hooks/use-document-editor.ts +++ b/packages/editor/src/core/hooks/use-document-editor.ts @@ -5,7 +5,7 @@ import * as Y from "yjs"; // extensions import { IssueWidget, SideMenuExtension } from "@/extensions"; // hooks -import { TFileHandler, useEditor } from "@/hooks/use-editor"; +import { useEditor } from "@/hooks/use-editor"; // plane editor extensions import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions"; // plane editor provider @@ -13,7 +13,7 @@ import { CollaborationProvider } from "@/plane-editor/providers"; // plane editor types import { TEmbedConfig } from "@/plane-editor/types"; // types -import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TExtensions } from "@/types"; +import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TExtensions, TFileHandler } from "@/types"; type DocumentEditorProps = { disabledExtensions?: TExtensions[]; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 41d962af0..01718fbe7 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -1,7 +1,7 @@ import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react"; import { Selection } from "@tiptap/pm/state"; import { EditorProps } from "@tiptap/pm/view"; -import { useEditor as useCustomEditor, Editor } from "@tiptap/react"; +import { useEditor as useTiptapEditor, Editor } from "@tiptap/react"; // components import { getEditorMenuItems } from "@/components/menus"; // extensions @@ -14,22 +14,7 @@ import { CollaborationProvider } from "@/plane-editor/providers"; // props import { CoreEditorProps } from "@/props"; // types -import { - DeleteImage, - EditorRefApi, - IMentionHighlight, - IMentionSuggestion, - RestoreImage, - TEditorCommands, - UploadImage, -} from "@/types"; - -export type TFileHandler = { - cancel: () => void; - delete: DeleteImage; - upload: UploadImage; - restore: RestoreImage; -}; +import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types"; export interface CustomEditorProps { editorClassName: string; @@ -54,26 +39,30 @@ export interface CustomEditorProps { value?: string | null | undefined; } -export const useEditor = ({ - editorClassName, - editorProps = {}, - enableHistory, - extensions = [], - fileHandler, - forwardedRef, - handleEditorReady, - id = "", - initialValue, - mentionHandler, - onChange, - placeholder, - provider, - tabIndex, - value, -}: CustomEditorProps) => { - const editor = useCustomEditor({ +export const useEditor = (props: CustomEditorProps) => { + const { + editorClassName, + editorProps = {}, + enableHistory, + extensions = [], + fileHandler, + forwardedRef, + handleEditorReady, + id = "", + initialValue, + mentionHandler, + onChange, + placeholder, + provider, + tabIndex, + value, + } = props; + + const editor = useTiptapEditor({ editorProps: { - ...CoreEditorProps(editorClassName), + ...CoreEditorProps({ + editorClassName, + }), ...editorProps, }, extensions: [ @@ -95,18 +84,10 @@ export const useEditor = ({ ...extensions, ], content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "

", - onCreate: async () => { - handleEditorReady?.(true); - }, - onTransaction: async ({ editor }) => { - setSavedSelection(editor.state.selection); - }, - onUpdate: async ({ editor }) => { - onChange?.(editor.getJSON(), editor.getHTML()); - }, - onDestroy: async () => { - handleEditorReady?.(false); - }, + onCreate: () => handleEditorReady?.(true), + onTransaction: ({ editor }) => setSavedSelection(editor.state.selection), + onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()), + onDestroy: () => handleEditorReady?.(false), }); const editorRef: MutableRefObject = useRef(null); diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index fcaf0c6dd..0a737873e 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -35,7 +35,9 @@ export const useReadOnlyEditor = ({ editable: false, content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "

", editorProps: { - ...CoreReadOnlyEditorProps(editorClassName), + ...CoreReadOnlyEditorProps({ + editorClassName, + }), ...editorProps, }, onCreate: async () => { diff --git a/packages/editor/src/core/props/props.tsx b/packages/editor/src/core/props/props.tsx index 11e829162..4bda3e51a 100644 --- a/packages/editor/src/core/props/props.tsx +++ b/packages/editor/src/core/props/props.tsx @@ -2,7 +2,13 @@ import { EditorProps } from "@tiptap/pm/view"; // helpers import { cn } from "@/helpers/common"; -export function CoreEditorProps(editorClassName: string): EditorProps { +export type TCoreEditorProps = { + editorClassName: string; +}; + +export const CoreEditorProps = (props: TCoreEditorProps): EditorProps => { + const { editorClassName } = props; + return { attributes: { class: cn( @@ -25,4 +31,4 @@ export function CoreEditorProps(editorClassName: string): EditorProps { return html.replace(//g, ""); }, }; -} +}; diff --git a/packages/editor/src/core/props/read-only.tsx b/packages/editor/src/core/props/read-only.tsx index ea583938f..aaa635a50 100644 --- a/packages/editor/src/core/props/read-only.tsx +++ b/packages/editor/src/core/props/read-only.tsx @@ -1,12 +1,18 @@ import { EditorProps } from "@tiptap/pm/view"; // helpers import { cn } from "@/helpers/common"; +// props +import { TCoreEditorProps } from "@/props"; -export const CoreReadOnlyEditorProps = (editorClassName: string): EditorProps => ({ - attributes: { - class: cn( - "prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none", - editorClassName - ), - }, -}); +export const CoreReadOnlyEditorProps = (props: TCoreEditorProps): EditorProps => { + const { editorClassName } = props; + + return { + attributes: { + class: cn( + "prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none", + editorClassName + ), + }, + }; +}; diff --git a/packages/editor/src/core/types/config.ts b/packages/editor/src/core/types/config.ts new file mode 100644 index 000000000..93d612e59 --- /dev/null +++ b/packages/editor/src/core/types/config.ts @@ -0,0 +1,17 @@ +import { DeleteImage, RestoreImage, UploadImage } from "@/types"; + +export type TFileHandler = { + cancel: () => void; + delete: DeleteImage; + upload: UploadImage; + restore: RestoreImage; +}; + +export type TEditorFontStyle = "sans-serif" | "serif" | "monospace"; + +export type TEditorFontSize = "small-font" | "large-font"; + +export type TDisplayConfig = { + fontStyle?: TEditorFontStyle; + fontSize?: TEditorFontSize; +}; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 695082c11..f817969c3 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -1,9 +1,7 @@ // helpers import { IMarking } from "@/helpers/scroll-to-node"; -// hooks -import { TFileHandler } from "@/hooks/use-editor"; // types -import { IMentionHighlight, IMentionSuggestion, TEditorCommands } from "@/types"; +import { IMentionHighlight, IMentionSuggestion, TDisplayConfig, TEditorCommands, TFileHandler } from "@/types"; export type EditorReadOnlyRefApi = { getMarkDown: () => string; @@ -26,6 +24,7 @@ export interface EditorRefApi extends EditorReadOnlyRefApi { export interface IEditorProps { containerClassName?: string; + displayConfig?: TDisplayConfig; editorClassName?: string; fileHandler: TFileHandler; forwardedRef?: React.MutableRefObject; @@ -50,6 +49,7 @@ export interface IRichTextEditor extends IEditorProps { export interface IReadOnlyEditorProps { containerClassName?: string; + displayConfig?: TDisplayConfig; editorClassName?: string; forwardedRef?: React.MutableRefObject; id: string; diff --git a/packages/editor/src/core/types/index.ts b/packages/editor/src/core/types/index.ts index 891a86286..7190cfb51 100644 --- a/packages/editor/src/core/types/index.ts +++ b/packages/editor/src/core/types/index.ts @@ -1,3 +1,4 @@ +export * from "./config"; export * from "./editor"; export * from "./embed"; export * from "./extensions"; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 828fab021..4a317e41c 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -34,5 +34,5 @@ export { type IMarking, useEditorMarkings } from "@/hooks/use-editor-markings"; export { useReadOnlyEditor } from "@/hooks/use-read-only-editor"; // types -export type { CustomEditorProps, TFileHandler } from "@/hooks/use-editor"; +export type { CustomEditorProps } from "@/hooks/use-editor"; export * from "@/types"; diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index 28fb2dd11..2ec35dc25 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -1,12 +1,82 @@ +.editor-container { + &.large-font { + --font-size-h1: 1.75rem; + --font-size-h2: 1.5rem; + --font-size-h3: 1.375rem; + --font-size-h4: 1.25rem; + --font-size-h5: 1.125rem; + --font-size-h6: 1rem; + --font-size-regular: 1rem; + --font-size-list: var(--font-size-regular); + --font-size-code: var(--font-size-regular); + + --line-height-h1: 2.25rem; + --line-height-h2: 2rem; + --line-height-h3: 1.75rem; + --line-height-h4: 1.5rem; + --line-height-h5: 1.5rem; + --line-height-h6: 1.5rem; + --line-height-regular: 1.5rem; + --line-height-list: var(--line-height-regular); + --line-height-code: var(--line-height-regular); + } + + &.small-font { + --font-size-h1: 1.4rem; + --font-size-h2: 1.2rem; + --font-size-h3: 1.1rem; + --font-size-h4: 1rem; + --font-size-h5: 0.9rem; + --font-size-h6: 0.8rem; + --font-size-regular: 0.8rem; + --font-size-list: var(--font-size-regular); + --font-size-code: var(--font-size-regular); + + --line-height-h1: 1.8rem; + --line-height-h2: 1.6rem; + --line-height-h3: 1.4rem; + --line-height-h4: 1.2rem; + --line-height-h5: 1.2rem; + --line-height-h6: 1.2rem; + --line-height-regular: 1.2rem; + --line-height-list: var(--line-height-regular); + --line-height-code: var(--line-height-regular); + } + + &.sans-serif { + --font-style: sans-serif; + } + + &.serif { + --font-style: serif; + } + + &.monospace { + --font-style: monospace; + } +} + .ProseMirror { - --font-size-h1: 1.5rem; - --font-size-h2: 1.3125rem; - --font-size-h3: 1.125rem; - --font-size-h4: 0.9375rem; - --font-size-h5: 0.8125rem; - --font-size-h6: 0.75rem; - --font-size-regular: 0.9375rem; - --font-size-list: var(--font-size-regular); + position: relative; + word-wrap: break-word; + white-space: pre-wrap; + -moz-tab-size: 4; + tab-size: 4; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; + outline: none; + cursor: text; + font-family: var(--font-style); + font-size: var(--font-size-regular); + line-height: 1.2; + color: inherit; + -moz-box-sizing: border-box; + box-sizing: border-box; + appearance: textfield; + -webkit-appearance: textfield; + -moz-appearance: textfield; } .ProseMirror p.is-editor-empty:first-child::before { @@ -179,29 +249,6 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { max-width: 400px !important; } -.ProseMirror { - position: relative; - word-wrap: break-word; - white-space: pre-wrap; - -moz-tab-size: 4; - tab-size: 4; - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; - outline: none; - cursor: text; - line-height: 1.2; - font-family: inherit; - font-size: var(--font-size-regular); - color: inherit; - -moz-box-sizing: border-box; - box-sizing: border-box; - appearance: textfield; - -webkit-appearance: textfield; - -moz-appearance: textfield; -} - .fade-in { opacity: 1; transition: opacity 0.3s ease-in; @@ -248,6 +295,7 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { opacity: 0; } +/* code block, inline code */ .ProseMirror pre { font-family: JetBrainsMono, monospace; tab-size: 2; @@ -256,10 +304,14 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { .ProseMirror pre code { background: none; color: inherit; - font-size: 0.8rem; padding: 0; } +.ProseMirror code { + font-size: var(--font-size-code); +} +/* end code block, inline code */ + div[data-type="horizontalRule"] { line-height: 0; padding: 0.25rem 0; @@ -342,48 +394,48 @@ ul[data-type="taskList"] ul[data-type="taskList"] { margin-top: 2rem; margin-bottom: 4px; font-size: var(--font-size-h1); + line-height: var(--line-height-h1); font-weight: 600; - line-height: 1.3; } .prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) { margin-top: 1.4rem; margin-bottom: 1px; font-size: var(--font-size-h2); + line-height: var(--line-height-h2); font-weight: 600; - line-height: 1.3; } .prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) { margin-top: 1rem; margin-bottom: 1px; font-size: var(--font-size-h3); + line-height: var(--line-height-h3); font-weight: 600; - line-height: 1.3; } .prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) { margin-top: 1rem; margin-bottom: 1px; font-size: var(--font-size-h4); + line-height: var(--line-height-h4); font-weight: 600; - line-height: 1.5; } .prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) { margin-top: 1rem; margin-bottom: 1px; font-size: var(--font-size-h5); + line-height: var(--line-height-h5); font-weight: 600; - line-height: 1.5; } .prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) { margin-top: 1rem; margin-bottom: 1px; font-size: var(--font-size-h6); + line-height: var(--line-height-h6); font-weight: 600; - line-height: 1.5; } .prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) { @@ -391,13 +443,13 @@ ul[data-type="taskList"] ul[data-type="taskList"] { margin-bottom: 1px; padding: 3px 0; font-size: var(--font-size-regular); - line-height: 1.5; + line-height: var(--line-height-regular); } .prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p, .prose :where(ul):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p { font-size: var(--font-size-list); - line-height: 1.5; + line-height: var(--line-height-list); } .prose :where(.prose > :first-child):not(:where([class~="not-prose"], [class~="not-prose"] *)) { diff --git a/packages/editor/src/styles/table.css b/packages/editor/src/styles/table.css index 6b45abcf5..2a0140a2b 100644 --- a/packages/editor/src/styles/table.css +++ b/packages/editor/src/styles/table.css @@ -12,10 +12,6 @@ width: 100%; } -.table-wrapper table p { - font-size: 14px; -} - .table-wrapper table td, .table-wrapper table th { min-width: 1em; @@ -115,4 +111,3 @@ opacity: 0; pointer-events: none; } - diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index a87d19146..660768845 100644 --- a/packages/ui/src/icons/index.ts +++ b/packages/ui/src/icons/index.ts @@ -15,9 +15,12 @@ export * from "./github-icon"; export * from "./gitlab-icon"; export * from "./layer-stack"; export * from "./layers-icon"; +export * from "./monospace-icon"; export * from "./photo-filter-icon"; export * from "./priority-icon"; export * from "./related-icon"; +export * from "./sans-serif-icon"; +export * from "./serif-icon"; export * from "./side-panel-icon"; export * from "./transfer-icon"; export * from "./info-icon"; diff --git a/packages/ui/src/icons/monospace-icon.tsx b/packages/ui/src/icons/monospace-icon.tsx new file mode 100644 index 000000000..8eb64f01c --- /dev/null +++ b/packages/ui/src/icons/monospace-icon.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const MonospaceIcon: React.FC = ({ className = "text-current", ...rest }) => ( + + + + +); diff --git a/packages/ui/src/icons/sans-serif-icon.tsx b/packages/ui/src/icons/sans-serif-icon.tsx new file mode 100644 index 000000000..2260130a7 --- /dev/null +++ b/packages/ui/src/icons/sans-serif-icon.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const SansSerifIcon: React.FC = ({ className = "text-current", ...rest }) => ( + + + + +); diff --git a/packages/ui/src/icons/serif-icon.tsx b/packages/ui/src/icons/serif-icon.tsx new file mode 100644 index 000000000..fab5c12d3 --- /dev/null +++ b/packages/ui/src/icons/serif-icon.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const SerifIcon: React.FC = ({ className = "text-current", ...rest }) => ( + + + + +); diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 4b4c80a3a..64628622d 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -8,6 +8,7 @@ import { EditorReadOnlyRefApi, EditorRefApi, IMarking, + TDisplayConfig, } from "@plane/editor"; // types import { IUserLite } from "@plane/types"; @@ -82,12 +83,16 @@ export const PageEditorBody: React.FC = observer((props) => { }); // editor flaggings const { documentEditor } = useEditorFlagging(); - // page filters - const { isFullWidth } = usePageFilters(); + const { fontSize, fontStyle, isFullWidth } = usePageFilters(); // issue-embed const { issueEmbedProps } = useIssueEmbed(workspaceSlug?.toString() ?? "", projectId?.toString() ?? ""); + const displayConfig: TDisplayConfig = { + fontSize, + fontStyle, + }; + useEffect(() => { updateMarkings(pageDescription ?? "

"); }, [pageDescription, updateMarkings]); @@ -139,6 +144,7 @@ export const PageEditorBody: React.FC = observer((props) => { value={pageDescriptionYJS} ref={editorRef} containerClassName="p-0 pb-64" + displayConfig={displayConfig} editorClassName="pl-10" onChange={handleDescriptionChange} mentionHandler={{ @@ -157,6 +163,7 @@ export const PageEditorBody: React.FC = observer((props) => { initialValue={pageDescription ?? "

"} handleEditorReady={handleReadOnlyEditorReady} containerClassName="p-0 pb-64 border-none" + displayConfig={displayConfig} editorClassName="pl-10" mentionHandler={{ highlights: mentionHighlights, diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index af2f2e6af..016ca8eb5 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -40,6 +40,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { const { workspaceSlug, projectId } = useParams(); // page filters const { isFullWidth, handleFullWidth } = usePageFilters(); + const handleArchivePage = async () => await archive().catch(() => setToast({ diff --git a/web/core/components/pages/editor/title.tsx b/web/core/components/pages/editor/title.tsx index 59502ba81..56d2f18f5 100644 --- a/web/core/components/pages/editor/title.tsx +++ b/web/core/components/pages/editor/title.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { CSSProperties, useState } from "react"; import { observer } from "mobx-react"; // editor import { EditorRefApi } from "@plane/editor"; @@ -8,6 +8,8 @@ import { EditorRefApi } from "@plane/editor"; import { TextArea } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; +// hooks +import { usePageFilters } from "@/hooks/use-page-filters"; type Props = { editorRef: React.RefObject; @@ -20,25 +22,28 @@ export const PageEditorTitle: React.FC = observer((props) => { const { editorRef, readOnly, title, updateTitle } = props; // states const [isLengthVisible, setIsLengthVisible] = useState(false); + // page filters + const { fontSize, fontStyle } = usePageFilters(); + // ui + const titleClassName = cn("bg-transparent tracking-[-2%] font-semibold", { + "text-[1.6rem] leading-[1.8rem]": fontSize === "small-font", + "text-[2rem] leading-[2.25rem]": fontSize === "large-font", + }); + const titleStyle: CSSProperties = { + fontFamily: fontStyle, + }; return ( <> {readOnly ? ( -
+
{title}
) : ( <>