diff --git a/apps/admin/.env.example b/apps/admin/.env.example index 59663aa6738..e6e7a40c9d7 100644 --- a/apps/admin/.env.example +++ b/apps/admin/.env.example @@ -1,6 +1,7 @@ VITE_API_BASE_URL="http://localhost:8000" VITE_WEB_BASE_URL="http://localhost:3000" +VITE_WEB_BASE_PATH="" VITE_ADMIN_BASE_URL="http://localhost:3001" VITE_ADMIN_BASE_PATH="/god-mode" diff --git a/apps/admin/app/(all)/(dashboard)/sidebar-help-section.tsx b/apps/admin/app/(all)/(dashboard)/sidebar-help-section.tsx index 9ee63f3be68..18766efa7ef 100644 --- a/apps/admin/app/(all)/(dashboard)/sidebar-help-section.tsx +++ b/apps/admin/app/(all)/(dashboard)/sidebar-help-section.tsx @@ -9,7 +9,7 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { HelpCircle, MoveLeft } from "lucide-react"; import { Transition } from "@headlessui/react"; -import { WEB_BASE_URL } from "@plane/constants"; +import { WEB_URL } from "@plane/constants"; // plane internal packages import { DiscordIcon, GithubIcon, NewTabIcon, PageIcon } from "@plane/propel/icons"; import { Tooltip } from "@plane/propel/tooltip"; @@ -45,7 +45,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection // refs const helpOptionsRef = useRef(null); - const redirectionLink = encodeURI(WEB_BASE_URL + "/"); + const redirectionLink = WEB_URL; return (
({ defaultValues, mode: "onChange" }); // derived values - const workspaceBaseURL = encodeURI(WEB_BASE_URL || window.location.origin + "/"); + const workspaceBaseURL = WEB_URL || encodeURI(window.location.origin + "/"); const handleCreateWorkspace = async (formData: IWorkspace) => { await instanceWorkspaceService diff --git a/apps/admin/app/root.tsx b/apps/admin/app/root.tsx index 3627aa86275..0529112e654 100644 --- a/apps/admin/app/root.tsx +++ b/apps/admin/app/root.tsx @@ -4,7 +4,7 @@ * See the LICENSE file for details. */ -import type { ReactNode } from "react"; +import React, { type ReactNode } from "react"; import { Links, Meta, Outlet, Scripts } from "react-router"; import type { LinksFunction } from "react-router"; import * as Sentry from "@sentry/react-router"; @@ -14,6 +14,7 @@ import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url"; import faviconIco from "@/app/assets/favicon/favicon.ico?url"; import { LogoSpinner } from "@/components/common/logo-spinner"; import globalStyles from "@/styles/globals.css?url"; +import { joinUrlPath } from "@plane/utils"; import { AppProviders } from "@/providers"; import type { Route } from "./+types/root"; // fonts @@ -25,13 +26,15 @@ import "@fontsource/ibm-plex-mono"; const APP_TITLE = "Plane | Simple, extensible, open-source project management tool."; const APP_DESCRIPTION = "Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind."; +const WEB_BASE_PATH = + (typeof import.meta !== "undefined" && import.meta.env?.BASE_URL) || process.env.VITE_ADMIN_BASE_PATH || "/god-mode"; export const links: LinksFunction = () => [ { rel: "apple-touch-icon", sizes: "180x180", href: appleTouchIcon }, { 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: "manifest", href: joinUrlPath(WEB_BASE_PATH, "site.webmanifest.json") }, { rel: "stylesheet", href: globalStyles }, { rel: "preload", @@ -82,6 +85,14 @@ export default function Root() { } export function HydrateFallback() { + const [isMounted, setIsMounted] = React.useState(false); + + React.useEffect(() => { + setIsMounted(true); + }, []); + + if (typeof window === "undefined" || !isMounted) return
; + return (
diff --git a/apps/admin/core/components/workspace/list-item.tsx b/apps/admin/core/components/workspace/list-item.tsx index 3e111972146..cc78811ba64 100644 --- a/apps/admin/core/components/workspace/list-item.tsx +++ b/apps/admin/core/components/workspace/list-item.tsx @@ -7,7 +7,7 @@ import { observer } from "mobx-react"; // plane internal packages -import { WEB_BASE_URL } from "@plane/constants"; +import { WEB_URL } from "@plane/constants"; import { NewTabIcon } from "@plane/propel/icons"; import { Tooltip } from "@plane/propel/tooltip"; import { getFileURL } from "@plane/utils"; @@ -28,16 +28,15 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace return (
{workspace?.logo_url && workspace.logo_url !== "" ? ( { + setIsMounted(true); + }, []); + + if (typeof window === "undefined" || !isMounted) return
; + return (
diff --git a/apps/space/public/site.webmanifest.json b/apps/space/public/site.webmanifest.json index 8885d137bbd..c8834acbd33 100644 --- a/apps/space/public/site.webmanifest.json +++ b/apps/space/public/site.webmanifest.json @@ -7,7 +7,15 @@ "background_color": "#f9fafb", "theme_color": "#3f76ff", "icons": [ - { "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } + { + "src": "favicon/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "favicon/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } ] } diff --git a/apps/web/.env.example b/apps/web/.env.example index 59663aa6738..e6e7a40c9d7 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,6 +1,7 @@ VITE_API_BASE_URL="http://localhost:8000" VITE_WEB_BASE_URL="http://localhost:3000" +VITE_WEB_BASE_PATH="" VITE_ADMIN_BASE_URL="http://localhost:3001" VITE_ADMIN_BASE_PATH="/god-mode" diff --git a/apps/web/Dockerfile.web b/apps/web/Dockerfile.web index 104a9e2f7f3..d38adf7abef 100644 --- a/apps/web/Dockerfile.web +++ b/apps/web/Dockerfile.web @@ -67,6 +67,9 @@ ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH ARG VITE_WEB_BASE_URL="" ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL +ARG VITE_WEB_BASE_PATH="/" +ENV VITE_WEB_BASE_PATH=$VITE_WEB_BASE_PATH + ENV NEXT_TELEMETRY_DISABLED=1 ENV TURBO_TELEMETRY_DISABLED=1 diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index ab41f06d4e6..31f3b04ccb5 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -12,7 +12,7 @@ import "@/styles/globals.css"; import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants"; // helpers -import { cn } from "@plane/utils"; +import { cn, joinUrlPath } from "@plane/utils"; // assets import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url"; @@ -57,6 +57,8 @@ export const meta = () => [ export default function RootLayout({ children }: { children: React.ReactNode }) { const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0"); + const WEB_BASE_PATH = + (typeof import.meta !== "undefined" && import.meta.env?.BASE_URL) || process.env.VITE_WEB_BASE_PATH || "/"; return ( @@ -64,7 +66,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - + {/* Meta info for PWA */} @@ -76,7 +78,6 @@ export default function RootLayout({ children }: { children: React.ReactNode }) -
diff --git a/apps/web/app/root.tsx b/apps/web/app/root.tsx index 1125cab3929..510a36373af 100644 --- a/apps/web/app/root.tsx +++ b/apps/web/app/root.tsx @@ -4,7 +4,8 @@ * See the LICENSE file for details. */ -import type { ReactNode } from "react"; +import React from "react"; +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"; @@ -12,7 +13,7 @@ import type { LinksFunction } from "react-router"; import { ThemeProvider, useTheme } from "next-themes"; // plane imports import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants"; -import { cn } from "@plane/utils"; +import { cn, joinUrlPath } from "@plane/utils"; // types // assets import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url"; @@ -35,16 +36,18 @@ import "@fontsource/material-symbols-rounded"; import "@fontsource/ibm-plex-mono"; const APP_TITLE = "Plane | Simple, extensible, open-source project management tool."; +const WEB_BASE_PATH = + (typeof import.meta !== "undefined" && import.meta.env?.BASE_URL) || process.env.VITE_WEB_BASE_PATH || "/"; 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: "manifest", href: joinUrlPath(WEB_BASE_PATH, "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 }, { rel: "preload", @@ -135,9 +138,14 @@ export default function Root() { export function HydrateFallback() { const { resolvedTheme } = useTheme(); + const [isMounted, setIsMounted] = React.useState(false); + + React.useEffect(() => { + setIsMounted(true); + }, []); // if we are on the server or the theme is not resolved, return an empty div - if (typeof window === "undefined" || resolvedTheme === undefined) return
; + if (typeof window === "undefined" || !isMounted || resolvedTheme === undefined) return
; return (
diff --git a/apps/web/public/manifest.json b/apps/web/public/manifest.json index 35917737d44..17aed8bf498 100644 --- a/apps/web/public/manifest.json +++ b/apps/web/public/manifest.json @@ -3,25 +3,25 @@ "short_name": "Plane", "icons": [ { - "src": "/icons/icon-192x192.png", + "src": "icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { - "src": "/icons/icon-348x348.png", + "src": "icons/icon-348x348.png", "sizes": "348x348", "type": "image/png" }, { - "src": "/icons/icon-512x512.png", + "src": "icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#FFFFFF", "background_color": "#FFFFFF", - "start_url": "/", + "start_url": ".", "display": "standalone", "orientation": "portrait" } diff --git a/apps/web/public/site.webmanifest.json b/apps/web/public/site.webmanifest.json index 7f53eaa6116..1670b02badf 100644 --- a/apps/web/public/site.webmanifest.json +++ b/apps/web/public/site.webmanifest.json @@ -7,7 +7,15 @@ "background_color": "#f9fafb", "theme_color": "#3f76ff", "icons": [ - { "src": "/plane-logos/plane-mobile-pwa.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/plane-logos/plane-mobile-pwa.png", "sizes": "512x512", "type": "image/png" } + { + "src": "plane-logos/plane-mobile-pwa.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "plane-logos/plane-mobile-pwa.png", + "sizes": "512x512", + "type": "image/png" + } ] } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index b80e3b65eee..9f2267ff9d4 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -3,6 +3,7 @@ import * as dotenv from "@dotenvx/dotenvx"; import { reactRouter } from "@react-router/dev/vite"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; +import { joinUrlPath } from "@plane/utils"; dotenv.config({ path: path.resolve(__dirname, ".env") }); @@ -14,7 +15,10 @@ const viteEnv = Object.keys(process.env) return a; }, {}); +const basePath = joinUrlPath(process.env.VITE_WEB_BASE_PATH ?? "", "/") ?? "/"; + export default defineConfig(() => ({ + base: basePath, define: { "process.env": JSON.stringify(viteEnv), }, diff --git a/turbo.json b/turbo.json index 5d1c227553b..bb41942c59c 100644 --- a/turbo.json +++ b/turbo.json @@ -6,6 +6,7 @@ "DEV", "LOG_LEVEL", "NODE_ENV", + "BASE_URL", "SENTRY_DSN", "SENTRY_ENVIRONMENT", "SENTRY_TRACES_SAMPLE_RATE",