Compare commits

...
Author SHA1 Message Date
VipinDevelops 4d6e8902d1 fix: enter handler for emoji 2025-11-13 13:32:27 +05:30
Prateek ShouryaandGitHub a04d3b5c29 [WEB-5414] fix: Use correct SWR cache key for fetching project details (#8103) 2025-11-12 23:31:26 +05:30
sriram veeraghantaandGitHub 30da349475 [WEB-5413] feat: adding sentry error handling in web, space and admin (#8099) 2025-11-12 19:03:47 +05:30
Anmol Singh BhatiaandGitHub 0b78e03055 [WEB-5394] fix: propel storybook circular dependencies (#8096)
* fix: resolve circular dependency crashes in storybook stories

* fix: toolbar circular dependency fix and code refactor
2025-11-11 19:29:16 +05:30
Prateek ShouryaandGitHub f8a9a1576d [WEB-5399] chore: update dependencies and improve PostHog integration (#8097)
- Updated PostHog React package to version 1.4.0 and posthog-js to version 1.255.1.
- Refactored PostHogProvider to include hydration state management for improved event tracking.
- Cleaned up version references in pnpm-lock.yaml for react-router and next-themes.
2025-11-11 19:27:49 +05:30
Krzysztof DąbrowskiandGitHub 6ba15545d9 fix: escape slashes in environment variable values during update (#8081) 2025-11-11 17:52:03 +05:30
sriramveeraghanta da22c2f670 chore(deps): bump turbo package and remove unused packages 2025-11-11 15:50:10 +05:30
sriramveeraghanta d47755b4c6 chore: remove unused files 2025-11-11 15:45:53 +05:30
sriramveeraghanta b51da2f997 chore: remove unused middleware files 2025-11-11 15:33:34 +05:30
Aaryan KhandelwalandGitHub 1447be12e6 [WEB-5370] fix: cycle and modules not being removed from the work item edit modal #8094 2025-11-11 14:55:27 +05:30
Prateek ShouryaandGitHub 433b5a4fe1 [WEB-5386] refactor: update all apps to use react-router for development and enable SSR for space app. (#8095) 2025-11-11 14:08:42 +05:30
sriramveeraghanta 4ae0763d0f chore: bump turbo version 2025-11-11 13:32:10 +05:30
Prateek ShouryaandGitHub a407105bb8 [WEB-5389] chore: enable progress bar for production builds and add logo spinner for hydration fallback (#8093)
* [WEB-5389] chore: enable progress bar for production builds

* feat: add LogoSpinner to HydrateFallback in admin, space, and web apps
2025-11-11 12:22:40 +05:30
M. PalanikannanandGitHub 5d7bf554dd [WEB-5381] fix: buffer issue with vite #8091 2025-11-10 20:33:41 +05:30
sriramveeraghanta 5ec09a7fe1 chore: update dockerfile pnpm install with ci=true 2025-11-10 18:28:51 +05:30
sriramveeraghanta 3f0721c59a chore: upgrade vite version using catalog 2025-11-10 13:13:22 +05:30
69 changed files with 1337 additions and 1770 deletions
+8 -8
View File
@@ -1,12 +1,12 @@
NEXT_PUBLIC_API_BASE_URL="http://localhost:8000"
VITE_API_BASE_URL="http://localhost:8000"
NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000"
VITE_WEB_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001"
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
VITE_ADMIN_BASE_URL="http://localhost:3001"
VITE_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002"
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
VITE_SPACE_BASE_URL="http://localhost:3002"
VITE_SPACE_BASE_PATH="/spaces"
NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100"
NEXT_PUBLIC_LIVE_BASE_PATH="/live"
VITE_LIVE_BASE_URL="http://localhost:3100"
VITE_LIVE_BASE_PATH="/live"
+25 -25
View File
@@ -28,35 +28,35 @@ FROM base AS installer
ENV NODE_ENV=production
# Public envs required at build time (pick up via process.env)
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_API_BASE_PATH="/api"
ENV NEXT_PUBLIC_API_BASE_PATH=$NEXT_PUBLIC_API_BASE_PATH
ARG VITE_API_BASE_URL=""
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ARG VITE_API_BASE_PATH="/api"
ENV VITE_API_BASE_PATH=$VITE_API_BASE_PATH
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG VITE_ADMIN_BASE_URL=""
ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL
ARG VITE_ADMIN_BASE_PATH="/god-mode"
ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ARG VITE_SPACE_BASE_URL=""
ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL
ARG VITE_SPACE_BASE_PATH="/spaces"
ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH
ARG NEXT_PUBLIC_LIVE_BASE_URL=""
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live"
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH
ARG VITE_LIVE_BASE_URL=""
ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL
ARG VITE_LIVE_BASE_PATH="/live"
ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_PATH=""
ENV NEXT_PUBLIC_WEB_BASE_PATH=$NEXT_PUBLIC_WEB_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
ARG NEXT_PUBLIC_WEBSITE_URL="https://plane.so"
ENV NEXT_PUBLIC_WEBSITE_URL=$NEXT_PUBLIC_WEBSITE_URL
ARG NEXT_PUBLIC_SUPPORT_EMAIL="support@plane.so"
ENV NEXT_PUBLIC_SUPPORT_EMAIL=$NEXT_PUBLIC_SUPPORT_EMAIL
ARG VITE_WEBSITE_URL="https://plane.so"
ENV VITE_WEBSITE_URL=$VITE_WEBSITE_URL
ARG VITE_SUPPORT_EMAIL="support@plane.so"
ENV VITE_SUPPORT_EMAIL=$VITE_SUPPORT_EMAIL
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
@@ -66,7 +66,7 @@ COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
# Build only the admin package
RUN pnpm turbo run build --filter=admin
+1 -1
View File
@@ -8,7 +8,7 @@ COPY . .
RUN corepack enable pnpm && pnpm add -g turbo
RUN pnpm install
ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV VITE_ADMIN_BASE_PATH="/god-mode"
EXPOSE 3000
+34
View File
@@ -0,0 +1,34 @@
/* eslint-disable import/order */
import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
Sentry.init({
dsn: process.env.VITE_SENTRY_DSN,
environment: process.env.VITE_SENTRY_ENVIRONMENT,
sendDefaultPii: process.env.VITE_SENTRY_SEND_DEFAULT_PII ? process.env.VITE_SENTRY_SEND_DEFAULT_PII === "1" : false,
release: process.env.VITE_APP_VERSION,
tracesSampleRate: process.env.VITE_SENTRY_TRACES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_TRACES_SAMPLE_RATE)
: 0.1,
profilesSampleRate: process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE)
: 0.1,
replaysSessionSampleRate: process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE)
: 0.1,
replaysOnErrorSampleRate: process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE)
: 1.0,
integrations: [],
});
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});
+12 -2
View File
@@ -1,10 +1,12 @@
import type { ReactNode } from "react";
import * as Sentry from "@sentry/react-router";
import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router";
import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url";
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 { LogoSpinner } from "@/components/common/logo-spinner";
import globalStyles from "@/styles/globals.css?url";
import type { Route } from "./+types/root";
import { AppProviders } from "./providers";
@@ -58,10 +60,18 @@ export default function Root() {
}
export function HydrateFallback() {
return null;
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />
</div>
);
}
export function ErrorBoundary() {
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (error) {
Sentry.captureException(error);
}
return (
<div>
<p>Something went wrong.</p>
@@ -38,7 +38,6 @@ const PROGRESS_CONFIG: Readonly<ProgressConfig> = {
easing: "ease",
trickle: true,
delay: 0,
isDisabled: import.meta.env.PROD, // Disable progress bar in production builds
} as const;
/**
-14
View File
@@ -1,14 +0,0 @@
import { next } from '@vercel/edge';
export default function middleware() {
return next({
headers: {
'Referrer-Policy': 'origin-when-cross-origin',
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
'X-DNS-Prefetch-Control': 'on',
'Strict-Transport-Security':
'max-age=31536000; includeSubDomains; preload',
},
});
}
+4 -14
View File
@@ -6,9 +6,9 @@
"private": true,
"type": "module",
"scripts": {
"dev": "cross-env NODE_ENV=development PORT=3001 node server.mjs",
"dev": "react-router dev --port 3001",
"build": "react-router build",
"preview": "react-router build && cross-env NODE_ENV=production PORT=3001 node server.mjs",
"preview": "react-router build && serve -s build/client -l 3001",
"start": "serve -s build/client -l 3001",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist && rm -rf build",
"check:lint": "eslint . --max-warnings 19",
@@ -27,23 +27,16 @@
"@plane/types": "workspace:*",
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@react-router/express": "^7.9.3",
"@react-router/node": "^7.9.3",
"@sentry/react-router": "catalog:",
"@tanstack/react-virtual": "^3.13.12",
"@tanstack/virtual-core": "^3.13.12",
"@vercel/edge": "1.2.2",
"axios": "catalog:",
"compression": "^1.8.1",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
"express": "^5.1.0",
"http-proxy-middleware": "^3.0.5",
"isbot": "^5.1.31",
"lodash-es": "catalog:",
"lucide-react": "catalog:",
"mobx": "catalog:",
"mobx-react": "catalog:",
"morgan": "^1.10.1",
"next-themes": "^0.2.1",
"react": "catalog:",
"react-dom": "catalog:",
@@ -59,15 +52,12 @@
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@react-router/dev": "^7.9.1",
"@types/compression": "^1.8.1",
"@types/express": "4.17.23",
"@types/lodash-es": "catalog:",
"@types/morgan": "^1.9.10",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"typescript": "catalog:",
"vite": "7.1.7",
"vite": "catalog:",
"vite-tsconfig-paths": "^5.1.4"
}
}
+4 -1
View File
@@ -1,8 +1,11 @@
import type { Config } from "@react-router/dev/config";
import { joinUrlPath } from "@plane/utils";
const basePath = joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ?? "", "/") ?? "/";
export default {
appDirectory: "app",
basename: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH,
basename: basePath,
// Admin runs as a client-side app; build a static client bundle only
ssr: false,
} satisfies Config;
-76
View File
@@ -1,76 +0,0 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import compression from "compression";
import dotenv from "dotenv";
import express from "express";
import morgan from "morgan";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, ".env") });
const BUILD_PATH = "./build/server/index.js";
const DEVELOPMENT = process.env.NODE_ENV !== "production";
// Derive the port from NEXT_PUBLIC_ADMIN_BASE_URL when available, otherwise
// default to http://localhost:3001 and fall back to PORT env if explicitly set.
const DEFAULT_BASE_URL = "http://localhost:3001";
const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || DEFAULT_BASE_URL;
let parsedBaseUrl;
try {
parsedBaseUrl = new URL(ADMIN_BASE_URL);
} catch {
parsedBaseUrl = new URL(DEFAULT_BASE_URL);
}
const PORT = Number.parseInt(parsedBaseUrl.port, 10);
async function start() {
const app = express();
app.use(compression());
app.disable("x-powered-by");
if (DEVELOPMENT) {
console.log("Starting development server");
const vite = await import("vite").then((vite) =>
vite.createServer({
server: { middlewareMode: true },
appType: "custom",
})
);
app.use(vite.middlewares);
app.use(async (req, res, next) => {
try {
const source = await vite.ssrLoadModule("./server/app.ts");
return source.app(req, res, next);
} catch (error) {
if (error instanceof Error) {
vite.ssrFixStacktrace(error);
}
next(error);
}
});
} else {
console.log("Starting production server");
app.use("/assets", express.static("build/client/assets", { immutable: true, maxAge: "1y" }));
app.use(morgan("tiny"));
app.use(express.static("build/client", { maxAge: "1h" }));
app.use(await import(BUILD_PATH).then((mod) => mod.app));
}
app.listen(PORT, () => {
const origin = `${parsedBaseUrl.protocol}//${parsedBaseUrl.hostname}:${PORT}`;
console.log(`Server is running on ${origin}`);
});
}
start().catch((error) => {
console.error(error);
process.exit(1);
});
-46
View File
@@ -1,46 +0,0 @@
import "react-router";
import { createRequestHandler } from "@react-router/express";
import express from "express";
import type { Express } from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
const NEXT_PUBLIC_API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
? process.env.NEXT_PUBLIC_API_BASE_URL.replace(/\/$/, "")
: "http://127.0.0.1:8000";
const NEXT_PUBLIC_API_BASE_PATH = process.env.NEXT_PUBLIC_API_BASE_PATH
? process.env.NEXT_PUBLIC_API_BASE_PATH.replace(/\/+$/, "")
: "/api";
const NORMALIZED_API_BASE_PATH = NEXT_PUBLIC_API_BASE_PATH.startsWith("/")
? NEXT_PUBLIC_API_BASE_PATH
: `/${NEXT_PUBLIC_API_BASE_PATH}`;
const NEXT_PUBLIC_ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH
? process.env.NEXT_PUBLIC_ADMIN_BASE_PATH.replace(/\/$/, "")
: "/";
export const app: Express = express();
// Ensure proxy-aware hostname/URL handling (e.g., X-Forwarded-Host/Proto)
// so generated URLs/redirects reflect the public host when behind Nginx.
// See related fix in Remix Express adapter.
app.set("trust proxy", true);
app.use(
"/api",
createProxyMiddleware({
target: NEXT_PUBLIC_API_BASE_URL,
changeOrigin: true,
secure: false,
pathRewrite: (path: string) =>
NORMALIZED_API_BASE_PATH === "/api" ? path : path.replace(/^\/api/, NORMALIZED_API_BASE_PATH),
})
);
const router = express.Router();
router.use(
createRequestHandler({
build: () => import("virtual:react-router/server-build"),
})
);
app.use(NEXT_PUBLIC_ADMIN_BASE_PATH, router);
+27 -50
View File
@@ -4,56 +4,33 @@ import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { joinUrlPath } from "@plane/utils";
const PUBLIC_ENV_KEYS = [
"NEXT_PUBLIC_API_BASE_URL",
"NEXT_PUBLIC_API_BASE_PATH",
"NEXT_PUBLIC_ADMIN_BASE_URL",
"NEXT_PUBLIC_ADMIN_BASE_PATH",
"NEXT_PUBLIC_SPACE_BASE_URL",
"NEXT_PUBLIC_SPACE_BASE_PATH",
"NEXT_PUBLIC_LIVE_BASE_URL",
"NEXT_PUBLIC_LIVE_BASE_PATH",
"NEXT_PUBLIC_WEB_BASE_URL",
"NEXT_PUBLIC_WEB_BASE_PATH",
"NEXT_PUBLIC_WEBSITE_URL",
"NEXT_PUBLIC_SUPPORT_EMAIL",
];
// Expose only vars starting with VITE_
const viteEnv = Object.keys(process.env)
.filter((k) => k.startsWith("VITE_"))
.reduce<Record<string, string>>((a, k) => {
a[k] = process.env[k] ?? "";
return a;
}, {});
const publicEnv = PUBLIC_ENV_KEYS.reduce<Record<string, string>>((acc, key) => {
acc[key] = process.env[key] ?? "";
return acc;
}, {});
const basePath = joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ?? "", "/") ?? "/";
export default defineConfig(({ isSsrBuild }) => {
// Only produce an SSR bundle when explicitly enabled.
// For static deployments (default), we skip the server build entirely.
const enableSsrBuild = process.env.ADMIN_ENABLE_SSR_BUILD === "true";
const basePath = joinUrlPath(process.env.NEXT_PUBLIC_ADMIN_BASE_PATH ?? "", "/") ?? "/";
return {
base: basePath,
define: {
"process.env": JSON.stringify(publicEnv),
export default defineConfig(() => ({
base: basePath,
define: {
"process.env": JSON.stringify(viteEnv),
},
build: {
assetsInlineLimit: 0,
},
plugins: [reactRouter(), tsconfigPaths({ projects: [path.resolve(__dirname, "tsconfig.json")] })],
resolve: {
alias: {
// Next.js compatibility shims used within admin
"next/image": path.resolve(__dirname, "app/compat/next/image.tsx"),
"next/link": path.resolve(__dirname, "app/compat/next/link.tsx"),
"next/navigation": path.resolve(__dirname, "app/compat/next/navigation.ts"),
},
build: {
assetsInlineLimit: 0,
rollupOptions:
isSsrBuild && enableSsrBuild
? {
input: path.resolve(__dirname, "server/app.ts"),
}
: undefined,
},
plugins: [reactRouter(), tsconfigPaths({ projects: [path.resolve(__dirname, "tsconfig.json")] })],
resolve: {
alias: {
// Next.js compatibility shims used within admin
"next/image": path.resolve(__dirname, "app/compat/next/image.tsx"),
"next/link": path.resolve(__dirname, "app/compat/next/link.tsx"),
"next/navigation": path.resolve(__dirname, "app/compat/next/navigation.ts"),
},
dedupe: ["react", "react-dom"],
},
// No SSR-specific overrides needed; alias resolves to ESM build
};
});
dedupe: ["react", "react-dom"],
},
// No SSR-specific overrides needed; alias resolves to ESM build
}));
+1 -1
View File
@@ -39,7 +39,7 @@ RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/
# Build the project and its dependencies
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
ENV TURBO_TELEMETRY_DISABLED=1
+8 -8
View File
@@ -1,12 +1,12 @@
NEXT_PUBLIC_API_BASE_URL="http://localhost:8000"
VITE_API_BASE_URL="http://localhost:8000"
NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000"
VITE_WEB_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001"
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
VITE_ADMIN_BASE_URL="http://localhost:3001"
VITE_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002"
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
VITE_SPACE_BASE_URL="http://localhost:3002"
VITE_SPACE_BASE_PATH="/spaces"
NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100"
NEXT_PUBLIC_LIVE_BASE_PATH="/live"
VITE_LIVE_BASE_URL="http://localhost:3100"
VITE_LIVE_BASE_PATH="/live"
+1 -1
View File
@@ -12,7 +12,7 @@ RUN pnpm install
EXPOSE 3002
ENV NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV VITE_SPACE_BASE_PATH="/spaces"
VOLUME [ "/app/node_modules", "/app/apps/space/node_modules"]
+33 -29
View File
@@ -28,35 +28,36 @@ FROM base AS installer
ENV NODE_ENV=production
# Public envs required at build time (pick up via process.env)
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_API_BASE_PATH="/api"
ENV NEXT_PUBLIC_API_BASE_PATH=$NEXT_PUBLIC_API_BASE_PATH
ARG VITE_API_BASE_URL=""
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ARG VITE_API_BASE_PATH="/api"
ENV VITE_API_BASE_PATH=$VITE_API_BASE_PATH
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG VITE_ADMIN_BASE_URL=""
ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL
ARG VITE_ADMIN_BASE_PATH="/god-mode"
ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ARG VITE_SPACE_BASE_URL=""
ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL
ARG VITE_SPACE_BASE_PATH="/spaces"
ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH
ARG NEXT_PUBLIC_LIVE_BASE_URL=""
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live"
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH
ARG VITE_LIVE_BASE_URL=""
ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL
ARG VITE_LIVE_BASE_PATH="/live"
ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_PATH=""
ENV NEXT_PUBLIC_WEB_BASE_PATH=$NEXT_PUBLIC_WEB_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
ARG NEXT_PUBLIC_WEBSITE_URL="https://plane.so"
ENV NEXT_PUBLIC_WEBSITE_URL=$NEXT_PUBLIC_WEBSITE_URL
ARG NEXT_PUBLIC_SUPPORT_EMAIL="support@plane.so"
ENV NEXT_PUBLIC_SUPPORT_EMAIL=$NEXT_PUBLIC_SUPPORT_EMAIL
ARG VITE_WEBSITE_URL="https://plane.so"
ENV VITE_WEBSITE_URL=$VITE_WEBSITE_URL
ARG VITE_SUPPORT_EMAIL="support@plane.so"
ENV VITE_SUPPORT_EMAIL=$VITE_SUPPORT_EMAIL
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
@@ -66,21 +67,24 @@ COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
# Build only the space package
RUN pnpm turbo run build --filter=space
# =========================================================================== #
FROM nginx:1.27-alpine AS production
FROM base AS runner
COPY apps/space/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=installer /app/apps/space/build/client /usr/share/nginx/html/spaces
COPY --from=installer /app/apps/space/build ./apps/space/build
COPY --from=installer /app/apps/space/node_modules ./apps/space/node_modules
COPY --from=installer /app/node_modules ./node_modules
WORKDIR /app/apps/space
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -fsS http://127.0.0.1:3000/ >/dev/null || exit 1
CMD ["nginx", "-g", "daemon off;"]
CMD ["npx", "react-router-serve", "./build/server/index.js"]
+34
View File
@@ -0,0 +1,34 @@
/* eslint-disable import/order */
import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
Sentry.init({
dsn: process.env.VITE_SENTRY_DSN,
environment: process.env.VITE_SENTRY_ENVIRONMENT,
sendDefaultPii: process.env.VITE_SENTRY_SEND_DEFAULT_PII ? process.env.VITE_SENTRY_SEND_DEFAULT_PII === "1" : false,
release: process.env.VITE_APP_VERSION,
tracesSampleRate: process.env.VITE_SENTRY_TRACES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_TRACES_SAMPLE_RATE)
: 0.1,
profilesSampleRate: process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE)
: 0.1,
replaysSessionSampleRate: process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE)
: 0.1,
replaysOnErrorSampleRate: process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE)
: 1.0,
integrations: [],
});
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});
@@ -1,66 +0,0 @@
"use client";
import { observer } from "mobx-react";
import { Outlet } from "react-router";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { PoweredBy } from "@/components/common/powered-by";
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
import { IssuesNavbarRoot } from "@/components/issues/navbar";
// hooks
import { usePublish, usePublishList } from "@/hooks/store/publish";
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
import type { Route } from "./+types/client-layout";
const IssuesClientLayout = observer((props: Route.ComponentProps) => {
const { anchor } = props.params;
// store hooks
const { fetchPublishSettings } = usePublishList();
const publishSettings = usePublish(anchor);
const { updateLayoutOptions } = useIssueFilter();
// fetch publish settings
const { error } = useSWR(
anchor ? `PUBLISH_SETTINGS_${anchor}` : null,
anchor
? async () => {
const response = await fetchPublishSettings(anchor);
if (response.view_props) {
updateLayoutOptions({
list: !!response.view_props.list,
kanban: !!response.view_props.kanban,
calendar: !!response.view_props.calendar,
gantt: !!response.view_props.gantt,
spreadsheet: !!response.view_props.spreadsheet,
});
}
}
: null
);
if (!publishSettings && !error) {
return (
<div className="flex items-center justify-center h-screen w-full">
<LogoSpinner />
</div>
);
}
if (error) return <SomethingWentWrongError />;
return (
<>
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
<IssuesNavbarRoot publishSettings={publishSettings} />
</div>
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">
<Outlet />
</div>
</div>
<PoweredBy />
</>
);
});
export default IssuesClientLayout;
+130 -42
View File
@@ -1,54 +1,142 @@
"use server";
import { observer } from "mobx-react";
import { Outlet } from "react-router";
import type { ShouldRevalidateFunctionArgs } from "react-router";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { PoweredBy } from "@/components/common/powered-by";
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
import { IssuesNavbarRoot } from "@/components/issues/navbar";
// hooks
import { PageNotFound } from "@/components/ui/not-found";
import { usePublish, usePublishList } from "@/hooks/store/publish";
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
import type { Route } from "./+types/layout";
type Props = {
children: React.ReactNode;
params: {
anchor: string;
};
};
const DEFAULT_TITLE = "Plane";
const DEFAULT_DESCRIPTION = "Made with Plane, an AI-powered work management platform with publishing capabilities.";
// TODO: Convert into SSR in order to generate metadata
export async function generateMetadata({ params }: Props) {
interface IssueMetadata {
name?: string;
description?: string;
cover_image?: string;
}
// Loader function runs on the server and fetches metadata
export async function loader({ params }: Route.LoaderArgs) {
const { anchor } = params;
const DEFAULT_TITLE = "Plane";
const DEFAULT_DESCRIPTION = "Made with Plane, an AI-powered work management platform with publishing capabilities.";
// Validate anchor before using in request (only allow alphanumeric, -, _)
const ANCHOR_REGEX = /^[a-zA-Z0-9_-]+$/;
if (!ANCHOR_REGEX.test(anchor)) {
return { title: DEFAULT_TITLE, description: DEFAULT_DESCRIPTION };
return { metadata: null };
}
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/public/anchor/${anchor}/meta/`);
const data = await response.json();
return {
title: data?.name || DEFAULT_TITLE,
description: data?.description || DEFAULT_DESCRIPTION,
openGraph: {
title: data?.name || DEFAULT_TITLE,
description: data?.description || DEFAULT_DESCRIPTION,
type: "website",
images: [
{
url: data?.cover_image,
width: 800,
height: 600,
alt: data?.name || DEFAULT_TITLE,
},
],
},
twitter: {
card: "summary_large_image",
title: data?.name || DEFAULT_TITLE,
description: data?.description || DEFAULT_DESCRIPTION,
images: [data?.cover_image],
},
};
} catch {
return { title: DEFAULT_TITLE, description: DEFAULT_DESCRIPTION };
const response = await fetch(`${process.env.VITE_API_BASE_URL}/api/public/anchor/${anchor}/meta/`);
if (!response.ok) {
return { metadata: null };
}
const metadata: IssueMetadata = await response.json();
return { metadata };
} catch (error) {
console.error("Error fetching issue metadata:", error);
return { metadata: null };
}
}
export default async function IssuesLayout(_props: Props) {
// return <IssuesClientLayout params={{ anchor }}>{children}</IssuesClientLayout>;
return null;
// Meta function uses the loader data to generate metadata
export function meta({ loaderData }: Route.MetaArgs) {
const metadata = loaderData?.metadata;
const title = metadata?.name || DEFAULT_TITLE;
const description = metadata?.description || DEFAULT_DESCRIPTION;
const coverImage = metadata?.cover_image;
const metaTags = [
{ title },
{ name: "description", content: description },
// OpenGraph metadata
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "og:type", content: "website" },
// Twitter metadata
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: title },
{ name: "twitter:description", content: description },
];
// Add images if cover image exists
if (coverImage) {
metaTags.push(
{ property: "og:image", content: coverImage },
{ property: "og:image:width", content: "800" },
{ property: "og:image:height", content: "600" },
{ property: "og:image:alt", content: title },
{ name: "twitter:image", content: coverImage }
);
}
return metaTags;
}
// Prevent loader from re-running on anchor param changes
export function shouldRevalidate({ currentParams, nextParams }: ShouldRevalidateFunctionArgs) {
return currentParams.anchor !== nextParams.anchor;
}
function IssuesLayout(props: Route.ComponentProps) {
const { anchor } = props.params;
// store hooks
const { fetchPublishSettings } = usePublishList();
const publishSettings = usePublish(anchor);
const { updateLayoutOptions } = useIssueFilter();
// fetch publish settings
const { error } = useSWR(
anchor ? `PUBLISH_SETTINGS_${anchor}` : null,
anchor
? async () => {
const response = await fetchPublishSettings(anchor);
if (response.view_props) {
updateLayoutOptions({
list: !!response.view_props.list,
kanban: !!response.view_props.kanban,
calendar: !!response.view_props.calendar,
gantt: !!response.view_props.gantt,
spreadsheet: !!response.view_props.spreadsheet,
});
}
}
: null
);
if (!publishSettings && !error) {
return (
<div className="flex items-center justify-center h-screen w-full">
<LogoSpinner />
</div>
);
}
if (error?.status === 404) return <PageNotFound />;
if (error) return <SomethingWentWrongError />;
return (
<>
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
<IssuesNavbarRoot publishSettings={publishSettings} />
</div>
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">
<Outlet />
</div>
</div>
<PoweredBy />
</>
);
}
export default observer(IssuesLayout);
+23 -4
View File
@@ -1,10 +1,13 @@
import * as Sentry from "@sentry/react-router";
import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router";
import type { HeadersFunction, LinksFunction } from "react-router";
// assets
import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url";
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 siteWebmanifest from "@/app/assets/favicon/site.webmanifest?url";
import { LogoSpinner } from "@/components/common/logo-spinner";
import globalStyles from "@/styles/globals.css?url";
// types
import type { Route } from "./+types/root";
@@ -20,10 +23,18 @@ 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: siteWebmanifest },
{ rel: "stylesheet", href: globalStyles },
];
export const headers: HeadersFunction = () => ({
"Referrer-Policy": "origin-when-cross-origin",
"X-Frame-Options": "SAMEORIGIN",
"X-Content-Type-Options": "nosniff",
"X-DNS-Prefetch-Control": "on",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
});
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
@@ -62,9 +73,17 @@ export default function Root() {
}
export function HydrateFallback() {
return null;
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />
</div>
);
}
export function ErrorBoundary() {
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (error) {
Sentry.captureException(error);
}
return <ErrorPage />;
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { index, layout, route } from "@react-router/dev/routes";
export default [
index("./page.tsx"),
route(":workspaceSlug/:projectId", "./[workspaceSlug]/[projectId]/page.tsx"),
layout("./issues/[anchor]/client-layout.tsx", [route("issues/:anchor", "./issues/[anchor]/page.tsx")]),
layout("./issues/[anchor]/layout.tsx", [route("issues/:anchor", "./issues/[anchor]/page.tsx")]),
// Catch-all route for 404 handling
route("*", "./not-found.tsx"),
] satisfies RouteConfig;
@@ -1,6 +1,6 @@
"use client";
import { Fragment, useEffect, useState } from "react";
import { Fragment, useEffect } from "react";
import { observer } from "mobx-react";
import { useRouter, useSearchParams } from "next/navigation";
import { Dialog, Transition } from "@headlessui/react";
@@ -25,19 +25,19 @@ export const IssuePeekOverview: React.FC<TIssuePeekOverview> = observer((props)
const state = searchParams.get("state") || undefined;
const priority = searchParams.get("priority") || undefined;
const labels = searchParams.get("labels") || undefined;
// states
const [isSidePeekOpen, setIsSidePeekOpen] = useState(false);
const [isModalPeekOpen, setIsModalPeekOpen] = useState(false);
// store
const issueDetailStore = useIssueDetails();
const issueDetails = issueDetailStore.peekId && peekId ? issueDetailStore.details[peekId.toString()] : undefined;
const { peekMode, setPeekId, getIssueById, fetchIssueDetails } = useIssueDetails();
// derived values
const issueDetails = peekId ? getIssueById(peekId.toString()) : undefined;
// state
const isSidePeekOpen = !!peekId && peekMode === "side";
const isModalPeekOpen = !!peekId && (peekMode === "modal" || peekMode === "full");
useEffect(() => {
if (anchor && peekId) {
issueDetailStore.fetchIssueDetails(anchor, peekId.toString());
fetchIssueDetails(anchor, peekId.toString());
}
}, [anchor, issueDetailStore, peekId]);
}, [anchor, fetchIssueDetails, peekId]);
const handleClose = () => {
// if close logic is passed down, call that instead of the below logic
@@ -46,7 +46,7 @@ export const IssuePeekOverview: React.FC<TIssuePeekOverview> = observer((props)
return;
}
issueDetailStore.setPeekId(null);
setPeekId(null);
let queryParams: any = {
board,
};
@@ -57,21 +57,6 @@ export const IssuePeekOverview: React.FC<TIssuePeekOverview> = observer((props)
router.push(`/issues/${anchor}?${queryParams}`);
};
useEffect(() => {
if (peekId) {
if (issueDetailStore.peekMode === "side") {
setIsSidePeekOpen(true);
setIsModalPeekOpen(false);
} else {
setIsModalPeekOpen(true);
setIsSidePeekOpen(false);
}
} else {
setIsSidePeekOpen(false);
setIsModalPeekOpen(false);
}
}, [peekId, issueDetailStore.peekMode]);
return (
<>
<Transition.Root appear show={isSidePeekOpen} as={Fragment}>
@@ -116,13 +101,13 @@ export const IssuePeekOverview: React.FC<TIssuePeekOverview> = observer((props)
<Dialog.Panel>
<div
className={`fixed left-1/2 top-1/2 z-20 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-custom-background-100 shadow-custom-shadow-xl transition-all duration-300 ${
issueDetailStore.peekMode === "modal" ? "h-[70%] w-3/5" : "h-[95%] w-[95%]"
peekMode === "modal" ? "h-[70%] w-3/5" : "h-[95%] w-[95%]"
}`}
>
{issueDetailStore.peekMode === "modal" && (
{peekMode === "modal" && (
<SidePeekView anchor={anchor} handleClose={handleClose} issueDetails={issueDetails} />
)}
{issueDetailStore.peekMode === "full" && (
{peekMode === "full" && (
<FullScreenPeekView anchor={anchor} handleClose={handleClose} issueDetails={issueDetails} />
)}
</div>
@@ -38,7 +38,6 @@ const PROGRESS_CONFIG: Readonly<ProgressConfig> = {
easing: "ease",
trickle: true,
delay: 0,
isDisabled: import.meta.env.PROD, // Disable progress bar in production builds
} as const;
/**
+1 -1
View File
@@ -1,6 +1,6 @@
import Link from "next/link";
// helpers
import { SUPPORT_EMAIL } from "./common.helper";
import { SUPPORT_EMAIL } from "@plane/constants";
export enum EPageTypes {
INIT = "INIT",
-2
View File
@@ -1,4 +1,2 @@
export const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "";
export const resolveGeneralTheme = (resolvedTheme: string | undefined) =>
resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system";
-13
View File
@@ -1,13 +0,0 @@
import { next } from "@vercel/edge";
export default function middleware() {
return next({
headers: {
"Referrer-Policy": "origin-when-cross-origin",
"X-Frame-Options": "SAMEORIGIN",
"X-Content-Type-Options": "nosniff",
"X-DNS-Prefetch-Control": "on",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
},
});
}
+6 -16
View File
@@ -5,10 +5,10 @@
"license": "AGPL-3.0",
"type": "module",
"scripts": {
"dev": "cross-env NODE_ENV=development PORT=3002 node server.mjs",
"dev": "react-router dev --port 3002",
"build": "react-router build",
"preview": "react-router build && cross-env NODE_ENV=production PORT=3002 node server.mjs",
"start": "serve -s build/client -l 3002",
"preview": "react-router build && PORT=3002 react-router-serve ./build/server/index.js",
"start": "PORT=3002 react-router-serve ./build/server/index.js",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf .react-router && rm -rf node_modules && rm -rf dist && rm -rf build",
"check:lint": "eslint . --max-warnings 28",
"check:types": "react-router typegen && tsc --noEmit",
@@ -30,24 +30,18 @@
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@popperjs/core": "^2.11.8",
"@react-router/express": "^7.9.3",
"@react-router/node": "^7.9.3",
"@vercel/edge": "1.2.2",
"@react-router/serve": "^7.9.5",
"@sentry/react-router": "catalog:",
"axios": "catalog:",
"clsx": "^2.0.0",
"compression": "^1.8.1",
"cross-env": "^7.0.3",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"express": "^5.1.0",
"http-proxy-middleware": "^3.0.5",
"isbot": "^5.1.31",
"lodash-es": "catalog:",
"lucide-react": "catalog:",
"mobx": "catalog:",
"mobx-react": "catalog:",
"mobx-utils": "catalog:",
"morgan": "^1.10.1",
"next-themes": "^0.2.1",
"react": "catalog:",
"react-dom": "catalog:",
@@ -56,7 +50,6 @@
"react-popper": "^2.3.0",
"react-router": "^7.9.1",
"react-router-dom": "^7.9.1",
"serve": "14.2.5",
"swr": "catalog:",
"uuid": "catalog:"
},
@@ -65,15 +58,12 @@
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@react-router/dev": "^7.9.1",
"@types/compression": "^1.8.1",
"@types/express": "4.17.23",
"@types/lodash-es": "catalog:",
"@types/morgan": "^1.9.10",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"typescript": "catalog:",
"vite": "7.1.7",
"vite": "catalog:",
"vite-tsconfig-paths": "^5.1.4"
}
}
+5 -3
View File
@@ -1,8 +1,10 @@
import type { Config } from "@react-router/dev/config";
import { joinUrlPath } from "@plane/utils";
const basePath = joinUrlPath(process.env.VITE_SPACE_BASE_PATH ?? "", "/") ?? "/";
export default {
appDirectory: "app",
basename: process.env.NEXT_PUBLIC_SPACE_BASE_PATH,
// Space runs as a client-side app; build a static client bundle only
ssr: false,
basename: basePath,
ssr: true,
} satisfies Config;
-76
View File
@@ -1,76 +0,0 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import compression from "compression";
import dotenv from "dotenv";
import express from "express";
import morgan from "morgan";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, ".env") });
const BUILD_PATH = "./build/server/index.js";
const DEVELOPMENT = process.env.NODE_ENV !== "production";
// Derive the port from NEXT_PUBLIC_SPACE_BASE_URL when available, otherwise
// default to http://localhost:3002 and fall back to PORT env if explicitly set.
const DEFAULT_BASE_URL = "http://localhost:3002";
const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || DEFAULT_BASE_URL;
let parsedBaseUrl;
try {
parsedBaseUrl = new URL(SPACE_BASE_URL);
} catch {
parsedBaseUrl = new URL(DEFAULT_BASE_URL);
}
const PORT = Number.parseInt(parsedBaseUrl.port, 10);
async function start() {
const app = express();
app.use(compression());
app.disable("x-powered-by");
if (DEVELOPMENT) {
console.log("Starting development server");
const vite = await import("vite").then((vite) =>
vite.createServer({
server: { middlewareMode: true },
appType: "custom",
})
);
app.use(vite.middlewares);
app.use(async (req, res, next) => {
try {
const source = await vite.ssrLoadModule("./server/app.ts");
return source.app(req, res, next);
} catch (error) {
if (error instanceof Error) {
vite.ssrFixStacktrace(error);
}
next(error);
}
});
} else {
console.log("Starting production server");
app.use("/assets", express.static("build/client/assets", { immutable: true, maxAge: "1y" }));
app.use(morgan("tiny"));
app.use(express.static("build/client", { maxAge: "1h" }));
app.use(await import(BUILD_PATH).then((mod) => mod.app));
}
app.listen(PORT, () => {
const origin = `${parsedBaseUrl.protocol}//${parsedBaseUrl.hostname}:${PORT}`;
console.log(`Server is running on ${origin}`);
});
}
start().catch((error) => {
console.error(error);
process.exit(1);
});
-46
View File
@@ -1,46 +0,0 @@
import "react-router";
import { createRequestHandler } from "@react-router/express";
import express from "express";
import type { Express } from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
const NEXT_PUBLIC_API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
? process.env.NEXT_PUBLIC_API_BASE_URL.replace(/\/$/, "")
: "http://127.0.0.1:8000";
const NEXT_PUBLIC_API_BASE_PATH = process.env.NEXT_PUBLIC_API_BASE_PATH
? process.env.NEXT_PUBLIC_API_BASE_PATH.replace(/\/+$/, "")
: "/api";
const NORMALIZED_API_BASE_PATH = NEXT_PUBLIC_API_BASE_PATH.startsWith("/")
? NEXT_PUBLIC_API_BASE_PATH
: `/${NEXT_PUBLIC_API_BASE_PATH}`;
const NEXT_PUBLIC_SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH
? process.env.NEXT_PUBLIC_SPACE_BASE_PATH.replace(/\/$/, "")
: "/";
export const app: Express = express();
// Ensure proxy-aware hostname/URL handling (e.g., X-Forwarded-Host/Proto)
// so generated URLs/redirects reflect the public host when behind Nginx.
// See related fix in Remix Express adapter.
app.set("trust proxy", true);
app.use(
"/api",
createProxyMiddleware({
target: NEXT_PUBLIC_API_BASE_URL,
changeOrigin: true,
secure: false,
pathRewrite: (path: string) =>
NORMALIZED_API_BASE_PATH === "/api" ? path : path.replace(/^\/api/, NORMALIZED_API_BASE_PATH),
})
);
const router = express.Router();
router.use(
createRequestHandler({
build: () => import("virtual:react-router/server-build"),
})
);
app.use(NEXT_PUBLIC_SPACE_BASE_PATH, router);
+26 -50
View File
@@ -4,56 +4,32 @@ import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { joinUrlPath } from "@plane/utils";
const PUBLIC_ENV_KEYS = [
"NEXT_PUBLIC_API_BASE_URL",
"NEXT_PUBLIC_API_BASE_PATH",
"NEXT_PUBLIC_ADMIN_BASE_URL",
"NEXT_PUBLIC_ADMIN_BASE_PATH",
"NEXT_PUBLIC_SPACE_BASE_URL",
"NEXT_PUBLIC_SPACE_BASE_PATH",
"NEXT_PUBLIC_LIVE_BASE_URL",
"NEXT_PUBLIC_LIVE_BASE_PATH",
"NEXT_PUBLIC_WEB_BASE_URL",
"NEXT_PUBLIC_WEB_BASE_PATH",
"NEXT_PUBLIC_WEBSITE_URL",
"NEXT_PUBLIC_SUPPORT_EMAIL",
];
// Expose only vars starting with VITE_
const viteEnv = Object.keys(process.env)
.filter((k) => k.startsWith("VITE_"))
.reduce<Record<string, string>>((a, k) => {
a[k] = process.env[k] ?? "";
return a;
}, {});
const publicEnv = PUBLIC_ENV_KEYS.reduce<Record<string, string>>((acc, key) => {
acc[key] = process.env[key] ?? "";
return acc;
}, {});
const basePath = joinUrlPath(process.env.VITE_SPACE_BASE_PATH ?? "", "/") ?? "/";
export default defineConfig(({ isSsrBuild }) => {
// Only produce an SSR bundle when explicitly enabled.
// For static deployments (default), we skip the server build entirely.
const enableSsrBuild = process.env.SPACE_ENABLE_SSR_BUILD === "true";
const basePath = joinUrlPath(process.env.NEXT_PUBLIC_SPACE_BASE_PATH ?? "", "/") ?? "/";
return {
base: basePath,
define: {
"process.env": JSON.stringify(publicEnv),
export default defineConfig(() => ({
base: basePath,
define: {
"process.env": JSON.stringify(viteEnv),
},
build: {
assetsInlineLimit: 0,
},
plugins: [reactRouter(), tsconfigPaths({ projects: [path.resolve(__dirname, "tsconfig.json")] })],
resolve: {
alias: {
// Next.js compatibility shims used within space
"next/image": path.resolve(__dirname, "app/compat/next/image.tsx"),
"next/link": path.resolve(__dirname, "app/compat/next/link.tsx"),
"next/navigation": path.resolve(__dirname, "app/compat/next/navigation.ts"),
},
build: {
assetsInlineLimit: 0,
rollupOptions:
isSsrBuild && enableSsrBuild
? {
input: path.resolve(__dirname, "server/app.ts"),
}
: undefined,
},
plugins: [reactRouter(), tsconfigPaths({ projects: [path.resolve(__dirname, "tsconfig.json")] })],
resolve: {
alias: {
// Next.js compatibility shims used within space
"next/image": path.resolve(__dirname, "app/compat/next/image.tsx"),
"next/link": path.resolve(__dirname, "app/compat/next/link.tsx"),
"next/navigation": path.resolve(__dirname, "app/compat/next/navigation.ts"),
},
dedupe: ["react", "react-dom"],
},
// No SSR-specific overrides needed; alias resolves to ESM build
};
});
dedupe: ["react", "react-dom"],
},
}));
+8 -8
View File
@@ -1,12 +1,12 @@
NEXT_PUBLIC_API_BASE_URL="http://localhost:8000"
VITE_API_BASE_URL="http://localhost:8000"
NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000"
VITE_WEB_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001"
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
VITE_ADMIN_BASE_URL="http://localhost:3001"
VITE_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002"
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
VITE_SPACE_BASE_URL="http://localhost:3002"
VITE_SPACE_BASE_PATH="/spaces"
NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100"
NEXT_PUBLIC_LIVE_BASE_PATH="/live"
VITE_LIVE_BASE_URL="http://localhost:3100"
VITE_LIVE_BASE_PATH="/live"
+17 -17
View File
@@ -39,31 +39,31 @@ RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG VITE_API_BASE_URL=""
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG VITE_ADMIN_BASE_URL=""
ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG VITE_ADMIN_BASE_PATH="/god-mode"
ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_LIVE_BASE_URL=""
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL
ARG VITE_LIVE_BASE_URL=""
ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live"
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH
ARG VITE_LIVE_BASE_PATH="/live"
ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG VITE_SPACE_BASE_URL=""
ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ARG VITE_SPACE_BASE_PATH="/spaces"
ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG VITE_WEB_BASE_URL=""
ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED=1
ENV TURBO_TELEMETRY_DISABLED=1
+5 -5
View File
@@ -9,11 +9,11 @@
// };
// const urls = [
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/instances/`,
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/`,
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/profile/`,
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/settings/`,
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/workspaces/?v=${Date.now()}`,
// `${process.env.VITE_API_BASE_URL}/api/instances/`,
// `${process.env.VITE_API_BASE_URL}/api/users/me/`,
// `${process.env.VITE_API_BASE_URL}/api/users/me/profile/`,
// `${process.env.VITE_API_BASE_URL}/api/users/me/settings/`,
// `${process.env.VITE_API_BASE_URL}/api/users/me/workspaces/?v=${Date.now()}`,
// ];
// urls.forEach((url) => preloadItem(url));
+34
View File
@@ -0,0 +1,34 @@
/* eslint-disable import/order */
import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
Sentry.init({
dsn: process.env.VITE_SENTRY_DSN,
environment: process.env.VITE_SENTRY_ENVIRONMENT,
sendDefaultPii: process.env.VITE_SENTRY_SEND_DEFAULT_PII ? process.env.VITE_SENTRY_SEND_DEFAULT_PII === "1" : false,
release: process.env.VITE_APP_VERSION,
tracesSampleRate: process.env.VITE_SENTRY_TRACES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_TRACES_SAMPLE_RATE)
: 0.1,
profilesSampleRate: process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE)
: 0.1,
replaysSessionSampleRate: process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE)
: 0.1,
replaysOnErrorSampleRate: process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE)
: 1.0,
integrations: [],
});
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});
+5 -5
View File
@@ -50,7 +50,7 @@ export const meta = () => [
];
export default function RootLayout({ children }: { children: React.ReactNode }) {
const isSessionRecorderEnabled = parseInt(process.env.NEXT_PUBLIC_ENABLE_SESSION_RECORDER || "0");
const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0");
return (
<html lang="en">
@@ -86,16 +86,16 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</div>
</AppProvider>
</body>
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && (
<Script defer data-domain={process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN} src="https://plausible.io/js/script.js" />
{process.env.VITE_PLAUSIBLE_DOMAIN && (
<Script defer data-domain={process.env.VITE_PLAUSIBLE_DOMAIN} src="https://plausible.io/js/script.js" />
)}
{!!isSessionRecorderEnabled && process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY && (
{!!isSessionRecorderEnabled && process.env.VITE_SESSION_RECORDER_KEY && (
<Script id="clarity-tracking">
{`(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];if(y){y.parentNode.insertBefore(t,y);}
})(window, document, "clarity", "script", "${process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY}");`}
})(window, document, "clarity", "script", "${process.env.VITE_SESSION_RECORDER_KEY}");`}
</Script>
)}
</html>
+16 -10
View File
@@ -1,4 +1,5 @@
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";
@@ -13,6 +14,7 @@ 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
@@ -34,7 +36,7 @@ export const links: LinksFunction = () => [
];
export function Layout({ children }: { children: ReactNode }) {
const isSessionRecorderEnabled = parseInt(process.env.NEXT_PUBLIC_ENABLE_SESSION_RECORDER || "0");
const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0");
return (
<html lang="en">
@@ -66,20 +68,16 @@ export function Layout({ children }: { children: ReactNode }) {
</div>
</AppProvider>
<Scripts />
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && (
<Script
defer
data-domain={process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
src="https://plausible.io/js/script.js"
/>
{process.env.VITE_PLAUSIBLE_DOMAIN && (
<Script defer data-domain={process.env.VITE_PLAUSIBLE_DOMAIN} src="https://plausible.io/js/script.js" />
)}
{!!isSessionRecorderEnabled && process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY && (
{!!isSessionRecorderEnabled && process.env.VITE_SESSION_RECORDER_KEY && (
<Script id="clarity-tracking">
{`(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];if(y){y.parentNode.insertBefore(t,y);}
})(window, document, "clarity", "script", "${process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY}");`}
})(window, document, "clarity", "script", "${process.env.VITE_SESSION_RECORDER_KEY}");`}
</Script>
)}
</body>
@@ -118,9 +116,17 @@ export default function Root() {
}
export function HydrateFallback() {
return null;
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />
</div>
);
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (error) {
Sentry.captureException(error);
}
return <CustomErrorComponent error={error} />;
}
@@ -30,7 +30,7 @@ export const SelectRepository: React.FC<Props> = (props) => {
const getKey = (pageIndex: number) => {
if (!workspaceSlug || !integration) return;
return `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/workspaces/${workspaceSlug}/workspace-integrations/${
return `${process.env.VITE_API_BASE_URL}/api/workspaces/${workspaceSlug}/workspace-integrations/${
integration.id
}/github-repositories/?page=${++pageIndex}`;
};
@@ -1,6 +1,7 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { xor } from "lodash-es";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
@@ -272,7 +273,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
if (isDraft) await draftIssues.updateIssue(workspaceSlug.toString(), data.id, payload);
else if (updateIssue) await updateIssue(payload.project_id, data.id, payload);
// check if we should add issue to cycle/module
// check if we should add/remove issue to/from cycle
if (
payload.cycle_id &&
payload.cycle_id !== "" &&
@@ -280,12 +281,30 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
) {
await addIssueToCycle(data as TBaseIssue, payload.cycle_id);
}
if (
payload.module_ids &&
payload.module_ids.length > 0 &&
(!payload.module_ids.includes(moduleId?.toString()) || storeType !== EIssuesStoreType.MODULE)
) {
await addIssueToModule(data as TBaseIssue, payload.module_ids);
if (data.cycle_id && !payload.cycle_id && data.project_id) {
await issues.removeIssueFromCycle(workspaceSlug.toString(), data.project_id, data.cycle_id, data.id);
fetchCycleDetails(workspaceSlug.toString(), data.project_id, data.cycle_id);
}
if (data.module_ids && payload.module_ids && data.project_id) {
const updatedModuleIds = xor(data.module_ids, payload.module_ids);
const modulesToAdd: string[] = [];
const modulesToRemove: string[] = [];
for (const moduleId of updatedModuleIds) {
if (data.module_ids.includes(moduleId)) {
modulesToRemove.push(moduleId);
} else {
modulesToAdd.push(moduleId);
}
}
await issues.changeModulesInIssue(
workspaceSlug.toString(),
data.project_id,
data.id,
modulesToAdd,
modulesToRemove
);
}
// add other property values
@@ -12,7 +12,7 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IProject, IUserLite, IWorkspace } from "@plane/types";
import { Loader, ToggleSwitch } from "@plane/ui";
// constants
import { PROJECT_MEMBERS } from "@/constants/fetch-keys";
import { PROJECT_DETAILS } from "@/constants/fetch-keys";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
@@ -64,7 +64,7 @@ export const ProjectSettingsMemberDefaults: React.FC<TProjectSettingsMemberDefau
const { reset, control } = useForm<IProject>({ defaultValues });
// fetching user members
useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(workspaceSlug, projectId) : null,
workspaceSlug && projectId ? PROJECT_DETAILS(workspaceSlug, projectId) : null,
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug, projectId) : null
);
+2 -2
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect } from "react";
// plane editor
import { getBinaryDataFromDocumentEditorHTMLString } from "@plane/editor";
import { convertBinaryDataToBase64String, getBinaryDataFromDocumentEditorHTMLString } from "@plane/editor";
import type { EditorRefApi } from "@plane/editor";
// plane types
import type { TDocumentPayload } from "@plane/types";
@@ -34,7 +34,7 @@ export const usePageFallback = (args: TArgs) => {
editor.setProviderDocument(latestDecodedDescription);
const { binary, html, json } = editor.getDocument();
if (!binary || !json) return;
const encodedBinary = Buffer.from(binary).toString("base64");
const encodedBinary = convertBinaryDataToBase64String(binary);
await updatePageDescription({
description_binary: encodedBinary,
@@ -38,7 +38,6 @@ const PROGRESS_CONFIG: Readonly<ProgressConfig> = {
easing: "ease",
trickle: true,
delay: 0,
isDisabled: import.meta.env.PROD, // Disable progress bar in production builds
} as const;
/**
+20 -15
View File
@@ -1,11 +1,11 @@
"use client";
import type { FC, ReactNode } from "react";
import { lazy, Suspense, useEffect } from "react";
import { lazy, Suspense, useEffect, useState } from "react";
import { PostHogProvider as PHProvider } from "@posthog/react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";
// constants
import { GROUP_WORKSPACE_TRACKER_EVENT } from "@plane/constants";
// helpers
@@ -29,18 +29,19 @@ const PostHogProvider: FC<IPosthogWrapper> = observer((props) => {
const { instance } = useInstance();
const { workspaceSlug, projectId } = useParams();
const { getWorkspaceRoleByWorkspaceSlug, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
// states
const [hydrated, setHydrated] = useState(false);
// derived values
const currentProjectRole = getProjectRoleByWorkspaceSlugAndProjectId(
workspaceSlug?.toString(),
projectId?.toString()
);
const currentWorkspaceRole = getWorkspaceRoleByWorkspaceSlug(workspaceSlug?.toString());
const is_telemetry_enabled = instance?.is_telemetry_enabled || false;
const is_posthog_enabled =
process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST && is_telemetry_enabled;
const is_posthog_enabled = process.env.VITE_POSTHOG_KEY && process.env.VITE_POSTHOG_HOST && is_telemetry_enabled;
useEffect(() => {
if (user) {
if (user && hydrated) {
// Identify sends an event, so you want may want to limit how often you call it
posthog?.identify(user.email, {
id: user.id,
@@ -57,19 +58,23 @@ const PostHogProvider: FC<IPosthogWrapper> = observer((props) => {
});
}
}
}, [user, currentProjectRole, currentWorkspaceRole, currentWorkspace]);
}, [user, currentProjectRole, currentWorkspaceRole, currentWorkspace, hydrated]);
useEffect(() => {
if (process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: "/ingest",
ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
debug: process.env.NEXT_PUBLIC_POSTHOG_DEBUG === "1", // Debug mode based on the environment variable
const posthogKey = process.env.VITE_POSTHOG_KEY;
const posthogHost = process.env.VITE_POSTHOG_HOST;
const isDebugMode = process.env.VITE_POSTHOG_DEBUG === "1";
if (posthogKey && posthogHost) {
posthog.init(posthogKey, {
api_host: posthogHost,
ui_host: posthogHost,
debug: isDebugMode, // Debug mode based on the environment variable
autocapture: false,
capture_pageview: false, // Disable automatic pageview capture, as we capture manually
capture_pageleave: true,
disable_session_recording: true,
});
setHydrated(true);
}
}, []);
@@ -86,16 +91,16 @@ const PostHogProvider: FC<IPosthogWrapper> = observer((props) => {
}
};
if (is_posthog_enabled) {
if (is_posthog_enabled && hydrated) {
document.addEventListener("click", clickHandler);
}
return () => {
document.removeEventListener("click", clickHandler);
};
}, [is_posthog_enabled]);
}, [hydrated, is_posthog_enabled]);
if (is_posthog_enabled)
if (is_posthog_enabled && hydrated)
return (
<PHProvider client={posthog}>
<Suspense>
-14
View File
@@ -1,14 +0,0 @@
import { next } from "@vercel/edge";
export default function middleware() {
return next({
headers: {
"Referrer-Policy": "origin-when-cross-origin",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-DNS-Prefetch-Control": "on",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
},
});
}
-5
View File
@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
-125
View File
@@ -1,125 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/** @type {import("next").NextConfig} */
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("dotenv").config({ path: ".env" });
const nextConfig = {
trailingSlash: true,
reactStrictMode: false,
swcMinify: true,
output: "standalone",
async headers() {
return [
{
source: "/(.*)?",
headers: [{ key: "X-Frame-Options", value: "DENY" }],
},
];
},
images: {
unoptimized: true,
},
experimental: {
optimizePackageImports: [
"lucide-react",
"date-fns",
"@headlessui/react",
"react-color",
"react-day-picker",
"react-dropzone",
"react-hook-form",
"lodash-es",
"clsx",
"tailwind-merge",
"@plane/constants",
"@plane/editor",
"@plane/hooks",
"@plane/i18n",
"@plane/logger",
"@plane/propel",
"@plane/services",
"@plane/shared-state",
"@plane/types",
"@plane/ui",
"@plane/utils",
],
},
async redirects() {
return [
{
source: "/:workspaceSlug/projects/:projectId/settings/:path*",
destination: "/:workspaceSlug/settings/projects/:projectId/:path*",
permanent: true,
},
{
source: "/:workspaceSlug/analytics",
destination: "/:workspaceSlug/analytics/overview",
permanent: true,
},
{
source: "/:workspaceSlug/settings/api-tokens",
destination: "/:workspaceSlug/settings/account/api-tokens",
permanent: true,
},
{
source: "/:workspaceSlug/projects/:projectId/inbox",
destination: "/:workspaceSlug/projects/:projectId/intake",
permanent: true,
},
{
source: "/accounts/sign-up",
destination: "/sign-up",
permanent: true,
},
{
source: "/sign-in",
destination: "/",
permanent: true,
},
{
source: "/signin",
destination: "/",
permanent: true,
},
{
source: "/register",
destination: "/sign-up",
permanent: true,
},
{
source: "/login",
destination: "/",
permanent: true,
},
];
},
async rewrites() {
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://app.posthog.com";
const rewrites = [
{
source: "/ingest/static/:path*",
destination: `${posthogHost}/static/:path*`,
},
{
source: "/ingest/:path*",
destination: `${posthogHost}/:path*`,
},
];
if (process.env.NEXT_PUBLIC_ADMIN_BASE_URL || process.env.NEXT_PUBLIC_ADMIN_BASE_PATH) {
const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || "";
const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
const GOD_MODE_BASE_URL = ADMIN_BASE_URL + ADMIN_BASE_PATH;
rewrites.push({
source: "/god-mode",
destination: `${GOD_MODE_BASE_URL}/`,
});
rewrites.push({
source: "/god-mode/:path*",
destination: `${GOD_MODE_BASE_URL}/:path*`,
});
}
return rewrites;
},
};
module.exports = nextConfig;
+12 -20
View File
@@ -5,9 +5,9 @@
"license": "AGPL-3.0",
"type": "module",
"scripts": {
"dev": "cross-env NODE_ENV=development PORT=3000 node server.mjs",
"dev": "react-router dev --port 3000",
"build": "react-router build",
"preview": "react-router build && cross-env NODE_ENV=production PORT=3000 node server.mjs",
"preview": "react-router build && serve -s build/client -l 3000",
"start": "serve -s build/client -l 3000",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf .react-router && rm -rf node_modules && rm -rf dist && rm -rf build",
"check:lint": "eslint . --max-warnings 821",
@@ -17,9 +17,9 @@
"fix:format": "prettier --write \"**/*.{ts,tsx,md,json,css,scss}\""
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "catalog:",
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "catalog:",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "catalog:",
"@atlaskit/pragmatic-drag-and-drop": "catalog:",
"@bprogress/core": "catalog:",
"@headlessui/react": "^1.7.19",
"@intercom/messenger-js-sdk": "^0.0.12",
@@ -34,31 +34,27 @@
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@popperjs/core": "^2.11.8",
"@posthog/react": "^1.4.0",
"@react-pdf/renderer": "^3.4.5",
"@react-router/express": "^7.9.3",
"@react-router/node": "^7.9.3",
"@sentry/react-router": "catalog:",
"@tanstack/react-table": "^8.21.3",
"axios": "catalog:",
"clsx": "^2.0.0",
"cmdk": "^1.0.0",
"comlink": "^4.4.1",
"compression": "^1.8.1",
"cross-env": "^7.0.3",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"emoji-picker-react": "^4.5.16",
"export-to-csv": "^1.4.0",
"express": "^5.1.0",
"http-proxy-middleware": "^3.0.5",
"isbot": "^5.1.31",
"lodash-es": "catalog:",
"lucide-react": "catalog:",
"mobx": "catalog:",
"mobx-react": "catalog:",
"mobx-utils": "catalog:",
"mobx": "catalog:",
"morgan": "^1.10.1",
"next-themes": "^0.2.1",
"posthog-js": "^1.131.3",
"posthog-js": "^1.255.1",
"react": "catalog:",
"react-color": "^2.19.3",
"react-dom": "catalog:",
"react-dropzone": "^14.2.3",
@@ -69,9 +65,8 @@
"react-masonry-component": "^6.3.0",
"react-pdf-html": "^2.1.2",
"react-popper": "^2.3.0",
"react-router-dom": "^7.9.1",
"react-router": "^7.9.1",
"react": "catalog:",
"react-router-dom": "^7.9.1",
"recharts": "^2.12.7",
"serve": "14.2.5",
"smooth-scroll-into-view-if-needed": "^2.0.2",
@@ -85,17 +80,14 @@
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@react-router/dev": "^7.9.1",
"@types/compression": "^1.8.1",
"@types/express": "4.17.23",
"@types/lodash-es": "catalog:",
"@types/morgan": "^1.9.10",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-color": "^3.0.6",
"@types/react-dom": "catalog:",
"@types/react": "catalog:",
"prettier": "^3.2.5",
"typescript": "catalog:",
"vite-tsconfig-paths": "^5.1.4",
"vite": "7.1.7"
"vite": "catalog:",
"vite-tsconfig-paths": "^5.1.4"
}
}
-1
View File
@@ -2,7 +2,6 @@ import type { Config } from "@react-router/dev/config";
export default {
appDirectory: "app",
basename: process.env.NEXT_PUBLIC_WEB_BASE_PATH,
// Web runs as a client-side app; build a static client bundle only
ssr: false,
} satisfies Config;
-77
View File
@@ -1,77 +0,0 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import compression from "compression";
import dotenv from "dotenv";
import express from "express";
import morgan from "morgan";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, ".env") });
const BUILD_PATH = "./build/server/index.js";
const DEVELOPMENT = process.env.NODE_ENV !== "production";
// Derive the port from NEXT_PUBLIC_WEB_BASE_URL when available, otherwise
// default to http://localhost:3000 and fall back to PORT env if explicitly set.
const DEFAULT_BASE_URL = "http://localhost:3000";
const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || DEFAULT_BASE_URL;
let parsedBaseUrl;
try {
parsedBaseUrl = new URL(WEB_BASE_URL);
} catch {
parsedBaseUrl = new URL(DEFAULT_BASE_URL);
}
const PORT = Number.parseInt(parsedBaseUrl.port, 10);
async function start() {
const app = express();
app.use(compression());
app.disable("x-powered-by");
if (DEVELOPMENT) {
console.log("Starting development server");
const vite = await import("vite").then((vite) =>
vite.createServer({
server: { middlewareMode: true },
appType: "custom",
})
);
app.use(vite.middlewares);
app.use(async (req, res, next) => {
try {
const source = await vite.ssrLoadModule("./server/app.ts");
return source.app(req, res, next);
} catch (error) {
if (error instanceof Error) {
vite.ssrFixStacktrace(error);
}
next(error);
}
});
} else {
console.log("Starting production server");
app.use("/assets", express.static("build/client/assets", { immutable: true, maxAge: "1y" }));
app.use(morgan("tiny"));
app.use(express.static("build/client", { maxAge: "1h" }));
app.use(await import(BUILD_PATH).then((mod) => mod.app));
}
app.listen(PORT, () => {
const origin = `${parsedBaseUrl.protocol}//${parsedBaseUrl.hostname}:${PORT}`;
console.log(`Server is running on ${origin}`);
});
}
start().catch((error) => {
console.error(error);
process.exit(1);
});
-47
View File
@@ -1,47 +0,0 @@
import "react-router";
import { createRequestHandler } from "@react-router/express";
import express from "express";
import type { Express } from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
import type { ServerBuild } from "react-router";
const NEXT_PUBLIC_API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
? process.env.NEXT_PUBLIC_API_BASE_URL.replace(/\/$/, "")
: "http://127.0.0.1:8000";
const NEXT_PUBLIC_API_BASE_PATH = process.env.NEXT_PUBLIC_API_BASE_PATH
? process.env.NEXT_PUBLIC_API_BASE_PATH.replace(/\/+$/, "")
: "/api";
const NORMALIZED_API_BASE_PATH = NEXT_PUBLIC_API_BASE_PATH.startsWith("/")
? NEXT_PUBLIC_API_BASE_PATH
: `/${NEXT_PUBLIC_API_BASE_PATH}`;
const NEXT_PUBLIC_WEB_BASE_PATH = process.env.NEXT_PUBLIC_WEB_BASE_PATH
? process.env.NEXT_PUBLIC_WEB_BASE_PATH.replace(/\/$/, "")
: "/";
export const app: Express = express();
// Ensure proxy-aware hostname/URL handling (e.g., X-Forwarded-Host/Proto)
// so generated URLs/redirects reflect the public host when behind Nginx.
// See related fix in Remix Express adapter.
app.set("trust proxy", true);
app.use(
"/api",
createProxyMiddleware({
target: NEXT_PUBLIC_API_BASE_URL,
changeOrigin: true,
secure: false,
pathRewrite: (path: string) =>
NORMALIZED_API_BASE_PATH === "/api" ? path : path.replace(/^\/api/, NORMALIZED_API_BASE_PATH),
})
);
const router = express.Router();
router.use(
createRequestHandler({
build: () => import("virtual:react-router/server-build") as Promise<ServerBuild>,
})
);
app.use(NEXT_PUBLIC_WEB_BASE_PATH, router);
+26 -60
View File
@@ -3,65 +3,31 @@ import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
const PUBLIC_ENV_KEYS = [
"ENABLE_EXPERIMENTAL_COREPACK",
"NEXT_PUBLIC_ADMIN_BASE_PATH",
"NEXT_PUBLIC_ADMIN_BASE_URL",
"NEXT_PUBLIC_API_BASE_PATH",
"NEXT_PUBLIC_API_BASE_URL",
"NEXT_PUBLIC_CRISP_ID",
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
"NEXT_PUBLIC_LIVE_BASE_PATH",
"NEXT_PUBLIC_LIVE_BASE_URL",
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
"NEXT_PUBLIC_POSTHOG_DEBUG",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
"NEXT_PUBLIC_SPACE_BASE_PATH",
"NEXT_PUBLIC_SPACE_BASE_URL",
"NEXT_PUBLIC_SUPPORT_EMAIL",
"NEXT_PUBLIC_WEB_BASE_PATH",
"NEXT_PUBLIC_WEB_BASE_URL",
"NEXT_PUBLIC_WEBSITE_URL",
"NODE_ENV",
];
// Expose only vars starting with VITE_
const viteEnv = Object.keys(process.env)
.filter((k) => k.startsWith("VITE_"))
.reduce<Record<string, string>>((a, k) => {
a[k] = process.env[k] ?? "";
return a;
}, {});
const publicEnv = PUBLIC_ENV_KEYS.reduce<Record<string, string>>((acc, key) => {
acc[key] = process.env[key] ?? "";
return acc;
}, {});
export default defineConfig(({ isSsrBuild }) => {
// Only produce an SSR bundle when explicitly enabled.
// For static deployments (default), we skip the server build entirely.
const enableSsrBuild = process.env.WEB_ENABLE_SSR_BUILD === "true";
return {
define: {
"process.env": JSON.stringify(publicEnv),
export default defineConfig(() => ({
define: {
"process.env": JSON.stringify(viteEnv),
},
build: {
assetsInlineLimit: 0,
},
plugins: [reactRouter(), tsconfigPaths({ projects: [path.resolve(__dirname, "tsconfig.json")] })],
resolve: {
alias: {
// Next.js compatibility shims used within web
"next/image": path.resolve(__dirname, "app/compat/next/image.tsx"),
"next/link": path.resolve(__dirname, "app/compat/next/link.tsx"),
"next/navigation": path.resolve(__dirname, "app/compat/next/navigation.ts"),
"next/script": path.resolve(__dirname, "app/compat/next/script.tsx"),
},
build: {
assetsInlineLimit: 0,
rollupOptions:
isSsrBuild && enableSsrBuild
? {
input: path.resolve(__dirname, "server/app.ts"),
}
: undefined,
},
plugins: [reactRouter(), tsconfigPaths({ projects: [path.resolve(__dirname, "tsconfig.json")] })],
resolve: {
alias: {
// Next.js compatibility shims used within web
"next/image": path.resolve(__dirname, "app/compat/next/image.tsx"),
"next/link": path.resolve(__dirname, "app/compat/next/link.tsx"),
"next/navigation": path.resolve(__dirname, "app/compat/next/navigation.ts"),
"next/script": path.resolve(__dirname, "app/compat/next/script.tsx"),
},
dedupe: ["react", "react-dom", "@headlessui/react"],
},
// No SSR-specific overrides needed; alias resolves to ESM build
};
});
dedupe: ["react", "react-dom", "@headlessui/react"],
},
// No SSR-specific overrides needed; alias resolves to ESM build
}));
+1
View File
@@ -141,6 +141,7 @@ function updateEnvFile() {
value=$(echo "$value" | sed 's/|/\\|/g')
sed -i '' "s|^$key=.*|$key=$value|g" "$file"
else
value=$(echo "$value" | sed 's/\//\\\//g')
sed -i "s/^$key=.*/$key=$value/g" "$file"
fi
fi
+2 -4
View File
@@ -19,7 +19,7 @@
"devDependencies": {
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.6.14",
"turbo": "^2.5.8"
"turbo": "2.6.1"
},
"pnpm": {
"overrides": {
@@ -34,19 +34,17 @@
"prosemirror-view": "1.40.0",
"@types/express": "4.17.23",
"typescript": "catalog:",
"sharp": "catalog:",
"vite": "catalog:"
},
"onlyBuiltDependencies": [
"@swc/core",
"core-js",
"esbuild",
"sharp",
"turbo",
"unrs-resolver"
]
},
"packageManager": "pnpm@10.12.1",
"packageManager": "pnpm@10.21.0",
"engines": {
"node": ">=22.18.0"
}
+12 -12
View File
@@ -1,26 +1,26 @@
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
export const API_BASE_PATH = process.env.NEXT_PUBLIC_API_BASE_PATH || "";
export const API_BASE_URL = process.env.VITE_API_BASE_URL || "";
export const API_BASE_PATH = process.env.VITE_API_BASE_PATH || "";
export const API_URL = encodeURI(`${API_BASE_URL}${API_BASE_PATH}`);
// God Mode Admin App Base Url
export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || "";
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
export const ADMIN_BASE_URL = process.env.VITE_ADMIN_BASE_URL || "";
export const ADMIN_BASE_PATH = process.env.VITE_ADMIN_BASE_PATH || "";
export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}`);
// Publish App Base Url
export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || "";
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
export const SPACE_BASE_URL = process.env.VITE_SPACE_BASE_URL || "";
export const SPACE_BASE_PATH = process.env.VITE_SPACE_BASE_PATH || "";
export const SITES_URL = encodeURI(`${SPACE_BASE_URL}${SPACE_BASE_PATH}`);
// Live App Base Url
export const LIVE_BASE_URL = process.env.NEXT_PUBLIC_LIVE_BASE_URL || "";
export const LIVE_BASE_PATH = process.env.NEXT_PUBLIC_LIVE_BASE_PATH || "";
export const LIVE_BASE_URL = process.env.VITE_LIVE_BASE_URL || "";
export const LIVE_BASE_PATH = process.env.VITE_LIVE_BASE_PATH || "";
export const LIVE_URL = encodeURI(`${LIVE_BASE_URL}${LIVE_BASE_PATH}`);
// Web App Base Url
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
export const WEB_BASE_PATH = process.env.NEXT_PUBLIC_WEB_BASE_PATH || "";
export const WEB_BASE_URL = process.env.VITE_WEB_BASE_URL || "";
export const WEB_BASE_PATH = process.env.VITE_WEB_BASE_PATH || "";
export const WEB_URL = encodeURI(`${WEB_BASE_URL}${WEB_BASE_PATH}`);
// plane website url
export const WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL || "https://plane.so";
export const WEBSITE_URL = process.env.VITE_WEBSITE_URL || "https://plane.so";
// support email
export const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "support@plane.so";
export const SUPPORT_EMAIL = process.env.VITE_SUPPORT_EMAIL || "support@plane.so";
// marketing links
export const MARKETING_PRICING_PAGE_LINK = "https://plane.so/pricing";
export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact";
+1
View File
@@ -49,6 +49,7 @@
"@tiptap/core": "catalog:",
"@tiptap/extension-blockquote": "^2.22.3",
"@tiptap/extension-character-count": "^2.22.3",
"buffer": "^6.0.3",
"@tiptap/extension-collaboration": "^2.22.3",
"@tiptap/extension-emoji": "^2.22.3",
"@tiptap/extension-image": "^2.22.3",
@@ -121,6 +121,7 @@ export const EmojisListDropdown = forwardRef<EmojiListRef, EmojisListDropdownPro
/>
<div
ref={dropdownContainerRef}
id="emojis-picker"
className={cn(
"relative max-h-80 w-[14rem] overflow-y-auto rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg space-y-2 opacity-0 invisible transition-opacity",
{
@@ -79,6 +79,7 @@ export const pasteRegex = /:([a-zA-Z0-9_+-]+):/g;
export const Emoji = Node.create<EmojiOptions, EmojiStorage>({
name: "emoji",
priority: 1001,
inline: true,
@@ -1,3 +1,4 @@
import { Buffer } from "buffer";
import { getSchema } from "@tiptap/core";
import { generateHTML, generateJSON } from "@tiptap/html";
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
+7
View File
@@ -19,6 +19,13 @@ export const CoreEditorProps = (props: TArgs): EditorProps => {
handleDOMEvents: {
keydown: (_view, event) => {
// prevent default event listeners from firing when slash command is active
if (["Enter"].includes(event.key)) {
const emojisPicker = document.querySelector("#emojis-picker");
if (emojisPicker) {
return true;
}
}
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
const slashCommand = document.querySelector("#slash-command");
if (slashCommand) {
@@ -1,7 +1,7 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useArgs } from "storybook/preview-api";
import { ChevronDownIcon } from "../icons";
import { ChevronDownIcon } from "../icons/arrows/chevron-down";
import { Collapsible } from "./collapsible";
const meta = {
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Copy, Download, Edit, Share, Trash, Star, Archive } from "lucide-react";
import { ChevronRightIcon } from "../icons";
import { ChevronRightIcon } from "../icons/arrows/chevron-right";
import { ContextMenu } from "./context-menu";
// cannot use satisfies here because base-ui does not have portable types.
@@ -1,7 +1,7 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useArgs } from "storybook/preview-api";
import { CloseIcon } from "../icons";
import { CloseIcon } from "../icons/actions/close-icon";
import { Dialog, EDialogWidth } from "./root";
const meta = {
@@ -1,7 +1,7 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useArgs } from "storybook/preview-api";
import { CloseIcon } from "../icons";
import { CloseIcon } from "../icons/actions/close-icon";
import { Popover } from "./root";
// cannot use satifies here because base-ui does not have portable types.
+1 -1
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Settings, User, Bell } from "lucide-react";
import { HomeIcon } from "../icons";
import { HomeIcon } from "../icons/workspace/home-icon";
import { Tabs } from "./tabs";
type TabOption = {
@@ -6,7 +6,6 @@ import {
Strikethrough,
Code,
Link,
List,
ListOrdered,
Quote,
AlignLeft,
@@ -17,6 +16,7 @@ import {
Globe2,
Lock,
} from "lucide-react";
import { ListLayoutIcon } from "../icons/layouts/list-icon";
import { Toolbar } from "./toolbar";
const meta = {
@@ -51,8 +51,8 @@ export const Default: Story = {
<Toolbar.Item icon={Strikethrough} tooltip="Strikethrough" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={List} tooltip="Bullet List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" />
<Toolbar.Item icon={ListLayoutIcon} tooltip="Bullet ListLayoutIcon" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered ListLayoutIcon" />
<Toolbar.Item icon={Quote} tooltip="Quote" />
</Toolbar.Group>
<Toolbar.Group>
@@ -82,8 +82,8 @@ export const WithActiveStates: Story = {
<Toolbar.Item icon={Underline} tooltip="Underline" shortcut={["Cmd", "U"]} isActive />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={List} tooltip="Bullet List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" isActive />
<Toolbar.Item icon={ListLayoutIcon} tooltip="Bullet ListLayoutIcon" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered ListLayoutIcon" isActive />
<Toolbar.Item icon={Quote} tooltip="Quote" />
</Toolbar.Group>
<Toolbar.Group>
@@ -118,8 +118,8 @@ export const CommentToolbar: Story = {
<Toolbar.Item icon={Code} tooltip="Code" shortcut={["Cmd", "`"]} />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={List} tooltip="Bullet List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" />
<Toolbar.Item icon={ListLayoutIcon} tooltip="Bullet ListLayoutIcon" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered ListLayoutIcon" />
</Toolbar.Group>
</div>
<Toolbar.SubmitButton>Comment</Toolbar.SubmitButton>
+2 -1
View File
@@ -1,5 +1,6 @@
import * as React from "react";
import { LucideIcon } from "lucide-react";
import type { ISvgIcons } from "../icons";
import { Tooltip } from "../tooltip";
import { cn } from "../utils";
@@ -15,7 +16,7 @@ export interface ToolbarGroupProps extends React.HTMLAttributes<HTMLDivElement>
}
export interface ToolbarItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
icon: LucideIcon;
icon: LucideIcon | React.FC<ISvgIcons>;
isActive?: boolean;
tooltip?: string;
shortcut?: string[];
+700 -665
View File
File diff suppressed because it is too large Load Diff
+8 -10
View File
@@ -1,16 +1,17 @@
packages:
- apps/*
- packages/*
- "!apps/api"
- "!apps/proxy"
- apps/*
- packages/*
- "!apps/api"
- "!apps/proxy"
catalog:
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": 1.4.0
"@atlaskit/pragmatic-drag-and-drop-hitbox": 1.1.0
"@atlaskit/pragmatic-drag-and-drop": 1.7.4
"@bprogress/core": ^1.3.4
"@sentry/node": 10.5.0
"@sentry/profiling-node": 10.5.0
"@sentry/node": 10.24.0
"@sentry/profiling-node": 10.24.0
"@sentry/react-router": 10.24.0
axios: 1.12.0
mobx: 6.12.0
mobx-react: 9.1.1
@@ -18,8 +19,6 @@ catalog:
lodash-es: 4.17.21
"@types/lodash-es": 4.17.12
lucide-react: 0.469.0
next: 14.2.32
sharp: 0.33.5
swr: 2.2.4
react: 18.3.1
react-dom: 18.3.1
@@ -34,5 +33,4 @@ catalog:
"@tiptap/html": ^2.22.3
onlyBuiltDependencies:
- turbo
- sharp
- turbo
+18 -18
View File
@@ -2,29 +2,29 @@
"$schema": "https://turbo.build/schema.json",
"globalEnv": [
"NODE_ENV",
"NEXT_PUBLIC_API_BASE_URL",
"NEXT_PUBLIC_ADMIN_BASE_URL",
"NEXT_PUBLIC_ADMIN_BASE_PATH",
"NEXT_PUBLIC_SPACE_BASE_URL",
"NEXT_PUBLIC_SPACE_BASE_PATH",
"NEXT_PUBLIC_WEB_BASE_URL",
"NEXT_PUBLIC_LIVE_BASE_URL",
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
"NEXT_PUBLIC_CRISP_ID",
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG",
"NEXT_PUBLIC_SUPPORT_EMAIL",
"VITE_API_BASE_URL",
"VITE_ADMIN_BASE_URL",
"VITE_ADMIN_BASE_PATH",
"VITE_SPACE_BASE_URL",
"VITE_SPACE_BASE_PATH",
"VITE_WEB_BASE_URL",
"VITE_LIVE_BASE_URL",
"VITE_PLAUSIBLE_DOMAIN",
"VITE_CRISP_ID",
"VITE_ENABLE_SESSION_RECORDER",
"VITE_SESSION_RECORDER_KEY",
"VITE_EXTRA_IMAGE_DOMAINS",
"VITE_POSTHOG_KEY",
"VITE_POSTHOG_HOST",
"VITE_POSTHOG_DEBUG",
"VITE_SUPPORT_EMAIL",
"ENABLE_EXPERIMENTAL_COREPACK"
],
"globalDependencies": ["pnpm-lock.yaml", "pnpm-workspace.yaml", ".npmrc"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**", "build/**"]
"outputs": ["dist/**", "build/**"]
},
"build-storybook": {
"dependsOn": ["^build"],
@@ -41,7 +41,7 @@
},
"check:lint": {
"cache": false,
"inputs": ["**/*", "!**/.next/**", "!**/dist/**"]
"inputs": ["**/*", "!**/build/**", "!**/dist/**"]
},
"check:format": {
"cache": false