diff --git a/.dockerignore b/.dockerignore index fe11e95b6..e0b681fef 100644 --- a/.dockerignore +++ b/.dockerignore @@ -66,4 +66,4 @@ temp/ .react-router/ build/ node_modules/ -README.md \ No newline at end of file +README.md diff --git a/.github/instructions/bash.instructions.md b/.github/instructions/bash.instructions.md new file mode 100644 index 000000000..b14d3c9ed --- /dev/null +++ b/.github/instructions/bash.instructions.md @@ -0,0 +1,48 @@ +--- +description: Guidelines for bash commands and tooling in the monorepo +applyTo: "**/*.sh" +--- + +# Bash & Tooling Instructions + +This document outlines the standard tools and commands used in this monorepo. + +## Package Manager + +We use **pnpm** for package management. +- **Do not use `npm` or `yarn`.** +- Lockfile: `pnpm-lock.yaml` +- Workspace configuration: `pnpm-workspace.yaml` + +### Common Commands +- Install dependencies: `pnpm install` +- Run a script in a specific package: `pnpm --filter run )} diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx index 3d58991ba..0ee635460 100644 --- a/apps/web/app/not-found.tsx +++ b/apps/web/app/not-found.tsx @@ -1,46 +1,42 @@ -"use client"; - -import React from "react"; -import type { Metadata } from "next"; -import Image from "next/image"; import Link from "next/link"; // ui import { Button } from "@plane/propel/button"; // images -import Image404 from "@/public/404.svg"; +import Image404 from "@/app/assets/404.svg?url"; +// types +import type { Route } from "./+types/not-found"; -export const metadata: Metadata = { - title: "404 - Page Not Found", - robots: { - index: false, - follow: false, - }, -}; +export const meta: Route.MetaFunction = () => [ + { title: "404 - Page Not Found" }, + { name: "robots", content: "noindex, nofollow" }, +]; -const PageNotFound = () => ( -
-
-
-
- 404- Page not found +function PageNotFound() { + return ( +
+
+
+
+ 404- Page not found +
+
+

Oops! Something went wrong.

+

+ Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is + temporarily unavailable. +

+
+ + + + +
-
-

Oops! Something went wrong.

-

- Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is - temporarily unavailable. -

-
- - - - -
-
-); + ); +} export default PageNotFound; diff --git a/apps/web/app/provider.tsx b/apps/web/app/provider.tsx index a83c75f9b..7d6811052 100644 --- a/apps/web/app/provider.tsx +++ b/apps/web/app/provider.tsx @@ -1,64 +1,64 @@ -"use client"; - -import type { FC, ReactNode } from "react"; -import { AppProgressProvider as ProgressProvider } from "@bprogress/next"; -import dynamic from "next/dynamic"; +import { lazy, Suspense } from "react"; import { useTheme, ThemeProvider } from "next-themes"; import { SWRConfig } from "swr"; // Plane Imports import { WEB_SWR_CONFIG } from "@plane/constants"; import { TranslationProvider } from "@plane/i18n"; import { Toast } from "@plane/propel/toast"; -//helpers +// helpers import { resolveGeneralTheme } from "@plane/utils"; // polyfills import "@/lib/polyfills"; +// progress bar +import { AppProgressBar } from "@/lib/b-progress"; // mobx store provider import { StoreProvider } from "@/lib/store-context"; // wrappers import { InstanceWrapper } from "@/lib/wrappers/instance-wrapper"; -// dynamic imports -const StoreWrapper = dynamic(() => import("@/lib/wrappers/store-wrapper"), { ssr: false }); -const PostHogProvider = dynamic(() => import("@/lib/posthog-provider"), { ssr: false }); -const IntercomProvider = dynamic(() => import("@/lib/intercom-provider"), { ssr: false }); + +// lazy imports +const StoreWrapper = lazy(function StoreWrapper() { + return import("@/lib/wrappers/store-wrapper"); +}); + +const PostHogProvider = lazy(function PostHogProvider() { + return import("@/lib/posthog-provider"); +}); + +const ChatSupportModal = lazy(function ChatSupportModal() { + return import("@/components/global/chat-support-modal"); +}); export interface IAppProvider { - children: ReactNode; + children: React.ReactNode; } -const ToastWithTheme = () => { +function ToastWithTheme() { const { resolvedTheme } = useTheme(); return ; -}; +} -export const AppProvider: FC = (props) => { +export function AppProvider(props: IAppProvider) { const { children } = props; // themes return ( - <> - - - - - - - - - - {children} - - - - - - - - - + + + + + + + + + + + {children} + + + + + + + ); -}; +} diff --git a/apps/web/app/root.tsx b/apps/web/app/root.tsx new file mode 100644 index 000000000..fe1c36765 --- /dev/null +++ b/apps/web/app/root.tsx @@ -0,0 +1,129 @@ +import type { ReactNode } from "react"; +import * as Sentry from "@sentry/react-router"; +import Script from "next/script"; +import { Links, Meta, Outlet, Scripts } from "react-router"; +import type { LinksFunction } from "react-router"; +// plane imports +import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants"; +import { cn } from "@plane/utils"; +// types +// assets +import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url"; +import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url"; +import faviconIco from "@/app/assets/favicon/favicon.ico?url"; +import icon180 from "@/app/assets/icons/icon-180x180.png?url"; +import icon512 from "@/app/assets/icons/icon-512x512.png?url"; +import ogImage from "@/app/assets/og-image.png?url"; +import { LogoSpinner } from "@/components/common/logo-spinner"; +import globalStyles from "@/styles/globals.css?url"; +import type { Route } from "./+types/root"; +// local +import { CustomErrorComponent } from "./error"; +import { AppProvider } from "./provider"; + +const APP_TITLE = "Plane | Simple, extensible, open-source project management tool."; + +export const links: LinksFunction = () => [ + { rel: "icon", type: "image/png", sizes: "32x32", href: favicon32 }, + { rel: "icon", type: "image/png", sizes: "16x16", href: favicon16 }, + { rel: "shortcut icon", href: faviconIco }, + { rel: "manifest", href: "/site.webmanifest.json" }, + { rel: "apple-touch-icon", href: icon512 }, + { rel: "apple-touch-icon", sizes: "180x180", href: icon180 }, + { rel: "apple-touch-icon", sizes: "512x512", href: icon512 }, + { rel: "manifest", href: "/manifest.json" }, + { rel: "stylesheet", href: globalStyles }, +]; + +export function Layout({ children }: { children: ReactNode }) { + const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0"); + + return ( + + + + + + {/* Meta info for PWA */} + + + + + + + + + + +
+
+ +
+
{children}
+
+
+ + {!!isSessionRecorderEnabled && process.env.VITE_SESSION_RECORDER_KEY && ( + + )} + + + ); +} + +export const meta: Route.MetaFunction = () => [ + { title: APP_TITLE }, + { name: "description", content: SITE_DESCRIPTION }, + { property: "og:title", content: APP_TITLE }, + { + property: "og:description", + content: "Open-source project management tool to manage work items, cycles, and product roadmaps easily", + }, + { property: "og:url", content: "https://app.plane.so/" }, + { property: "og:image", content: ogImage }, + { property: "og:image:width", content: "1200" }, + { property: "og:image:height", content: "630" }, + { property: "og:image:alt", content: "Plane - Modern project management" }, + { + name: "keywords", + content: + "software development, plan, ship, software, accelerate, code management, release management, project management, work item tracking, agile, scrum, kanban, collaboration", + }, + { name: "twitter:site", content: "@planepowers" }, + { name: "twitter:card", content: "summary_large_image" }, + { name: "twitter:image", content: ogImage }, + { name: "twitter:image:width", content: "1200" }, + { name: "twitter:image:height", content: "630" }, + { name: "twitter:image:alt", content: "Plane - Modern project management" }, +]; + +export default function Root() { + return ; +} + +export function HydrateFallback() { + return ( +
+ +
+ ); +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + if (error) { + Sentry.captureException(error); + } + + return ; +} diff --git a/apps/web/app/routes.ts b/apps/web/app/routes.ts new file mode 100644 index 000000000..258993783 --- /dev/null +++ b/apps/web/app/routes.ts @@ -0,0 +1,84 @@ +import { route } from "@react-router/dev/routes"; +import type { RouteConfigEntry } from "@react-router/dev/routes"; +import { coreRoutes } from "./routes/core"; +import { extendedRoutes } from "./routes/extended"; + +/** + * Merges two route configurations intelligently. + * - Deep merges children when the same layout file exists in both arrays + * - Deduplicates routes by file property, preferring extended over core + * - Maintains order: core routes first, then extended routes at each level + */ +function mergeRoutes(core: RouteConfigEntry[], extended: RouteConfigEntry[]): RouteConfigEntry[] { + // Step 1: Create a Map to track routes by file path + const routeMap = new Map(); + + // Step 2: Process core routes first + for (const coreRoute of core) { + const fileKey = coreRoute.file; + routeMap.set(fileKey, coreRoute); + } + + // Step 3: Process extended routes + for (const extendedRoute of extended) { + const fileKey = extendedRoute.file; + + if (routeMap.has(fileKey)) { + // Route exists in both - need to merge + const coreRoute = routeMap.get(fileKey)!; + + // Check if both have children (layouts that need deep merging) + if (coreRoute.children && extendedRoute.children) { + // Deep merge: recursively merge children + const mergedChildren = mergeRoutes( + Array.isArray(coreRoute.children) ? coreRoute.children : [], + Array.isArray(extendedRoute.children) ? extendedRoute.children : [] + ); + routeMap.set(fileKey, { + ...extendedRoute, + children: mergedChildren, + }); + } else { + // No children or only one has children - prefer extended + routeMap.set(fileKey, extendedRoute); + } + } else { + // Route only exists in extended + routeMap.set(fileKey, extendedRoute); + } + } + + // Step 4: Build final array maintaining order (core first, then extended-only) + const result: RouteConfigEntry[] = []; + + // Add all core routes (now merged or original) + for (const coreRoute of core) { + const fileKey = coreRoute.file; + if (routeMap.has(fileKey)) { + result.push(routeMap.get(fileKey)!); + routeMap.delete(fileKey); // Remove so we don't add it again + } + } + + // Add remaining extended-only routes + for (const extendedRoute of extended) { + const fileKey = extendedRoute.file; + if (routeMap.has(fileKey)) { + result.push(routeMap.get(fileKey)!); + routeMap.delete(fileKey); + } + } + + return result; +} + +/** + * Main Routes Configuration + * This file serves as the entry point for the route configuration. + */ +const mergedRoutes: RouteConfigEntry[] = mergeRoutes(coreRoutes, extendedRoutes); + +// Add catch-all route at the end (404 handler) +const routes: RouteConfigEntry[] = [...mergedRoutes, route("*", "./not-found.tsx")]; + +export default routes; diff --git a/apps/web/app/routes/core.ts b/apps/web/app/routes/core.ts new file mode 100644 index 000000000..ccb9d78d3 --- /dev/null +++ b/apps/web/app/routes/core.ts @@ -0,0 +1,409 @@ +import { index, layout, route } from "@react-router/dev/routes"; +import type { RouteConfig, RouteConfigEntry } from "@react-router/dev/routes"; + +export const coreRoutes: RouteConfigEntry[] = [ + // ======================================================================== + // USER MANAGEMENT ROUTES + // ======================================================================== + + // Home - Sign In + layout("./(home)/layout.tsx", [index("./(home)/page.tsx")]), + + // Sign Up + layout("./(all)/sign-up/layout.tsx", [route("sign-up", "./(all)/sign-up/page.tsx")]), + + // Account Routes - Password Management + layout("./(all)/accounts/forgot-password/layout.tsx", [ + route("accounts/forgot-password", "./(all)/accounts/forgot-password/page.tsx"), + ]), + layout("./(all)/accounts/reset-password/layout.tsx", [ + route("accounts/reset-password", "./(all)/accounts/reset-password/page.tsx"), + ]), + layout("./(all)/accounts/set-password/layout.tsx", [ + route("accounts/set-password", "./(all)/accounts/set-password/page.tsx"), + ]), + + // Create Workspace + layout("./(all)/create-workspace/layout.tsx", [route("create-workspace", "./(all)/create-workspace/page.tsx")]), + + // Onboarding + layout("./(all)/onboarding/layout.tsx", [route("onboarding", "./(all)/onboarding/page.tsx")]), + + // Invitations + layout("./(all)/invitations/layout.tsx", [route("invitations", "./(all)/invitations/page.tsx")]), + + // Workspace Invitations + layout("./(all)/workspace-invitations/layout.tsx", [ + route("workspace-invitations", "./(all)/workspace-invitations/page.tsx"), + ]), + + // ======================================================================== + // ALL APP ROUTES + // ======================================================================== + layout("./(all)/layout.tsx", [ + // ====================================================================== + // WORKSPACE-SCOPED ROUTES + // ====================================================================== + layout("./(all)/[workspaceSlug]/layout.tsx", [ + // ==================================================================== + // PROJECTS APP SECTION - WORKSPACE LEVEL ROUTES + // ==================================================================== + layout("./(all)/[workspaceSlug]/(projects)/layout.tsx", [ + // -------------------------------------------------------------------- + // WORKSPACE LEVEL ROUTES + // -------------------------------------------------------------------- + + // Workspace Home + route(":workspaceSlug", "./(all)/[workspaceSlug]/(projects)/page.tsx"), + + // Active Cycles + layout("./(all)/[workspaceSlug]/(projects)/active-cycles/layout.tsx", [ + route(":workspaceSlug/active-cycles", "./(all)/[workspaceSlug]/(projects)/active-cycles/page.tsx"), + ]), + + // Analytics + layout("./(all)/[workspaceSlug]/(projects)/analytics/[tabId]/layout.tsx", [ + route(":workspaceSlug/analytics/:tabId", "./(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx"), + ]), + + // Browse + layout("./(all)/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx", [ + route(":workspaceSlug/browse/:workItem", "./(all)/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx"), + ]), + + // Drafts + layout("./(all)/[workspaceSlug]/(projects)/drafts/layout.tsx", [ + route(":workspaceSlug/drafts", "./(all)/[workspaceSlug]/(projects)/drafts/page.tsx"), + ]), + + // Notifications + layout("./(all)/[workspaceSlug]/(projects)/notifications/layout.tsx", [ + route(":workspaceSlug/notifications", "./(all)/[workspaceSlug]/(projects)/notifications/page.tsx"), + ]), + + // Profile + layout("./(all)/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx", [ + route(":workspaceSlug/profile/:userId", "./(all)/[workspaceSlug]/(projects)/profile/[userId]/page.tsx"), + route( + ":workspaceSlug/profile/:userId/:profileViewId", + "./(all)/[workspaceSlug]/(projects)/profile/[userId]/[profileViewId]/page.tsx" + ), + route( + ":workspaceSlug/profile/:userId/activity", + "./(all)/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx" + ), + ]), + + // Stickies + layout("./(all)/[workspaceSlug]/(projects)/stickies/layout.tsx", [ + route(":workspaceSlug/stickies", "./(all)/[workspaceSlug]/(projects)/stickies/page.tsx"), + ]), + + // Workspace Views + layout("./(all)/[workspaceSlug]/(projects)/workspace-views/layout.tsx", [ + route(":workspaceSlug/workspace-views", "./(all)/[workspaceSlug]/(projects)/workspace-views/page.tsx"), + route( + ":workspaceSlug/workspace-views/:globalViewId", + "./(all)/[workspaceSlug]/(projects)/workspace-views/[globalViewId]/page.tsx" + ), + ]), + + // Archived Projects + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx", [ + route( + ":workspaceSlug/projects/archives", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx" + ), + ]), + + // -------------------------------------------------------------------- + // PROJECT LEVEL ROUTES + // -------------------------------------------------------------------- + + // Project List + layout("./(all)/[workspaceSlug]/(projects)/projects/(list)/layout.tsx", [ + route(":workspaceSlug/projects", "./(all)/[workspaceSlug]/(projects)/projects/(list)/page.tsx"), + ]), + + // Project Detail + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx", [ + // Project Issues List + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/issues", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx" + ), + ]), + // Issue Detail + route( + ":workspaceSlug/projects/:projectId/issues/:issueId", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx" + ), + + // Cycle Detail + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/cycles/:cycleId", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx" + ), + ]), + + // Cycles List + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/cycles", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx" + ), + ]), + + // Module Detail + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/modules/:moduleId", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx" + ), + ]), + + // Modules List + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/modules", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx" + ), + ]), + + // View Detail + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/views/:viewId", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx" + ), + ]), + + // Views List + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/views", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx" + ), + ]), + + // Page Detail + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/pages/:pageId", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx" + ), + ]), + + // Pages List + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/pages", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx" + ), + ]), + // Intake list + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/intake", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx" + ), + ]), + ]), + + // Project Archives - Issues, Cycles, Modules + // Project Archives - Issues - List + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/archives/issues", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx" + ), + ]), + + // Project Archives - Issues - Detail + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/archives/issues/:archivedIssueId", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx" + ), + ]), + + // Project Archives - Cycles + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/archives/cycles", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx" + ), + ]), + + // Project Archives - Modules + layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx", [ + route( + ":workspaceSlug/projects/:projectId/archives/modules", + "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx" + ), + ]), + ]), + + // ==================================================================== + // SETTINGS SECTION + // ==================================================================== + layout("./(all)/[workspaceSlug]/(settings)/layout.tsx", [ + // -------------------------------------------------------------------- + // WORKSPACE SETTINGS + // -------------------------------------------------------------------- + + layout("./(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx", [ + route(":workspaceSlug/settings", "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx"), + route( + ":workspaceSlug/settings/members", + "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx" + ), + route( + ":workspaceSlug/settings/billing", + "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx" + ), + route( + ":workspaceSlug/settings/exports", + "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx" + ), + route( + ":workspaceSlug/settings/webhooks", + "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx" + ), + route( + ":workspaceSlug/settings/webhooks/:webhookId", + "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx" + ), + ]), + + // -------------------------------------------------------------------- + // ACCOUNT SETTINGS + // -------------------------------------------------------------------- + + layout("./(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx", [ + route(":workspaceSlug/settings/account", "./(all)/[workspaceSlug]/(settings)/settings/account/page.tsx"), + route( + ":workspaceSlug/settings/account/activity", + "./(all)/[workspaceSlug]/(settings)/settings/account/activity/page.tsx" + ), + route( + ":workspaceSlug/settings/account/preferences", + "./(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx" + ), + route( + ":workspaceSlug/settings/account/notifications", + "./(all)/[workspaceSlug]/(settings)/settings/account/notifications/page.tsx" + ), + route( + ":workspaceSlug/settings/account/security", + "./(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx" + ), + route( + ":workspaceSlug/settings/account/api-tokens", + "./(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx" + ), + ]), + + // -------------------------------------------------------------------- + // PROJECT SETTINGS + // -------------------------------------------------------------------- + + layout("./(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx", [ + // No Projects available page + route(":workspaceSlug/settings/projects", "./(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx"), + layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx", [ + // Project Settings + route( + ":workspaceSlug/settings/projects/:projectId", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx" + ), + // Project Members + route( + ":workspaceSlug/settings/projects/:projectId/members", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx" + ), + // Project Features + route( + ":workspaceSlug/settings/projects/:projectId/features", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx" + ), + // Project States + route( + ":workspaceSlug/settings/projects/:projectId/states", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx" + ), + // Project Labels + route( + ":workspaceSlug/settings/projects/:projectId/labels", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx" + ), + // Project Estimates + route( + ":workspaceSlug/settings/projects/:projectId/estimates", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx" + ), + // Project Automations + layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/layout.tsx", [ + route( + ":workspaceSlug/settings/projects/:projectId/automations", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx" + ), + ]), + ]), + ]), + ]), + ]), + // ====================================================================== + // STANDALONE ROUTES (outside workspace context) + // ====================================================================== + + // -------------------------------------------------------------------- + // PROFILE SETTINGS + // -------------------------------------------------------------------- + + layout("./(all)/profile/layout.tsx", [ + route("profile", "./(all)/profile/page.tsx"), + route("profile/activity", "./(all)/profile/activity/page.tsx"), + route("profile/appearance", "./(all)/profile/appearance/page.tsx"), + route("profile/notifications", "./(all)/profile/notifications/page.tsx"), + route("profile/security", "./(all)/profile/security/page.tsx"), + ]), + ]), + + // ======================================================================== + // REDIRECT ROUTES + // ======================================================================== + // Legacy URL redirects for backward compatibility + + // -------------------------------------------------------------------- + // REDIRECT ROUTES + // -------------------------------------------------------------------- + + // Project settings redirect: /:workspaceSlug/projects/:projectId/settings/:path* + // → /:workspaceSlug/settings/projects/:projectId/:path* + route(":workspaceSlug/projects/:projectId/settings/*", "routes/redirects/core/project-settings.tsx"), + + // Analytics redirect: /:workspaceSlug/analytics → /:workspaceSlug/analytics/overview + route(":workspaceSlug/analytics", "routes/redirects/core/analytics.tsx"), + + // API tokens redirect: /:workspaceSlug/settings/api-tokens + // → /:workspaceSlug/settings/account/api-tokens + route(":workspaceSlug/settings/api-tokens", "routes/redirects/core/api-tokens.tsx"), + + // Inbox redirect: /:workspaceSlug/projects/:projectId/inbox + // → /:workspaceSlug/projects/:projectId/intake + route(":workspaceSlug/projects/:projectId/inbox", "routes/redirects/core/inbox.tsx"), + + // Sign-up redirects + route("accounts/sign-up", "routes/redirects/core/accounts-signup.tsx"), + + // Sign-in redirects (all redirect to home page) + route("sign-in", "routes/redirects/core/sign-in.tsx"), + route("signin", "routes/redirects/core/signin.tsx"), + route("login", "routes/redirects/core/login.tsx"), + + // Register redirect + route("register", "routes/redirects/core/register.tsx"), +] satisfies RouteConfig; diff --git a/apps/web/app/routes/extended.ts b/apps/web/app/routes/extended.ts new file mode 100644 index 000000000..bbc5aa4cc --- /dev/null +++ b/apps/web/app/routes/extended.ts @@ -0,0 +1,3 @@ +import type { RouteConfigEntry } from "@react-router/dev/routes"; + +export const extendedRoutes: RouteConfigEntry[] = []; diff --git a/apps/web/app/routes/redirects/core/accounts-signup.tsx b/apps/web/app/routes/redirects/core/accounts-signup.tsx new file mode 100644 index 000000000..5343e27be --- /dev/null +++ b/apps/web/app/routes/redirects/core/accounts-signup.tsx @@ -0,0 +1,9 @@ +import { redirect } from "react-router"; + +export const clientLoader = () => { + throw redirect("/sign-up/"); +}; + +export default function AccountsSignup() { + return null; +} diff --git a/apps/web/app/routes/redirects/core/analytics.tsx b/apps/web/app/routes/redirects/core/analytics.tsx new file mode 100644 index 000000000..21bacf509 --- /dev/null +++ b/apps/web/app/routes/redirects/core/analytics.tsx @@ -0,0 +1,11 @@ +import { redirect } from "react-router"; +import type { Route } from "./+types/analytics"; + +export const clientLoader = ({ params }: Route.ClientLoaderArgs) => { + const { workspaceSlug } = params; + throw redirect(`/${workspaceSlug}/analytics/overview/`); +}; + +export default function Analytics() { + return null; +} diff --git a/apps/web/app/routes/redirects/core/api-tokens.tsx b/apps/web/app/routes/redirects/core/api-tokens.tsx new file mode 100644 index 000000000..68007aa41 --- /dev/null +++ b/apps/web/app/routes/redirects/core/api-tokens.tsx @@ -0,0 +1,11 @@ +import { redirect } from "react-router"; +import type { Route } from "./+types/api-tokens"; + +export const clientLoader = ({ params }: Route.ClientLoaderArgs) => { + const { workspaceSlug } = params; + throw redirect(`/${workspaceSlug}/settings/account/api-tokens/`); +}; + +export default function ApiTokens() { + return null; +} diff --git a/apps/web/app/routes/redirects/core/inbox.tsx b/apps/web/app/routes/redirects/core/inbox.tsx new file mode 100644 index 000000000..bf2bd9a50 --- /dev/null +++ b/apps/web/app/routes/redirects/core/inbox.tsx @@ -0,0 +1,11 @@ +import { redirect } from "react-router"; +import type { Route } from "./+types/inbox"; + +export const clientLoader = ({ params }: Route.ClientLoaderArgs) => { + const { workspaceSlug, projectId } = params; + throw redirect(`/${workspaceSlug}/projects/${projectId}/intake/`); +}; + +export default function Inbox() { + return null; +} diff --git a/apps/web/app/routes/redirects/core/index.ts b/apps/web/app/routes/redirects/core/index.ts new file mode 100644 index 000000000..efd3ae40f --- /dev/null +++ b/apps/web/app/routes/redirects/core/index.ts @@ -0,0 +1,38 @@ +import { route } from "@react-router/dev/routes"; +import type { RouteConfigEntry } from "@react-router/dev/routes"; + +export const coreRedirectRoutes: RouteConfigEntry[] = [ + // ======================================================================== + // WORKSPACE & PROJECT REDIRECTS + // ======================================================================== + + // Project settings redirect: /:workspaceSlug/projects/:projectId/settings/:path* + // → /:workspaceSlug/settings/projects/:projectId/:path* + route(":workspaceSlug/projects/:projectId/settings/*", "routes/redirects/core/project-settings.tsx"), + + // Analytics redirect: /:workspaceSlug/analytics → /:workspaceSlug/analytics/overview + route(":workspaceSlug/analytics", "routes/redirects/core/analytics.tsx"), + + // API tokens redirect: /:workspaceSlug/settings/api-tokens + // → /:workspaceSlug/settings/account/api-tokens + route(":workspaceSlug/settings/api-tokens", "routes/redirects/core/api-tokens.tsx"), + + // Inbox redirect: /:workspaceSlug/projects/:projectId/inbox + // → /:workspaceSlug/projects/:projectId/intake + route(":workspaceSlug/projects/:projectId/inbox", "routes/redirects/core/inbox.tsx"), + + // ======================================================================== + // AUTHENTICATION REDIRECTS + // ======================================================================== + + // Sign-up redirects + route("accounts/sign-up", "routes/redirects/core/accounts-signup.tsx"), + + // Sign-in redirects (all redirect to home page) + route("sign-in", "routes/redirects/core/sign-in.tsx"), + route("signin", "routes/redirects/core/signin.tsx"), + route("login", "routes/redirects/core/login.tsx"), + + // Register redirect + route("register", "routes/redirects/core/register.tsx"), +]; diff --git a/apps/web/app/routes/redirects/core/login.tsx b/apps/web/app/routes/redirects/core/login.tsx new file mode 100644 index 000000000..ed49c8ca3 --- /dev/null +++ b/apps/web/app/routes/redirects/core/login.tsx @@ -0,0 +1,9 @@ +import { redirect } from "react-router"; + +export const clientLoader = () => { + throw redirect("/"); +}; + +export default function Login() { + return null; +} diff --git a/apps/web/app/routes/redirects/core/project-settings.tsx b/apps/web/app/routes/redirects/core/project-settings.tsx new file mode 100644 index 000000000..da14c515a --- /dev/null +++ b/apps/web/app/routes/redirects/core/project-settings.tsx @@ -0,0 +1,13 @@ +import { redirect } from "react-router"; +import type { Route } from "./+types/project-settings"; + +export const clientLoader = ({ params }: Route.ClientLoaderArgs) => { + const { workspaceSlug, projectId } = params; + const splat = params["*"] || ""; + const destination = `/${workspaceSlug}/settings/projects/${projectId}${splat ? `/${splat}` : ""}/`; + throw redirect(destination); +}; + +export default function ProjectSettings() { + return null; +} diff --git a/apps/web/app/routes/redirects/core/register.tsx b/apps/web/app/routes/redirects/core/register.tsx new file mode 100644 index 000000000..791040495 --- /dev/null +++ b/apps/web/app/routes/redirects/core/register.tsx @@ -0,0 +1,9 @@ +import { redirect } from "react-router"; + +export const clientLoader = () => { + throw redirect("/sign-up/"); +}; + +export default function Register() { + return null; +} diff --git a/apps/web/app/routes/redirects/core/sign-in.tsx b/apps/web/app/routes/redirects/core/sign-in.tsx new file mode 100644 index 000000000..83a91a3eb --- /dev/null +++ b/apps/web/app/routes/redirects/core/sign-in.tsx @@ -0,0 +1,9 @@ +import { redirect } from "react-router"; + +export const clientLoader = () => { + throw redirect("/"); +}; + +export default function SignIn() { + return null; +} diff --git a/apps/web/app/routes/redirects/core/signin.tsx b/apps/web/app/routes/redirects/core/signin.tsx new file mode 100644 index 000000000..e440e8399 --- /dev/null +++ b/apps/web/app/routes/redirects/core/signin.tsx @@ -0,0 +1,9 @@ +import { redirect } from "react-router"; + +export const clientLoader = () => { + throw redirect("/"); +}; + +export default function Signin() { + return null; +} diff --git a/apps/web/app/routes/redirects/extended/index.ts b/apps/web/app/routes/redirects/extended/index.ts new file mode 100644 index 000000000..7f2c496e1 --- /dev/null +++ b/apps/web/app/routes/redirects/extended/index.ts @@ -0,0 +1,3 @@ +import type { RouteConfigEntry } from "@react-router/dev/routes"; + +export const extendedRedirectRoutes: RouteConfigEntry[] = []; diff --git a/apps/web/app/routes/redirects/index.ts b/apps/web/app/routes/redirects/index.ts new file mode 100644 index 000000000..4c78ea354 --- /dev/null +++ b/apps/web/app/routes/redirects/index.ts @@ -0,0 +1,10 @@ +import type { RouteConfigEntry } from "@react-router/dev/routes"; +import { coreRedirectRoutes } from "./core"; +import { extendedRedirectRoutes } from "./extended"; + +/** + * REDIRECT ROUTES + * Centralized configuration for all route redirects + * Migrated from Next.js next.config.js redirects + */ +export const redirectRoutes: RouteConfigEntry[] = [...coreRedirectRoutes, ...extendedRedirectRoutes]; diff --git a/apps/web/app/types/next-link.d.ts b/apps/web/app/types/next-link.d.ts new file mode 100644 index 000000000..c724e3aec --- /dev/null +++ b/apps/web/app/types/next-link.d.ts @@ -0,0 +1,12 @@ +declare module "next/link" { + type Props = React.ComponentProps<"a"> & { + href: string; + replace?: boolean; + prefetch?: boolean; + scroll?: boolean; + shallow?: boolean; + }; + + const Link: React.FC; + export default Link; +} diff --git a/apps/web/app/types/next-navigation.d.ts b/apps/web/app/types/next-navigation.d.ts new file mode 100644 index 000000000..67a80c4fa --- /dev/null +++ b/apps/web/app/types/next-navigation.d.ts @@ -0,0 +1,14 @@ +declare module "next/navigation" { + export function useRouter(): { + push: (url: string) => void; + replace: (url: string) => void; + back: () => void; + forward: () => void; + refresh: () => void; + prefetch: (url: string) => Promise; + }; + + export function usePathname(): string; + export function useSearchParams(): URLSearchParams; + export function useParams>(): T; +} diff --git a/apps/web/app/types/next-script.d.ts b/apps/web/app/types/next-script.d.ts new file mode 100644 index 000000000..299b5ed4c --- /dev/null +++ b/apps/web/app/types/next-script.d.ts @@ -0,0 +1,15 @@ +declare module "next/script" { + type ScriptProps = { + src?: string; + id?: string; + strategy?: "beforeInteractive" | "afterInteractive" | "lazyOnload" | "worker"; + onLoad?: () => void; + onError?: () => void; + children?: string; + defer?: boolean; + [key: string]: any; + }; + + const Script: React.FC; + export default Script; +} diff --git a/apps/web/app/types/react-router-virtual.d.ts b/apps/web/app/types/react-router-virtual.d.ts new file mode 100644 index 000000000..abf3b638e --- /dev/null +++ b/apps/web/app/types/react-router-virtual.d.ts @@ -0,0 +1,5 @@ +declare module "virtual:react-router/server-build" { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const build: any; + export default build; +} diff --git a/apps/web/ce/components/active-cycles/root.tsx b/apps/web/ce/components/active-cycles/root.tsx index caad61a0a..ae0aa9b1f 100644 --- a/apps/web/ce/components/active-cycles/root.tsx +++ b/apps/web/ce/components/active-cycles/root.tsx @@ -1,4 +1,6 @@ // local imports import { WorkspaceActiveCyclesUpgrade } from "./workspace-active-cycles-upgrade"; -export const WorkspaceActiveCyclesRoot = () => ; +export function WorkspaceActiveCyclesRoot() { + return ; +} diff --git a/apps/web/ce/components/active-cycles/workspace-active-cycles-upgrade.tsx b/apps/web/ce/components/active-cycles/workspace-active-cycles-upgrade.tsx index 1ce060b01..316245fd3 100644 --- a/apps/web/ce/components/active-cycles/workspace-active-cycles-upgrade.tsx +++ b/apps/web/ce/components/active-cycles/workspace-active-cycles-upgrade.tsx @@ -1,8 +1,4 @@ -"use client"; - -import React from "react"; import { observer } from "mobx-react"; -import Image from "next/image"; import { AlertOctagon, BarChart4, CircleDashed, Folder, Microscope, Search } from "lucide-react"; // plane imports import { MARKETING_PRICING_PAGE_LINK } from "@plane/constants"; @@ -10,6 +6,13 @@ import { useTranslation } from "@plane/i18n"; import { getButtonStyling } from "@plane/propel/button"; import { ContentWrapper } from "@plane/ui"; import { cn } from "@plane/utils"; +// assets +import ctaL1Dark from "@/app/assets/workspace-active-cycles/cta-l-1-dark.webp?url"; +import ctaL1Light from "@/app/assets/workspace-active-cycles/cta-l-1-light.webp?url"; +import ctaR1Dark from "@/app/assets/workspace-active-cycles/cta-r-1-dark.webp?url"; +import ctaR1Light from "@/app/assets/workspace-active-cycles/cta-r-1-light.webp?url"; +import ctaR2Dark from "@/app/assets/workspace-active-cycles/cta-r-2-dark.webp?url"; +import ctaR2Light from "@/app/assets/workspace-active-cycles/cta-r-2-light.webp?url"; // components import { ProIcon } from "@/components/common/pro-icon"; // hooks @@ -58,7 +61,7 @@ export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [ }, ]; -export const WorkspaceActiveCyclesUpgrade = observer(() => { +export const WorkspaceActiveCyclesUpgrade = observer(function WorkspaceActiveCyclesUpgrade() { const { t } = useTranslation(); // store hooks const { @@ -92,31 +95,19 @@ export const WorkspaceActiveCyclesUpgrade = observer(() => {
- l-1
- r-1 + r-1 - r-2 + r-2
diff --git a/apps/web/ce/components/analytics/use-analytics-tabs.tsx b/apps/web/ce/components/analytics/use-analytics-tabs.tsx new file mode 100644 index 000000000..71f1ab15f --- /dev/null +++ b/apps/web/ce/components/analytics/use-analytics-tabs.tsx @@ -0,0 +1,11 @@ +import { useMemo } from "react"; +import { useTranslation } from "@plane/i18n"; +import { getAnalyticsTabs } from "./tabs"; + +export const useAnalyticsTabs = (workspaceSlug: string) => { + const { t } = useTranslation(); + + const analyticsTabs = useMemo(() => getAnalyticsTabs(t), [t]); + + return analyticsTabs; +}; diff --git a/apps/web/ce/components/app-rail/app-rail-hoc.tsx b/apps/web/ce/components/app-rail/app-rail-hoc.tsx new file mode 100644 index 000000000..63d13e5c2 --- /dev/null +++ b/apps/web/ce/components/app-rail/app-rail-hoc.tsx @@ -0,0 +1,32 @@ +// hoc/withDockItems.tsx +import React from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { PlaneNewIcon } from "@plane/propel/icons"; +import type { AppSidebarItemData } from "@/components/sidebar/sidebar-item"; +import { useWorkspacePaths } from "@/hooks/use-workspace-paths"; + +type WithDockItemsProps = { + dockItems: (AppSidebarItemData & { shouldRender: boolean })[]; +}; + +export function withDockItems

(WrappedComponent: React.ComponentType

) { + const ComponentWithDockItems = observer(function ComponentWithDockItems(props: Omit) { + const { workspaceSlug } = useParams(); + const { isProjectsPath, isNotificationsPath } = useWorkspacePaths(); + + const dockItems: (AppSidebarItemData & { shouldRender: boolean })[] = [ + { + label: "Projects", + icon: , + href: `/${workspaceSlug}/`, + isActive: isProjectsPath && !isNotificationsPath, + shouldRender: true, + }, + ]; + + return ; + }); + + return ComponentWithDockItems; +} diff --git a/apps/web/ce/components/app-rail/index.ts b/apps/web/ce/components/app-rail/index.ts index 1efe34c51..c29a9bf13 100644 --- a/apps/web/ce/components/app-rail/index.ts +++ b/apps/web/ce/components/app-rail/index.ts @@ -1 +1 @@ -export * from "./root"; +export * from "./app-rail-hoc"; diff --git a/apps/web/ce/components/app-rail/root.tsx b/apps/web/ce/components/app-rail/root.tsx deleted file mode 100644 index 259764b26..000000000 --- a/apps/web/ce/components/app-rail/root.tsx +++ /dev/null @@ -1,4 +0,0 @@ -"use client"; -import React from "react"; - -export const AppRailRoot = () => <>; diff --git a/apps/web/ce/components/automations/list/wrapper.tsx b/apps/web/ce/components/automations/list/wrapper.tsx new file mode 100644 index 000000000..80b819aa0 --- /dev/null +++ b/apps/web/ce/components/automations/list/wrapper.tsx @@ -0,0 +1,9 @@ +type Props = { + projectId: string; + workspaceSlug: string; + children: React.ReactNode; +}; + +export function AutomationsListWrapper(props: Props) { + return <>{props.children}; +} diff --git a/apps/web/ce/components/automations/root.tsx b/apps/web/ce/components/automations/root.tsx index e7f15288b..9dbb44942 100644 --- a/apps/web/ce/components/automations/root.tsx +++ b/apps/web/ce/components/automations/root.tsx @@ -1,5 +1,3 @@ -"use client"; - import type { FC } from "react"; import React from "react"; @@ -8,4 +6,6 @@ export type TCustomAutomationsRootProps = { workspaceSlug: string; }; -export const CustomAutomationsRoot: FC = () => <>; +export function CustomAutomationsRoot(_props: TCustomAutomationsRootProps) { + return <>; +} diff --git a/apps/web/ce/components/breadcrumbs/common.tsx b/apps/web/ce/components/breadcrumbs/common.tsx index 86a123915..397de3db5 100644 --- a/apps/web/ce/components/breadcrumbs/common.tsx +++ b/apps/web/ce/components/breadcrumbs/common.tsx @@ -1,32 +1,17 @@ -"use client"; - -import type { FC } from "react"; -// plane imports -import type { EProjectFeatureKey } from "@plane/constants"; // local components +import { useProjectNavigationPreferences } from "@/hooks/use-navigation-preferences"; import { ProjectBreadcrumb } from "./project"; -import { ProjectFeatureBreadcrumb } from "./project-feature"; type TCommonProjectBreadcrumbProps = { workspaceSlug: string; projectId: string; - featureKey?: EProjectFeatureKey; - isLast?: boolean; }; -export const CommonProjectBreadcrumbs: FC = (props) => { - const { workspaceSlug, projectId, featureKey, isLast = false } = props; - return ( - <> - - {featureKey && ( - - )} - - ); -}; +export function CommonProjectBreadcrumbs(props: TCommonProjectBreadcrumbProps) { + const { workspaceSlug, projectId } = props; + // preferences + const { preferences: projectPreferences } = useProjectNavigationPreferences(); + + if (projectPreferences.navigationMode === "horizontal") return null; + return ; +} diff --git a/apps/web/ce/components/breadcrumbs/project-feature.tsx b/apps/web/ce/components/breadcrumbs/project-feature.tsx index cad4338d3..51d26d8ea 100644 --- a/apps/web/ce/components/breadcrumbs/project-feature.tsx +++ b/apps/web/ce/components/breadcrumbs/project-feature.tsx @@ -1,17 +1,13 @@ -"use client"; - -import type { FC } from "react"; +import type { ReactNode } from "react"; import { observer } from "mobx-react"; // plane imports -import { EProjectFeatureKey } from "@plane/constants"; -import type { ISvgIcons } from "@plane/propel/icons"; -import { BreadcrumbNavigationDropdown, Breadcrumbs } from "@plane/ui"; +import type { EProjectFeatureKey } from "@plane/constants"; +import { Breadcrumbs } from "@plane/ui"; // components -import { SwitcherLabel } from "@/components/common/switcher-label"; +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; import type { TNavigationItem } from "@/components/workspace/sidebar/project-navigation"; // hooks import { useProject } from "@/hooks/store/use-project"; -import { useAppRouter } from "@/hooks/use-app-router"; // local imports import { getProjectFeatureNavigation } from "../projects/navigation/helper"; @@ -23,10 +19,10 @@ type TProjectFeatureBreadcrumbProps = { additionalNavigationItems?: TNavigationItem[]; }; -export const ProjectFeatureBreadcrumb = observer((props: TProjectFeatureBreadcrumbProps) => { +export const ProjectFeatureBreadcrumb = observer(function ProjectFeatureBreadcrumb( + props: TProjectFeatureBreadcrumbProps +) { const { workspaceSlug, projectId, featureKey, isLast = false, additionalNavigationItems } = props; - // router - const router = useAppRouter(); // store hooks const { getPartialProjectById } = useProject(); // derived values @@ -39,27 +35,21 @@ export const ProjectFeatureBreadcrumb = observer((props: TProjectFeatureBreadcru // if additional navigation items are provided, add them to the navigation items const allNavigationItems = [...(additionalNavigationItems || []), ...navigationItems]; + const currentNavigationItem = allNavigationItems.find((item) => item.key === featureKey); + const icon = currentNavigationItem?.icon as ReactNode; + const name = currentNavigationItem?.name; + const href = currentNavigationItem?.href; + return ( <> item.shouldRender) - .map((item) => ({ - key: item.key, - title: item.name, - customContent: } />, - action: () => router.push(item.href), - icon: item.icon as FC, - }))} - handleOnClick={() => { - router.push( - `/${workspaceSlug}/projects/${projectId}/${featureKey === EProjectFeatureKey.WORK_ITEMS ? "issues" : featureKey}/` - ); - }} + {icon}} /> } showSeparator={false} diff --git a/apps/web/ce/components/breadcrumbs/project.tsx b/apps/web/ce/components/breadcrumbs/project.tsx index 2f6c67bd7..fc17a5eb1 100644 --- a/apps/web/ce/components/breadcrumbs/project.tsx +++ b/apps/web/ce/components/breadcrumbs/project.tsx @@ -1,12 +1,9 @@ -"use client"; - import { observer } from "mobx-react"; +import { Logo } from "@plane/propel/emoji-icon-picker"; import { ProjectIcon } from "@plane/propel/icons"; // plane imports import type { ICustomSearchSelectOption } from "@plane/types"; import { BreadcrumbNavigationSearchDropdown, Breadcrumbs } from "@plane/ui"; -// components -import { Logo } from "@/components/common/logo"; import { SwitcherLabel } from "@/components/common/switcher-label"; // hooks import { useProject } from "@/hooks/store/use-project"; @@ -19,7 +16,7 @@ type TProjectBreadcrumbProps = { handleOnClick?: () => void; }; -export const ProjectBreadcrumb = observer((props: TProjectBreadcrumbProps) => { +export const ProjectBreadcrumb = observer(function ProjectBreadcrumb(props: TProjectBreadcrumbProps) { const { workspaceSlug, projectId, handleOnClick } = props; // router const router = useAppRouter(); @@ -52,7 +49,7 @@ export const ProjectBreadcrumb = observer((props: TProjectBreadcrumbProps) => { // helpers const renderIcon = (projectDetails: TProject) => ( - + ); diff --git a/apps/web/ce/components/command-palette/actions/work-item-actions/change-state-list.tsx b/apps/web/ce/components/command-palette/actions/work-item-actions/change-state-list.tsx index fb3595d56..091aaea3f 100644 --- a/apps/web/ce/components/command-palette/actions/work-item-actions/change-state-list.tsx +++ b/apps/web/ce/components/command-palette/actions/work-item-actions/change-state-list.tsx @@ -14,7 +14,7 @@ export type TChangeWorkItemStateListProps = { handleStateChange: (stateId: string) => void; }; -export const ChangeWorkItemStateList = observer((props: TChangeWorkItemStateListProps) => { +export const ChangeWorkItemStateList = observer(function ChangeWorkItemStateList(props: TChangeWorkItemStateListProps) { const { projectId, currentStateId, handleStateChange } = props; // store hooks const { getProjectStates } = useProjectState(); diff --git a/apps/web/ce/components/command-palette/helpers.tsx b/apps/web/ce/components/command-palette/helpers.tsx index 865aa9e53..fee3166e8 100644 --- a/apps/web/ce/components/command-palette/helpers.tsx +++ b/apps/web/ce/components/command-palette/helpers.tsx @@ -1,5 +1,3 @@ -"use client"; - import { LayoutGrid } from "lucide-react"; // plane imports import { CycleIcon, ModuleIcon, PageIcon, ProjectIcon, ViewsIcon } from "@plane/propel/icons"; @@ -93,7 +91,7 @@ export const commandGroups: TCommandGroups = { if (!!projectId && page?.project_ids?.includes(projectId)) redirectProjectId = projectId; return redirectProjectId ? `/${page?.workspace__slug}/projects/${redirectProjectId}/pages/${page?.id}` - : `/${page?.workspace__slug}/pages/${page?.id}`; + : `/${page?.workspace__slug}/wiki/${page?.id}`; }, title: "Pages", }, diff --git a/apps/web/ce/components/command-palette/index.ts b/apps/web/ce/components/command-palette/index.ts index 62404249d..cb220b2bd 100644 --- a/apps/web/ce/components/command-palette/index.ts +++ b/apps/web/ce/components/command-palette/index.ts @@ -1,3 +1,2 @@ export * from "./actions"; -export * from "./modals"; export * from "./helpers"; diff --git a/apps/web/ce/components/command-palette/modals/index.ts b/apps/web/ce/components/command-palette/modals/index.ts deleted file mode 100644 index a4fac4b91..000000000 --- a/apps/web/ce/components/command-palette/modals/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./workspace-level"; -export * from "./project-level"; -export * from "./issue-level"; diff --git a/apps/web/ce/components/command-palette/modals/project-level.tsx b/apps/web/ce/components/command-palette/modals/project-level.tsx index 6b9e8000d..baf430f55 100644 --- a/apps/web/ce/components/command-palette/modals/project-level.tsx +++ b/apps/web/ce/components/command-palette/modals/project-level.tsx @@ -14,7 +14,7 @@ export type TProjectLevelModalsProps = { projectId: string; }; -export const ProjectLevelModals = observer((props: TProjectLevelModalsProps) => { +export const ProjectLevelModals = observer(function ProjectLevelModals(props: TProjectLevelModalsProps) { const { workspaceSlug, projectId } = props; // store hooks const { diff --git a/apps/web/ce/components/command-palette/modals/issue-level.tsx b/apps/web/ce/components/command-palette/modals/work-item-level.tsx similarity index 75% rename from apps/web/ce/components/command-palette/modals/issue-level.tsx rename to apps/web/ce/components/command-palette/modals/work-item-level.tsx index f720e38ea..8f71596c7 100644 --- a/apps/web/ce/components/command-palette/modals/issue-level.tsx +++ b/apps/web/ce/components/command-palette/modals/work-item-level.tsx @@ -15,21 +15,23 @@ import { useUser } from "@/hooks/store/user"; import { useAppRouter } from "@/hooks/use-app-router"; import { useIssuesActions } from "@/hooks/use-issues-actions"; -export type TIssueLevelModalsProps = { - projectId: string | undefined; - issueId: string | undefined; +export type TWorkItemLevelModalsProps = { + workItemIdentifier: string | undefined; }; -export const IssueLevelModals: FC = observer((props) => { - const { projectId, issueId } = props; +export const WorkItemLevelModals = observer(function WorkItemLevelModals(props: TWorkItemLevelModalsProps) { + const { workItemIdentifier } = props; // router const { workspaceSlug, cycleId, moduleId } = useParams(); const router = useAppRouter(); // store hooks const { data: currentUser } = useUser(); const { - issue: { getIssueById }, + issue: { getIssueById, getIssueIdByIdentifier }, } = useIssueDetail(); + // derived values + const workItemId = workItemIdentifier ? getIssueIdByIdentifier(workItemIdentifier) : undefined; + const workItemDetails = workItemId ? getIssueById(workItemId) : undefined; const { removeIssue: removeEpic } = useIssuesActions(EIssuesStoreType.EPIC); const { removeIssue: removeWorkItem } = useIssuesActions(EIssuesStoreType.PROJECT); @@ -44,13 +46,12 @@ export const IssueLevelModals: FC = observer((props) => createWorkItemAllowedProjectIds, } = useCommandPalette(); // derived values - const issueDetails = issueId ? getIssueById(issueId) : undefined; const { fetchSubIssues: fetchSubWorkItems } = useIssueDetail(); const { fetchSubIssues: fetchEpicSubWorkItems } = useIssueDetail(EIssueServiceType.EPICS); const handleDeleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { try { - const isEpic = issueDetails?.is_epic; + const isEpic = workItemDetails?.is_epic; const deleteAction = isEpic ? removeEpic : removeWorkItem; const redirectPath = `/${workspaceSlug}/projects/${projectId}/${isEpic ? "epics" : "issues"}`; @@ -62,10 +63,10 @@ export const IssueLevelModals: FC = observer((props) => }; const handleCreateIssueSubmit = async (newIssue: TIssue) => { - if (!workspaceSlug || !newIssue.project_id || !newIssue.id || newIssue.parent_id !== issueDetails?.id) return; + if (!workspaceSlug || !newIssue.project_id || !newIssue.id || newIssue.parent_id !== workItemDetails?.id) return; - const fetchAction = issueDetails?.is_epic ? fetchEpicSubWorkItems : fetchSubWorkItems; - await fetchAction(workspaceSlug?.toString(), newIssue.project_id, issueDetails.id); + const fetchAction = workItemDetails?.is_epic ? fetchEpicSubWorkItems : fetchSubWorkItems; + await fetchAction(workspaceSlug?.toString(), newIssue.project_id, workItemDetails.id); }; const getCreateIssueModalData = () => { @@ -83,13 +84,15 @@ export const IssueLevelModals: FC = observer((props) => onSubmit={handleCreateIssueSubmit} allowedProjectIds={createWorkItemAllowedProjectIds} /> - {workspaceSlug && projectId && issueId && issueDetails && ( + {workspaceSlug && workItemId && workItemDetails && workItemDetails.project_id && ( toggleDeleteIssueModal(false)} isOpen={isDeleteIssueModalOpen} - data={issueDetails} - onSubmit={() => handleDeleteIssue(workspaceSlug.toString(), projectId?.toString(), issueId?.toString())} - isEpic={issueDetails?.is_epic} + data={workItemDetails} + onSubmit={() => + handleDeleteIssue(workspaceSlug.toString(), workItemDetails.project_id!, workItemId?.toString()) + } + isEpic={workItemDetails?.is_epic} /> )} { +export const WorkspaceLevelModals = observer(function WorkspaceLevelModals(props: TWorkspaceLevelModalsProps) { const { workspaceSlug } = props; // store hooks const { isCreateProjectModalOpen, toggleCreateProjectModal } = useCommandPalette(); diff --git a/apps/web/ce/components/command-palette/power-k/constants.ts b/apps/web/ce/components/command-palette/power-k/constants.ts new file mode 100644 index 000000000..3b2a051c2 --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/constants.ts @@ -0,0 +1,6 @@ +// core +import type { TPowerKModalPageDetails } from "@/components/power-k/ui/modal/constants"; +// local imports +import type { TPowerKPageTypeExtended } from "./types"; + +export const POWER_K_MODAL_PAGE_DETAILS_EXTENDED: Record = {}; diff --git a/apps/web/ce/components/command-palette/power-k/context-detector.ts b/apps/web/ce/components/command-palette/power-k/context-detector.ts new file mode 100644 index 000000000..e84ca20b8 --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/context-detector.ts @@ -0,0 +1,5 @@ +import type { Params } from "react-router"; +// local imports +import type { TPowerKContextTypeExtended } from "./types"; + +export const detectExtendedContextFromURL = (_params: Params): TPowerKContextTypeExtended | null => null; diff --git a/apps/web/ce/components/command-palette/power-k/hooks/use-extended-context-indicator.ts b/apps/web/ce/components/command-palette/power-k/hooks/use-extended-context-indicator.ts new file mode 100644 index 000000000..e92e54e2e --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/hooks/use-extended-context-indicator.ts @@ -0,0 +1,8 @@ +// local imports +import type { TPowerKContextType } from "@/components/power-k/core/types"; + +type TArgs = { + activeContext: TPowerKContextType | null; +}; + +export const useExtendedContextIndicator = (_args: TArgs): string | null => null; diff --git a/apps/web/core/components/onboarding/tour/index.ts b/apps/web/ce/components/command-palette/power-k/pages/context-based/index.ts similarity index 100% rename from apps/web/core/components/onboarding/tour/index.ts rename to apps/web/ce/components/command-palette/power-k/pages/context-based/index.ts diff --git a/apps/web/ce/components/command-palette/power-k/pages/context-based/root.tsx b/apps/web/ce/components/command-palette/power-k/pages/context-based/root.tsx new file mode 100644 index 000000000..12833d9df --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/pages/context-based/root.tsx @@ -0,0 +1,13 @@ +// components +import type { TPowerKCommandConfig } from "@/components/power-k/core/types"; +import type { ContextBasedActionsProps, TContextEntityMap } from "@/components/power-k/ui/pages/context-based"; +// local imports +import type { TPowerKContextTypeExtended } from "../../types"; + +export const CONTEXT_ENTITY_MAP_EXTENDED: Record = {}; + +export function PowerKContextBasedActionsExtended(_props: ContextBasedActionsProps) { + return null; +} + +export const usePowerKContextBasedExtendedActions = (): TPowerKCommandConfig[] => []; diff --git a/apps/web/ce/components/command-palette/power-k/pages/context-based/work-item/state-menu-item.tsx b/apps/web/ce/components/command-palette/power-k/pages/context-based/work-item/state-menu-item.tsx new file mode 100644 index 000000000..2478fdf4f --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/pages/context-based/work-item/state-menu-item.tsx @@ -0,0 +1,34 @@ +import { observer } from "mobx-react"; +// plane types +import { StateGroupIcon } from "@plane/propel/icons"; +import type { IState } from "@plane/types"; +// components +import { PowerKModalCommandItem } from "@/components/power-k/ui/modal/command-item"; + +export type TPowerKProjectStatesMenuItemsProps = { + handleSelect: (stateId: string) => void; + projectId: string | undefined; + selectedStateId: string | undefined; + states: IState[]; + workspaceSlug: string; +}; + +export const PowerKProjectStatesMenuItems = observer(function PowerKProjectStatesMenuItems( + props: TPowerKProjectStatesMenuItemsProps +) { + const { handleSelect, selectedStateId, states } = props; + + return ( + <> + {states.map((state) => ( + } + label={state.name} + isSelected={state.id === selectedStateId} + onSelect={() => handleSelect(state.id)} + /> + ))} + + ); +}); diff --git a/apps/web/ce/components/command-palette/power-k/search/no-results-command.tsx b/apps/web/ce/components/command-palette/power-k/search/no-results-command.tsx new file mode 100644 index 000000000..789facdfe --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/search/no-results-command.tsx @@ -0,0 +1,36 @@ +import { Command } from "cmdk"; +import { Search } from "lucide-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// components +import type { TPowerKContext } from "@/components/power-k/core/types"; +// plane web imports +import { PowerKModalCommandItem } from "@/components/power-k/ui/modal/command-item"; + +export type TPowerKModalNoSearchResultsCommandProps = { + context: TPowerKContext; + searchTerm: string; + updateSearchTerm: (value: string) => void; +}; + +export function PowerKModalNoSearchResultsCommand(props: TPowerKModalNoSearchResultsCommandProps) { + const { updateSearchTerm } = props; + // translation + const { t } = useTranslation(); + + return ( + + + {t("power_k.search_menu.no_results")}{" "} + {t("power_k.search_menu.clear_search")} +

+ } + onSelect={() => updateSearchTerm("")} + /> + + ); +} diff --git a/apps/web/ce/components/command-palette/power-k/search/search-results-map.tsx b/apps/web/ce/components/command-palette/power-k/search/search-results-map.tsx new file mode 100644 index 000000000..90d5b5f20 --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/search/search-results-map.tsx @@ -0,0 +1,8 @@ +// components +import type { TPowerKSearchResultGroupDetails } from "@/components/power-k/ui/modal/search-results-map"; +// local imports +import type { TPowerKSearchResultsKeysExtended } from "../types"; + +type TSearchResultsGroupsMapExtended = Record; + +export const SEARCH_RESULTS_GROUPS_MAP_EXTENDED: TSearchResultsGroupsMapExtended = {}; diff --git a/apps/web/ce/components/command-palette/power-k/types.ts b/apps/web/ce/components/command-palette/power-k/types.ts new file mode 100644 index 000000000..4e497f8b8 --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/types.ts @@ -0,0 +1,5 @@ +export type TPowerKContextTypeExtended = never; + +export type TPowerKPageTypeExtended = never; + +export type TPowerKSearchResultsKeysExtended = never; diff --git a/apps/web/ce/components/comments/comment-block.tsx b/apps/web/ce/components/comments/comment-block.tsx index c5c9b442a..becc8acb4 100644 --- a/apps/web/ce/components/comments/comment-block.tsx +++ b/apps/web/ce/components/comments/comment-block.tsx @@ -1,10 +1,9 @@ -import type { FC, ReactNode } from "react"; +import type { ReactNode } from "react"; import { useRef } from "react"; import { observer } from "mobx-react"; // plane imports import { useTranslation } from "@plane/i18n"; import type { TIssueComment } from "@plane/types"; -import { EIssueCommentAccessSpecifier } from "@plane/types"; import { Avatar, Tooltip } from "@plane/ui"; import { calculateTimeAgo, cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils"; // hooks @@ -17,7 +16,7 @@ type TCommentBlock = { children: ReactNode; }; -export const CommentBlock: FC = observer((props) => { +export const CommentBlock = observer(function CommentBlock(props: TCommentBlock) { const { comment, ends, quickActions, children } = props; // refs const commentBlockRef = useRef(null); @@ -56,9 +55,7 @@ export const CommentBlock: FC = observer((props) => {
- - {`${displayName}${comment.access === EIssueCommentAccessSpecifier.EXTERNAL ? " (External User)" : ""}`} - + {displayName}
commented{" "} @@ -67,7 +64,7 @@ export const CommentBlock: FC = observer((props) => { position="bottom" > - {calculateTimeAgo(comment.updated_at)} + {calculateTimeAgo(comment.created_at)} {comment.edited_at && ` (${t("edited")})`} diff --git a/apps/web/ce/components/common/extended-app-header.tsx b/apps/web/ce/components/common/extended-app-header.tsx index 59dbf3394..f8661d5c2 100644 --- a/apps/web/ce/components/common/extended-app-header.tsx +++ b/apps/web/ce/components/common/extended-app-header.tsx @@ -1,16 +1,26 @@ import type { ReactNode } from "react"; import { observer } from "mobx-react"; +import { useParams } from "react-router"; +// components import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-button"; +// hooks import { useAppTheme } from "@/hooks/store/use-app-theme"; +import { useProjectNavigationPreferences } from "@/hooks/use-navigation-preferences"; -export const ExtendedAppHeader = observer((props: { header: ReactNode }) => { +export const ExtendedAppHeader = observer(function ExtendedAppHeader(props: { header: ReactNode }) { const { header } = props; + // params + const { projectId, workItem } = useParams(); + // preferences + const { preferences: projectPreferences } = useProjectNavigationPreferences(); // store hooks const { sidebarCollapsed } = useAppTheme(); + // derived values + const shouldShowSidebarToggleButton = projectPreferences.navigationMode === "accordion" || (!projectId && !workItem); return ( <> - {sidebarCollapsed && } + {sidebarCollapsed && shouldShowSidebarToggleButton && }
{header}
); diff --git a/apps/web/ce/components/common/subscription/subscription-pill.tsx b/apps/web/ce/components/common/subscription/subscription-pill.tsx index ba30d3ad6..e03ecf8d7 100644 --- a/apps/web/ce/components/common/subscription/subscription-pill.tsx +++ b/apps/web/ce/components/common/subscription/subscription-pill.tsx @@ -4,4 +4,6 @@ type TProps = { workspace?: IWorkspace; }; -export const SubscriptionPill = (props: TProps) => <>; +export function SubscriptionPill(props: TProps) { + return <>; +} diff --git a/apps/web/ce/components/cycles/active-cycle/root.tsx b/apps/web/ce/components/cycles/active-cycle/root.tsx index 8ac331988..12e395fba 100644 --- a/apps/web/ce/components/cycles/active-cycle/root.tsx +++ b/apps/web/ce/components/cycles/active-cycle/root.tsx @@ -1,11 +1,13 @@ -"use client"; - -import { useMemo } from "react"; import { observer } from "mobx-react"; +import { useTheme } from "next-themes"; import { Disclosure } from "@headlessui/react"; // plane imports import { useTranslation } from "@plane/i18n"; +import type { ICycle } from "@plane/types"; import { Row } from "@plane/ui"; +// assets +import darkActiveCycleAsset from "@/app/assets/empty-state/cycle/active-dark.webp?url"; +import lightActiveCycleAsset from "@/app/assets/empty-state/cycle/active-light.webp?url"; // components import { ActiveCycleStats } from "@/components/cycles/active-cycle/cycle-stats"; import { ActiveCycleProductivity } from "@/components/cycles/active-cycle/productivity"; @@ -16,7 +18,6 @@ import { CyclesListItem } from "@/components/cycles/list/cycles-list-item"; import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; // hooks import { useCycle } from "@/hooks/store/use-cycle"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import type { ActiveCycleIssueDetails } from "@/store/issue/cycle"; interface IActiveCycleDetails { @@ -26,15 +27,80 @@ interface IActiveCycleDetails { showHeader?: boolean; } -export const ActiveCycleRoot: React.FC = observer((props) => { +type ActiveCyclesComponentProps = { + cycleId: string | null | undefined; + activeCycle: ICycle | null; + activeCycleResolvedPath: string; + workspaceSlug: string; + projectId: string; + handleFiltersUpdate: (filters: any) => void; + cycleIssueDetails?: ActiveCycleIssueDetails | { nextPageResults: boolean }; +}; + +const ActiveCyclesComponent = observer(function ActiveCyclesComponent({ + cycleId, + activeCycle, + activeCycleResolvedPath, + workspaceSlug, + projectId, + handleFiltersUpdate, + cycleIssueDetails, +}: ActiveCyclesComponentProps) { + const { t } = useTranslation(); + + if (!cycleId || !activeCycle) { + return ( + + ); + } + + return ( +
+ + +
+ + + +
+
+
+ ); +}); + +export const ActiveCycleRoot = observer(function ActiveCycleRoot(props: IActiveCycleDetails) { const { workspaceSlug, projectId, cycleId: propsCycleId, showHeader = true } = props; + // theme hook + const { resolvedTheme } = useTheme(); // plane hooks const { t } = useTranslation(); // store hooks const { currentProjectActiveCycleId } = useCycle(); // derived values const cycleId = propsCycleId ?? currentProjectActiveCycleId; - const activeCycleResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/cycle/active" }); + const activeCycleResolvedPath = resolvedTheme === "light" ? lightActiveCycleAsset : darkActiveCycleAsset; // fetch cycle details const { handleFiltersUpdate, @@ -42,52 +108,6 @@ export const ActiveCycleRoot: React.FC = observer((props) = cycleIssueDetails, } = useCyclesDetails({ workspaceSlug, projectId, cycleId }); - const ActiveCyclesComponent = useMemo( - () => ( - <> - {!cycleId || !activeCycle ? ( - - ) : ( -
- {cycleId && ( - - )} - -
- - - -
-
-
- )} - - ), - [cycleId, activeCycle, workspaceSlug, projectId, handleFiltersUpdate, cycleIssueDetails] - ); - return ( <> {showHeader ? ( @@ -97,12 +117,30 @@ export const ActiveCycleRoot: React.FC = observer((props) = - {ActiveCyclesComponent} + + + )} ) : ( - <>{ActiveCyclesComponent} + )} ); diff --git a/apps/web/ce/components/cycles/additional-actions.tsx b/apps/web/ce/components/cycles/additional-actions.tsx index 0fd9efb31..96a4a004d 100644 --- a/apps/web/ce/components/cycles/additional-actions.tsx +++ b/apps/web/ce/components/cycles/additional-actions.tsx @@ -4,4 +4,6 @@ type Props = { cycleId: string; projectId: string; }; -export const CycleAdditionalActions: FC = observer(() => <>); +export const CycleAdditionalActions = observer(function CycleAdditionalActions(_props: Props) { + return <>; +}); diff --git a/apps/web/ce/components/cycles/analytics-sidebar/base.tsx b/apps/web/ce/components/cycles/analytics-sidebar/base.tsx index 37a070774..3bcf28321 100644 --- a/apps/web/ce/components/cycles/analytics-sidebar/base.tsx +++ b/apps/web/ce/components/cycles/analytics-sidebar/base.tsx @@ -1,5 +1,3 @@ -"use client"; -import type { FC } from "react"; import { Fragment } from "react"; import { observer } from "mobx-react"; // plane imports @@ -19,7 +17,7 @@ type ProgressChartProps = { projectId: string; cycleId: string; }; -export const SidebarChart: FC = observer((props) => { +export const SidebarChart = observer(function SidebarChart(props: ProgressChartProps) { const { workspaceSlug, projectId, cycleId } = props; // hooks diff --git a/apps/web/ce/components/cycles/analytics-sidebar/root.tsx b/apps/web/ce/components/cycles/analytics-sidebar/root.tsx index 6be4361ef..947a8844c 100644 --- a/apps/web/ce/components/cycles/analytics-sidebar/root.tsx +++ b/apps/web/ce/components/cycles/analytics-sidebar/root.tsx @@ -1,4 +1,3 @@ -"use client"; import type { FC } from "react"; import React from "react"; // components @@ -10,4 +9,6 @@ type Props = { cycleId: string; }; -export const SidebarChartRoot: FC = (props) => ; +export function SidebarChartRoot(props: Props) { + return ; +} diff --git a/apps/web/ce/components/cycles/end-cycle/modal.tsx b/apps/web/ce/components/cycles/end-cycle/modal.tsx index 754c84f9f..87b4ded45 100644 --- a/apps/web/ce/components/cycles/end-cycle/modal.tsx +++ b/apps/web/ce/components/cycles/end-cycle/modal.tsx @@ -10,4 +10,6 @@ interface Props { cycleName: string; } -export const EndCycleModal: React.FC = () => <>; +export function EndCycleModal(_props: Props) { + return <>; +} diff --git a/apps/web/ce/components/de-dupe/de-dupe-button.tsx b/apps/web/ce/components/de-dupe/de-dupe-button.tsx index 94d800ca8..1575abac8 100644 --- a/apps/web/ce/components/de-dupe/de-dupe-button.tsx +++ b/apps/web/ce/components/de-dupe/de-dupe-button.tsx @@ -1,4 +1,3 @@ -"use client"; import type { FC } from "react"; import React from "react"; // local components @@ -10,7 +9,7 @@ type TDeDupeButtonRoot = { label: string; }; -export const DeDupeButtonRoot: FC = (props) => { +export function DeDupeButtonRoot(props: TDeDupeButtonRoot) { const { workspaceSlug, isDuplicateModalOpen, label, handleOnClick } = props; return <>; -}; +} diff --git a/apps/web/ce/components/de-dupe/duplicate-modal/root.tsx b/apps/web/ce/components/de-dupe/duplicate-modal/root.tsx index 55eb084fd..577152bee 100644 --- a/apps/web/ce/components/de-dupe/duplicate-modal/root.tsx +++ b/apps/web/ce/components/de-dupe/duplicate-modal/root.tsx @@ -1,5 +1,3 @@ -"use-client"; - import type { FC } from "react"; // types import type { TDeDupeIssue } from "@plane/types"; @@ -10,7 +8,7 @@ type TDuplicateModalRootProps = { handleDuplicateIssueModal: (value: boolean) => void; }; -export const DuplicateModalRoot: FC = (props) => { +export function DuplicateModalRoot(props: TDuplicateModalRootProps) { const { workspaceSlug, issues, handleDuplicateIssueModal } = props; return <>; -}; +} diff --git a/apps/web/ce/components/de-dupe/duplicate-popover/root.tsx b/apps/web/ce/components/de-dupe/duplicate-popover/root.tsx index 3dd227cc8..957fb01f2 100644 --- a/apps/web/ce/components/de-dupe/duplicate-popover/root.tsx +++ b/apps/web/ce/components/de-dupe/duplicate-popover/root.tsx @@ -1,5 +1,3 @@ -"use client"; - import type { FC } from "react"; import React from "react"; import { observer } from "mobx-react"; @@ -18,7 +16,7 @@ type TDeDupeIssuePopoverRootProps = { isIntakeIssue?: boolean; }; -export const DeDupeIssuePopoverRoot: FC = observer((props) => { +export const DeDupeIssuePopoverRoot = observer(function DeDupeIssuePopoverRoot(props: TDeDupeIssuePopoverRootProps) { const {} = props; return <>; }); diff --git a/apps/web/ce/components/de-dupe/issue-block/button-label.tsx b/apps/web/ce/components/de-dupe/issue-block/button-label.tsx index d6e363456..2ec2b8caa 100644 --- a/apps/web/ce/components/de-dupe/issue-block/button-label.tsx +++ b/apps/web/ce/components/de-dupe/issue-block/button-label.tsx @@ -1,5 +1,3 @@ -"use client"; - import type { FC } from "react"; type TDeDupeIssueButtonLabelProps = { @@ -7,7 +5,7 @@ type TDeDupeIssueButtonLabelProps = { buttonLabel: string; }; -export const DeDupeIssueButtonLabel: FC = (props) => { +export function DeDupeIssueButtonLabel(props: TDeDupeIssueButtonLabelProps) { const { isOpen, buttonLabel } = props; return <>; -}; +} diff --git a/apps/web/ce/components/desktop/helper.ts b/apps/web/ce/components/desktop/helper.ts new file mode 100644 index 000000000..2082e74ed --- /dev/null +++ b/apps/web/ce/components/desktop/helper.ts @@ -0,0 +1 @@ +export const isSidebarToggleVisible = () => true; diff --git a/apps/web/ce/components/desktop/index.ts b/apps/web/ce/components/desktop/index.ts new file mode 100644 index 000000000..c562f8102 --- /dev/null +++ b/apps/web/ce/components/desktop/index.ts @@ -0,0 +1,2 @@ +export * from "./helper"; +export * from "./sidebar-workspace-menu"; diff --git a/apps/web/ce/components/desktop/sidebar-workspace-menu.tsx b/apps/web/ce/components/desktop/sidebar-workspace-menu.tsx new file mode 100644 index 000000000..98ea7e961 --- /dev/null +++ b/apps/web/ce/components/desktop/sidebar-workspace-menu.tsx @@ -0,0 +1,3 @@ +export function DesktopSidebarWorkspaceMenu() { + return null; +} diff --git a/apps/web/ce/components/editor/embeds/index.ts b/apps/web/ce/components/editor/embeds/index.ts deleted file mode 100644 index 8146e94d9..000000000 --- a/apps/web/ce/components/editor/embeds/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./mentions"; diff --git a/apps/web/ce/components/editor/embeds/mentions/root.tsx b/apps/web/ce/components/editor/embeds/mentions/root.tsx index 23f15fe27..2f3f9a5b6 100644 --- a/apps/web/ce/components/editor/embeds/mentions/root.tsx +++ b/apps/web/ce/components/editor/embeds/mentions/root.tsx @@ -1,4 +1,8 @@ -// plane editor -import type { TMentionComponentProps } from "@plane/editor"; +// plane imports +import type { TCallbackMentionComponentProps } from "@plane/editor"; -export const EditorAdditionalMentionsRoot: React.FC = () => null; +export type TEditorMentionComponentProps = TCallbackMentionComponentProps; + +export function EditorAdditionalMentionsRoot(_props: TEditorMentionComponentProps) { + return null; +} diff --git a/apps/web/ce/components/editor/index.ts b/apps/web/ce/components/editor/index.ts deleted file mode 100644 index cf8352ae4..000000000 --- a/apps/web/ce/components/editor/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./embeds"; diff --git a/apps/web/ce/components/epics/epic-modal/modal.tsx b/apps/web/ce/components/epics/epic-modal/modal.tsx index f1fec6ba8..f1dbe83d5 100644 --- a/apps/web/ce/components/epics/epic-modal/modal.tsx +++ b/apps/web/ce/components/epics/epic-modal/modal.tsx @@ -1,4 +1,3 @@ -"use client"; import type { FC } from "react"; import React from "react"; import type { TIssue } from "@plane/types"; @@ -17,4 +16,6 @@ export interface EpicModalProps { isProjectSelectionDisabled?: boolean; } -export const CreateUpdateEpicModal: FC = (props) => <>; +export function CreateUpdateEpicModal(props: EpicModalProps) { + return <>; +} diff --git a/apps/web/ce/components/estimates/estimate-list-item-buttons.tsx b/apps/web/ce/components/estimates/estimate-list-item-buttons.tsx index 936fdc622..a3e88778c 100644 --- a/apps/web/ce/components/estimates/estimate-list-item-buttons.tsx +++ b/apps/web/ce/components/estimates/estimate-list-item-buttons.tsx @@ -1,10 +1,6 @@ -import type { FC } from "react"; import { observer } from "mobx-react"; -import { Pen, Trash } from "lucide-react"; +import { Trash } from "lucide-react"; import { PROJECT_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants"; -import { Tooltip } from "@plane/propel/tooltip"; -// components -import { ProIcon } from "@/components/common/pro-icon"; type TEstimateListItem = { estimateId: string; @@ -15,28 +11,12 @@ type TEstimateListItem = { onDeleteClick?: (estimateId: string) => void; }; -export const EstimateListItemButtons: FC = observer((props) => { +export const EstimateListItemButtons = observer(function EstimateListItemButtons(props: TEstimateListItem) { const { estimateId, isAdmin, isEditable, onDeleteClick } = props; if (!isAdmin || !isEditable) return <>; return (
- -
Upgrade
- -
- } - position="top" - > - -
); -}; +} diff --git a/apps/web/ce/components/gantt-chart/blocks/blocks-list.tsx b/apps/web/ce/components/gantt-chart/blocks/blocks-list.tsx index 593e80502..1a14dd2de 100644 --- a/apps/web/ce/components/gantt-chart/blocks/blocks-list.tsx +++ b/apps/web/ce/components/gantt-chart/blocks/blocks-list.tsx @@ -15,7 +15,7 @@ export type GanttChartBlocksProps = { enableDependency: boolean | ((blockId: string) => boolean); }; -export const GanttChartBlocksList: FC = (props) => { +export function GanttChartBlocksList(props: GanttChartBlocksProps) { const { blockIds, blockToRender, @@ -50,4 +50,4 @@ export const GanttChartBlocksList: FC = (props) => { ))} ); -}; +} diff --git a/apps/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx b/apps/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx index a68118b6c..5257bacec 100644 --- a/apps/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx +++ b/apps/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx @@ -6,4 +6,6 @@ type LeftDependencyDraggableProps = { ganttContainerRef: RefObject; }; -export const LeftDependencyDraggable = (props: LeftDependencyDraggableProps) => <>; +export function LeftDependencyDraggable(props: LeftDependencyDraggableProps) { + return <>; +} diff --git a/apps/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx b/apps/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx index 7a36ec9b3..a8388d4f4 100644 --- a/apps/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx +++ b/apps/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx @@ -5,4 +5,6 @@ type RightDependencyDraggableProps = { block: IGanttBlock; ganttContainerRef: RefObject; }; -export const RightDependencyDraggable = (props: RightDependencyDraggableProps) => <>; +export function RightDependencyDraggable(props: RightDependencyDraggableProps) { + return <>; +} diff --git a/apps/web/ce/components/gantt-chart/dependency/dependency-paths.tsx b/apps/web/ce/components/gantt-chart/dependency/dependency-paths.tsx index e52805e17..d88f5206d 100644 --- a/apps/web/ce/components/gantt-chart/dependency/dependency-paths.tsx +++ b/apps/web/ce/components/gantt-chart/dependency/dependency-paths.tsx @@ -3,7 +3,7 @@ import type { FC } from "react"; type Props = { isEpic?: boolean; }; -export const TimelineDependencyPaths: FC = (props) => { +export function TimelineDependencyPaths(props: Props) { const { isEpic = false } = props; return <>; -}; +} diff --git a/apps/web/ce/components/gantt-chart/dependency/draggable-dependency-path.tsx b/apps/web/ce/components/gantt-chart/dependency/draggable-dependency-path.tsx index 3b4aa350d..d37b2f550 100644 --- a/apps/web/ce/components/gantt-chart/dependency/draggable-dependency-path.tsx +++ b/apps/web/ce/components/gantt-chart/dependency/draggable-dependency-path.tsx @@ -1 +1,3 @@ -export const TimelineDraggablePath = () => <>; +export function TimelineDraggablePath() { + return <>; +} diff --git a/apps/web/ce/components/global/index.ts b/apps/web/ce/components/global/index.ts index c87c8ae02..08b85c764 100644 --- a/apps/web/ce/components/global/index.ts +++ b/apps/web/ce/components/global/index.ts @@ -1,2 +1 @@ export * from "./version-number"; -export * from "./product-updates-header"; diff --git a/apps/web/ce/components/global/product-updates/changelog.tsx b/apps/web/ce/components/global/product-updates/changelog.tsx new file mode 100644 index 000000000..672b7490b --- /dev/null +++ b/apps/web/ce/components/global/product-updates/changelog.tsx @@ -0,0 +1,83 @@ +import { useState, useEffect, useRef } from "react"; +import { observer } from "mobx-react"; +// hooks +import { Loader } from "@plane/ui"; +import { ProductUpdatesFallback } from "@/components/global/product-updates/fallback"; +import { useInstance } from "@/hooks/store/use-instance"; + +export const ProductUpdatesChangelog = observer(function ProductUpdatesChangelog() { + // refs + const isLoadingRef = useRef(true); + // states + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); + // store hooks + const { config } = useInstance(); + // derived values + const changeLogUrl = config?.instance_changelog_url; + const shouldShowFallback = !changeLogUrl || changeLogUrl === "" || hasError; + + // timeout fallback - if iframe doesn't load within 15 seconds, show error + useEffect(() => { + if (!changeLogUrl || changeLogUrl === "") { + setIsLoading(false); + isLoadingRef.current = false; + return; + } + + setIsLoading(true); + setHasError(false); + isLoadingRef.current = true; + + const timeoutId = setTimeout(() => { + if (isLoadingRef.current) { + setHasError(true); + setIsLoading(false); + isLoadingRef.current = false; + } + }, 15000); // 15 second timeout + + return () => { + clearTimeout(timeoutId); + }; + }, [changeLogUrl]); + + const handleIframeLoad = () => { + setTimeout(() => { + isLoadingRef.current = false; + setIsLoading(false); + }, 1000); + }; + + const handleIframeError = () => { + isLoadingRef.current = false; + setHasError(true); + setIsLoading(false); + }; + + // Show fallback if URL is missing, empty, or iframe failed to load + if (shouldShowFallback) { + return ( + + ); + } + + return ( +
+ {isLoading && ( + + + + )} +