Compare commits

..

1 Commits

Author SHA1 Message Date
sriramveeraghanta 667fc7e3ab chore: update next path redirections with validation 2025-10-30 18:56:38 +05:30
1182 changed files with 6949 additions and 10508 deletions
+1 -1
View File
@@ -66,4 +66,4 @@ temp/
.react-router/
build/
node_modules/
README.md
README.md
-6
View File
@@ -102,12 +102,6 @@ dev-editor
storybook-static
CLAUDE.md
build/
.react-router/
AGENTS.md
build/
.react-router/
AGENTS.md
temp/
-5
View File
@@ -1,5 +0,0 @@
# React Router - https://github.com/remix-run/react-router-templates/blob/dc79b1a065f59f3bfd840d4ef75cc27689b611e6/default/.dockerignore
.react-router/
build/
node_modules/
README.md
@@ -1,7 +1,6 @@
module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
ignorePatterns: ["build/**", "dist/**", ".vite/**"],
rules: {
"no-duplicate-imports": "off",
"import/no-duplicates": ["error", { "prefer-inline": false }],
+69 -52
View File
@@ -1,86 +1,103 @@
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS base
WORKDIR /app
ENV TURBO_TELEMETRY_DISABLED=1
# Setup pnpm package manager with corepack and configure global bin directory for caching
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV CI=1
RUN corepack enable pnpm
# =========================================================================== #
RUN corepack enable
# *****************************************************************************
# STAGE 1: Build the project
# *****************************************************************************
FROM base AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
RUN pnpm add -g turbo@2.5.8
ARG TURBO_VERSION=2.5.6
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
COPY . .
# Create a pruned workspace for just the admin app
RUN turbo prune --scope=admin --docker
# =========================================================================== #
# *****************************************************************************
# STAGE 2: Install dependencies & build the project
# *****************************************************************************
FROM base AS installer
# Build in production mode; we still install dev deps explicitly below
ENV NODE_ENV=production
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm
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
# 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 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 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 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 NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED=1
ENV TURBO_TELEMETRY_DISABLED=1
RUN pnpm turbo run build --filter=admin
# *****************************************************************************
# STAGE 3: Copy the project and start it
# *****************************************************************************
FROM base AS runner
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer /app/apps/admin/.next/standalone ./
COPY --from=installer /app/apps/admin/.next/static ./apps/admin/.next/static
COPY --from=installer /app/apps/admin/public ./apps/admin/public
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
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 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 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 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
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
# Fetch dependencies to cache store, then install offline with dev deps
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
# Build only the admin package
RUN pnpm turbo run build --filter=admin
# =========================================================================== #
FROM nginx:1.27-alpine AS production
COPY apps/admin/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=installer /app/apps/admin/build/client /usr/share/nginx/html/god-mode
ENV NEXT_TELEMETRY_DISABLED=1
ENV TURBO_TELEMETRY_DISABLED=1
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 ["node", "apps/admin/server.js"]
+2 -2
View File
@@ -1,5 +1,5 @@
"use client";
import type { FC } from "react";
import { useForm } from "react-hook-form";
import { Lightbulb } from "lucide-react";
import { Button } from "@plane/propel/button";
@@ -17,7 +17,7 @@ type IInstanceAIForm = {
type AIFormValues = Record<TInstanceAIConfigurationKeys, string>;
export const InstanceAIForm: React.FC<IInstanceAIForm> = (props) => {
export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
const { config } = props;
// store
const { updateInstanceConfigurations } = useInstance();
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Artificial Intelligence Settings - God Mode",
};
export default function AILayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
+1 -4
View File
@@ -6,10 +6,9 @@ import { Loader } from "@plane/ui";
// hooks
import { useInstance } from "@/hooks/store";
// components
import type { Route } from "./+types/page";
import { InstanceAIForm } from "./form";
const InstanceAIPage = observer<React.FC<Route.ComponentProps>>(() => {
const InstanceAIPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig } = useInstance();
@@ -43,6 +42,4 @@ const InstanceAIPage = observer<React.FC<Route.ComponentProps>>(() => {
);
});
export const meta: Route.MetaFunction = () => [{ title: "Artificial Intelligence Settings - God Mode" }];
export default InstanceAIPage;
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Gitea Authentication - God Mode",
};
export default function GiteaAuthenticationLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
@@ -3,17 +3,18 @@
import { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import useSWR from "swr";
// plane internal packages
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// components
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
// hooks
import { useInstance } from "@/hooks/store";
// icons
import giteaLogo from "@/public/logos/gitea-logo.svg";
//local components
import type { Route } from "./+types/page";
import { InstanceGiteaConfigForm } from "./form";
const InstanceGiteaAuthenticationPage = observer(() => {
@@ -21,6 +22,8 @@ const InstanceGiteaAuthenticationPage = observer(() => {
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// theme
const { resolvedTheme } = useTheme();
// config
const enableGiteaConfig = formattedConfig?.IS_GITEA_ENABLED ?? "";
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
@@ -97,6 +100,5 @@ const InstanceGiteaAuthenticationPage = observer(() => {
</>
);
});
export const meta: Route.MetaFunction = () => [{ title: "Gitea Authentication - God Mode" }];
export default InstanceGiteaAuthenticationPage;
@@ -1,5 +1,6 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
@@ -28,7 +29,7 @@ type Props = {
type GithubConfigFormValues = Record<TInstanceGithubAuthenticationConfigurationKeys, string>;
export const InstanceGithubConfigForm: React.FC<Props> = (props) => {
export const InstanceGithubConfigForm: FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "GitHub Authentication - God Mode",
};
export default function GitHubAuthenticationLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
@@ -10,17 +10,16 @@ import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
import { resolveGeneralTheme } from "@plane/utils";
// components
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
// hooks
import { useInstance } from "@/hooks/store";
// icons
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
// local components
import type { Route } from "./+types/page";
import { InstanceGithubConfigForm } from "./form";
const InstanceGithubAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
const InstanceGithubAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
@@ -112,6 +111,4 @@ const InstanceGithubAuthenticationPage = observer<React.FC<Route.ComponentProps>
);
});
export const meta: Route.MetaFunction = () => [{ title: "GitHub Authentication - God Mode" }];
export default InstanceGithubAuthenticationPage;
@@ -1,3 +1,4 @@
import type { FC } from "react";
import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
@@ -24,7 +25,7 @@ type Props = {
type GitlabConfigFormValues = Record<TInstanceGitlabAuthenticationConfigurationKeys, string>;
export const InstanceGitlabConfigForm: React.FC<Props> = (props) => {
export const InstanceGitlabConfigForm: FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "GitLab Authentication - God Mode",
};
export default function GitlabAuthenticationLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
@@ -7,16 +7,15 @@ import useSWR from "swr";
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// components
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
// hooks
import { useInstance } from "@/hooks/store";
// icons
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
// local components
import type { Route } from "./+types/page";
import { InstanceGitlabConfigForm } from "./form";
const InstanceGitlabAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
const InstanceGitlabAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
@@ -100,6 +99,4 @@ const InstanceGitlabAuthenticationPage = observer<React.FC<Route.ComponentProps>
);
});
export const meta: Route.MetaFunction = () => [{ title: "GitLab Authentication - God Mode" }];
export default InstanceGitlabAuthenticationPage;
@@ -1,5 +1,5 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
@@ -27,7 +27,7 @@ type Props = {
type GoogleConfigFormValues = Record<TInstanceGoogleAuthenticationConfigurationKeys, string>;
export const InstanceGoogleConfigForm: React.FC<Props> = (props) => {
export const InstanceGoogleConfigForm: FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Google Authentication - God Mode",
};
export default function GoogleAuthenticationLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
@@ -7,16 +7,15 @@ import useSWR from "swr";
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// components
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
// hooks
import { useInstance } from "@/hooks/store";
// icons
import GoogleLogo from "@/public/logos/google-logo.svg";
// local components
import type { Route } from "./+types/page";
import { InstanceGoogleConfigForm } from "./form";
const InstanceGoogleAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
const InstanceGoogleAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
@@ -101,6 +100,4 @@ const InstanceGoogleAuthenticationPage = observer<React.FC<Route.ComponentProps>
);
});
export const meta: Route.MetaFunction = () => [{ title: "Google Authentication - God Mode" }];
export default InstanceGoogleAuthenticationPage;
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Authentication Settings - Plane Web",
};
export default function AuthenticationLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
@@ -12,9 +12,8 @@ import { cn } from "@plane/utils";
import { useInstance } from "@/hooks/store";
// plane admin components
import { AuthenticationModes } from "@/plane-admin/components/authentication";
import type { Route } from "./+types/page";
const InstanceAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
const InstanceAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
@@ -112,6 +111,4 @@ const InstanceAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() =
);
});
export const meta: Route.MetaFunction = () => [{ title: "Authentication Settings - Plane Web" }];
export default InstanceAuthenticationPage;
@@ -1,6 +1,7 @@
"use client";
import { useMemo, useState } from "react";
import type { FC } from "react";
import React, { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
// types
import { Button } from "@plane/propel/button";
@@ -30,7 +31,7 @@ const EMAIL_SECURITY_OPTIONS: { [key in TEmailSecurityKeys]: string } = {
NONE: "No email security",
};
export const InstanceEmailForm: React.FC<IInstanceEmailForm> = (props) => {
export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
const { config } = props;
// states
const [isSendTestEmailModalOpen, setIsSendTestEmailModalOpen] = useState(false);
@@ -0,0 +1,14 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
interface EmailLayoutProps {
children: ReactNode;
}
export const metadata: Metadata = {
title: "Email Settings - God Mode",
};
export default function EmailLayout({ children }: EmailLayoutProps) {
return <>{children}</>;
}
@@ -8,10 +8,9 @@ import { Loader, ToggleSwitch } from "@plane/ui";
// hooks
import { useInstance } from "@/hooks/store";
// components
import type { Route } from "./+types/page";
import { InstanceEmailForm } from "./email-config-form";
const InstanceEmailPage = observer<React.FC<Route.ComponentProps>>(() => {
const InstanceEmailPage: React.FC = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
@@ -92,6 +91,4 @@ const InstanceEmailPage = observer<React.FC<Route.ComponentProps>>(() => {
);
});
export const meta: Route.MetaFunction = () => [{ title: "Email Settings - God Mode" }];
export default InstanceEmailPage;
@@ -1,4 +1,5 @@
import { useEffect, useState, Fragment } from "react";
import type { FC } from "react";
import React, { useEffect, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
// plane imports
import { Button } from "@plane/propel/button";
@@ -19,7 +20,7 @@ enum ESendEmailSteps {
const instanceService = new InstanceService();
export const SendTestEmailModal: React.FC<Props> = (props) => {
export const SendTestEmailModal: FC<Props> = (props) => {
const { isOpen, handleClose } = props;
// state
@@ -61,10 +62,10 @@ export const SendTestEmailModal: React.FC<Props> = (props) => {
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={Fragment}
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
@@ -77,7 +78,7 @@ export const SendTestEmailModal: React.FC<Props> = (props) => {
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="my-10 flex justify-center p-4 text-center sm:p-0 md:my-20">
<Transition.Child
as={Fragment}
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
@@ -1,4 +1,5 @@
"use client";
import type { FC } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { Telescope } from "lucide-react";
@@ -19,7 +20,7 @@ export interface IGeneralConfigurationForm {
instanceAdmins: IInstanceAdmin[];
}
export const GeneralConfigurationForm: React.FC<IGeneralConfigurationForm> = observer((props) => {
export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer((props) => {
const { instance, instanceAdmins } = props;
// hooks
const { instanceConfigurations, updateInstanceInfo, updateInstanceConfigurations } = useInstance();
@@ -1,5 +1,6 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
@@ -13,7 +14,7 @@ type TIntercomConfig = {
isTelemetryEnabled: boolean;
};
export const IntercomConfig: React.FC<TIntercomConfig> = observer((props) => {
export const IntercomConfig: FC<TIntercomConfig> = observer((props) => {
const { isTelemetryEnabled } = props;
// hooks
const { instanceConfigurations, updateInstanceConfigurations, fetchInstanceConfigurations } = useInstance();
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "General Settings - God Mode",
};
export default function GeneralLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
@@ -3,7 +3,6 @@ import { observer } from "mobx-react";
// hooks
import { useInstance } from "@/hooks/store";
// components
import type { Route } from "./+types/page";
import { GeneralConfigurationForm } from "./form";
function GeneralPage() {
@@ -29,6 +28,4 @@ function GeneralPage() {
);
}
export const meta: Route.MetaFunction = () => [{ title: "General Settings - God Mode" }];
export default observer(GeneralPage);
+3 -4
View File
@@ -1,5 +1,6 @@
"use client";
import type { FC } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { Menu, Settings } from "lucide-react";
@@ -10,7 +11,7 @@ import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
// hooks
import { useTheme } from "@/hooks/store";
export const HamburgerToggle = observer(() => {
export const HamburgerToggle: FC = observer(() => {
const { isSidebarCollapsed, toggleSidebar } = useTheme();
return (
<div
@@ -22,7 +23,7 @@ export const HamburgerToggle = observer(() => {
);
});
export const AdminHeader = observer(() => {
export const AdminHeader: FC = observer(() => {
const pathName = usePathname();
const getHeaderTitle = (pathName: string) => {
@@ -43,8 +44,6 @@ export const AdminHeader = observer(() => {
return "GitHub";
case "gitlab":
return "GitLab";
case "gitea":
return "Gitea";
case "workspace":
return "Workspace";
case "create":
@@ -1,4 +1,5 @@
"use client";
import type { FC } from "react";
import { useForm } from "react-hook-form";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
@@ -14,7 +15,7 @@ type IInstanceImageConfigForm = {
type ImageConfigFormValues = Record<TInstanceImageConfigurationKeys, string>;
export const InstanceImageConfigForm: React.FC<IInstanceImageConfigForm> = (props) => {
export const InstanceImageConfigForm: FC<IInstanceImageConfigForm> = (props) => {
const { config } = props;
// store hooks
const { updateInstanceConfigurations } = useInstance();
@@ -0,0 +1,14 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
interface ImageLayoutProps {
children: ReactNode;
}
export const metadata: Metadata = {
title: "Images Settings - God Mode",
};
export default function ImageLayout({ children }: ImageLayoutProps) {
return <>{children}</>;
}
@@ -6,10 +6,9 @@ import { Loader } from "@plane/ui";
// hooks
import { useInstance } from "@/hooks/store";
// local
import type { Route } from "./+types/page";
import { InstanceImageConfigForm } from "./form";
const InstanceImagePage = observer<React.FC<Route.ComponentProps>>(() => {
const InstanceImagePage = observer(() => {
// store
const { formattedConfig, fetchInstanceConfigurations } = useInstance();
@@ -39,6 +38,4 @@ const InstanceImagePage = observer<React.FC<Route.ComponentProps>>(() => {
);
});
export const meta: Route.MetaFunction = () => [{ title: "Images Settings - God Mode" }];
export default InstanceImagePage;
+13 -9
View File
@@ -1,28 +1,34 @@
"use client";
import type { FC, ReactNode } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
import { Outlet } from "react-router";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { NewUserPopup } from "@/components/new-user-popup";
// hooks
import { useUser } from "@/hooks/store";
// local components
import type { Route } from "./+types/layout";
import { AdminHeader } from "./header";
import { AdminSidebar } from "./sidebar";
const AdminLayout: React.FC<Route.ComponentProps> = () => {
type TAdminLayout = {
children: ReactNode;
};
const AdminLayout: FC<TAdminLayout> = (props) => {
const { children } = props;
// router
const { replace } = useRouter();
const router = useRouter();
// store hooks
const { isUserLoggedIn } = useUser();
useEffect(() => {
if (isUserLoggedIn === false) replace("/");
}, [replace, isUserLoggedIn]);
if (isUserLoggedIn === false) {
router.push("/");
}
}, [router, isUserLoggedIn]);
if (isUserLoggedIn === undefined) {
return (
@@ -38,9 +44,7 @@ const AdminLayout: React.FC<Route.ComponentProps> = () => {
<AdminSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
<AdminHeader />
<div className="h-full w-full overflow-hidden">
<Outlet />
</div>
<div className="h-full w-full overflow-hidden">{children}</div>
</main>
<NewUserPopup />
</div>
@@ -1,5 +1,6 @@
"use client";
import type { FC } from "react";
import { useState, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
@@ -34,7 +35,7 @@ const helpOptions = [
},
];
export const AdminSidebarHelpSection: React.FC = observer(() => {
export const AdminSidebarHelpSection: FC = observer(() => {
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// store
+2 -1
View File
@@ -1,5 +1,6 @@
"use client";
import type { FC } from "react";
import { useEffect, useRef } from "react";
import { observer } from "mobx-react";
// plane helpers
@@ -11,7 +12,7 @@ import { AdminSidebarDropdown } from "./sidebar-dropdown";
import { AdminSidebarHelpSection } from "./sidebar-help-section";
import { AdminSidebarMenu } from "./sidebar-menu";
export const AdminSidebar = observer(() => {
export const AdminSidebar: FC = observer(() => {
// store
const { isSidebarCollapsed, toggleSidebar } = useTheme();
@@ -2,10 +2,9 @@
import { observer } from "mobx-react";
// components
import type { Route } from "./+types/page";
import { WorkspaceCreateForm } from "./form";
const WorkspaceCreatePage = observer<React.FC<Route.ComponentProps>>(() => (
const WorkspaceCreatePage = observer(() => (
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Create a new workspace on this instance.</div>
@@ -19,6 +18,4 @@ const WorkspaceCreatePage = observer<React.FC<Route.ComponentProps>>(() => (
</div>
));
export const meta: Route.MetaFunction = () => [{ title: "Create Workspace - God Mode" }];
export default WorkspaceCreatePage;
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Workspace Management - God Mode",
};
export default function WorkspaceManagementLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
@@ -16,9 +16,8 @@ import { cn } from "@plane/utils";
import { WorkspaceListItem } from "@/components/workspace/list-item";
// hooks
import { useInstance, useWorkspace } from "@/hooks/store";
import type { Route } from "./+types/page";
const WorkspaceManagementPage = observer<React.FC<Route.ComponentProps>>(() => {
const WorkspaceManagementPage = observer(() => {
// states
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// store
@@ -168,6 +167,4 @@ const WorkspaceManagementPage = observer<React.FC<Route.ComponentProps>>(() => {
);
});
export const meta: Route.MetaFunction = () => [{ title: "Workspace Management - God Mode" }];
export default WorkspaceManagementPage;
+4 -5
View File
@@ -1,15 +1,14 @@
import { Info } from "lucide-react";
import type { FC } from "react";
import { Info, X } from "lucide-react";
// plane constants
import type { TAdminAuthErrorInfo } from "@plane/constants";
// icons
import { CloseIcon } from "@plane/propel/icons";
type TAuthBanner = {
bannerData: TAdminAuthErrorInfo | undefined;
handleBannerData?: (bannerData: TAdminAuthErrorInfo | undefined) => void;
};
export const AuthBanner: React.FC<TAuthBanner> = (props) => {
export const AuthBanner: FC<TAuthBanner> = (props) => {
const { bannerData, handleBannerData } = props;
if (!bannerData) return <></>;
@@ -23,7 +22,7 @@ export const AuthBanner: React.FC<TAuthBanner> = (props) => {
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80"
onClick={() => handleBannerData && handleBannerData(undefined)}
>
<CloseIcon className="w-4 h-4 flex-shrink-0" />
<X className="w-4 h-4 flex-shrink-0" />
</div>
</div>
);
+6 -5
View File
@@ -1,3 +1,4 @@
import type { ReactNode } from "react";
import Image from "next/image";
import Link from "next/link";
import { KeyRound, Mails } from "lucide-react";
@@ -7,16 +8,16 @@ import { SUPPORT_EMAIL, EAdminAuthErrorCodes } from "@plane/constants";
import type { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types";
import { resolveGeneralTheme } from "@plane/utils";
// components
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
import { GithubConfiguration } from "@/components/authentication/github-config";
import { GitlabConfiguration } from "@/components/authentication/gitlab-config";
import { GoogleConfiguration } from "@/components/authentication/google-config";
import { PasswordLoginConfiguration } from "@/components/authentication/password-config-switch";
// images
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
import GoogleLogo from "@/public/logos/google-logo.svg";
export enum EErrorAlertType {
BANNER_ALERT = "BANNER_ALERT",
@@ -27,7 +28,7 @@ export enum EErrorAlertType {
}
const errorCodeMessages: {
[key in EAdminAuthErrorCodes]: { title: string; message: (email?: string | undefined) => React.ReactNode };
[key in EAdminAuthErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
} = {
// admin
[EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST]: {
+2 -20
View File
@@ -1,27 +1,9 @@
"use client";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
import { Outlet } from "react-router";
// hooks
import { useUser } from "@/hooks/store/use-user";
function RootLayout() {
// router
const { replace } = useRouter();
// store hooks
const { isUserLoggedIn } = useUser();
useEffect(() => {
if (isUserLoggedIn === true) replace("/general");
}, [replace, isUserLoggedIn]);
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
<Outlet />
{children}
</div>
);
}
export default observer(RootLayout);
-6
View File
@@ -8,7 +8,6 @@ import { InstanceSetupForm } from "@/components/instance/setup-form";
// hooks
import { useInstance } from "@/hooks/store";
// components
import type { Route } from "./+types/page";
import { InstanceSignInForm } from "./sign-in-form";
const HomePage = () => {
@@ -39,8 +38,3 @@ const HomePage = () => {
};
export default observer(HomePage);
export const meta: Route.MetaFunction = () => [
{ title: "Admin Instance Setup & Sign-In" },
{ name: "description", content: "Configure your Plane instance or sign in to the admin portal." },
];
+2 -1
View File
@@ -1,5 +1,6 @@
"use client";
import type { FC } from "react";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { Eye, EyeOff } from "lucide-react";
@@ -45,7 +46,7 @@ const defaultFromData: TFormData = {
password: "",
};
export const InstanceSignInForm: React.FC = () => {
export const InstanceSignInForm: FC = () => {
// search params
const searchParams = useSearchParams();
const emailParam = searchParams.get("email") || undefined;
+6 -1
View File
@@ -1,9 +1,14 @@
import type { FC, ReactNode } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// hooks
import { useInstance } from "@/hooks/store";
export const InstanceProvider = observer<React.FC<React.PropsWithChildren>>((props) => {
type InstanceProviderProps = {
children: ReactNode;
};
export const InstanceProvider: FC<InstanceProviderProps> = observer((props) => {
const { children } = props;
// store hooks
const { fetchInstanceInfo } = useInstance();
@@ -2,25 +2,24 @@
import { ThemeProvider } from "next-themes";
import { SWRConfig } from "swr";
import { AppProgressBar } from "@/lib/b-progress";
import { InstanceProvider } from "./(all)/instance.provider";
import { StoreProvider } from "./(all)/store.provider";
import { ToastWithTheme } from "./(all)/toast";
import { UserProvider } from "./(all)/user.provider";
// providers
import { InstanceProvider } from "./instance.provider";
import { StoreProvider } from "./store.provider";
import { ToastWithTheme } from "./toast";
import { UserProvider } from "./user.provider";
const DEFAULT_SWR_CONFIG = {
refreshWhenHidden: false,
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnMount: true,
refreshInterval: 600_000,
refreshInterval: 600000,
errorRetryCount: 3,
};
export function AppProviders({ children }: { children: React.ReactNode }) {
export default function InstanceLayout({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<AppProgressBar />
<ToastWithTheme />
<SWRConfig value={DEFAULT_SWR_CONFIG}>
<StoreProvider>
+2 -1
View File
@@ -1,5 +1,6 @@
"use client";
import type { ReactNode } from "react";
import { createContext } from "react";
// plane admin store
import { RootStore } from "@/plane-admin/store/root.store";
@@ -23,7 +24,7 @@ function initializeStore(initialData = {}) {
}
export type StoreProviderProps = {
children: React.ReactNode;
children: ReactNode;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialState?: any;
};
+6 -1
View File
@@ -1,12 +1,17 @@
"use client";
import type { FC, ReactNode } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// hooks
import { useInstance, useTheme, useUser } from "@/hooks/store";
export const UserProvider = observer<React.FC<React.PropsWithChildren>>(({ children }) => {
interface IUserProvider {
children: ReactNode;
}
export const UserProvider: FC<IUserProvider> = observer(({ children }) => {
// hooks
const { isSidebarCollapsed, toggleSidebar } = useTheme();
const { currentUser, fetchCurrentUser } = useUser();
-35
View File
@@ -1,35 +0,0 @@
/**
* Ensures that a URL has a trailing slash while preserving query parameters and fragments
* @param url - The URL to process
* @returns The URL with a trailing slash added to the pathname (if not already present)
*/
export function ensureTrailingSlash(url: string): string {
try {
const fallbackBaseUrl =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "http://dummy.com";
// Handle relative URLs by creating a URL object with a fallback base URL
const urlObj = new URL(url, fallbackBaseUrl);
// Don't modify root path
if (urlObj.pathname === "/") {
return url;
}
// Add trailing slash if it doesn't exist
if (!urlObj.pathname.endsWith("/")) {
urlObj.pathname += "/";
}
// For relative URLs, return just the path + search + hash
if (url.startsWith("/")) {
return urlObj.pathname + urlObj.search + urlObj.hash;
}
// For absolute URLs, return the full URL
return urlObj.toString();
} catch (error) {
// If URL parsing fails, return the original URL
console.warn("Failed to parse URL for trailing slash enforcement:", url, error);
return url;
}
}
-14
View File
@@ -1,14 +0,0 @@
"use client";
import React from "react";
// Minimal shim so code using next/image compiles under React Router + Vite
// without changing call sites. It just renders a native img.
type NextImageProps = React.ImgHTMLAttributes<HTMLImageElement> & {
src: string;
};
const Image: React.FC<NextImageProps> = ({ src, alt = "", ...rest }) => <img src={src} alt={alt} {...rest} />;
export default Image;
-24
View File
@@ -1,24 +0,0 @@
"use client";
import React from "react";
import { Link as RRLink } from "react-router";
import { ensureTrailingSlash } from "./helper";
type NextLinkProps = React.ComponentProps<"a"> & {
href: string;
replace?: boolean;
prefetch?: boolean; // next.js prop, ignored
scroll?: boolean; // next.js prop, ignored
shallow?: boolean; // next.js prop, ignored
};
const Link: React.FC<NextLinkProps> = ({
href,
replace,
prefetch: _prefetch,
scroll: _scroll,
shallow: _shallow,
...rest
}) => <RRLink to={ensureTrailingSlash(href)} replace={replace} {...rest} />;
export default Link;
-34
View File
@@ -1,34 +0,0 @@
"use client";
import { useMemo } from "react";
import { useLocation, useNavigate, useSearchParams as useSearchParamsRR } from "react-router";
import { ensureTrailingSlash } from "./helper";
export function useRouter() {
const navigate = useNavigate();
return useMemo(
() => ({
push: (to: string) => navigate(ensureTrailingSlash(to)),
replace: (to: string) => navigate(ensureTrailingSlash(to), { replace: true }),
back: () => navigate(-1),
forward: () => navigate(1),
refresh: () => {
location.reload();
},
prefetch: async (_to: string) => {
// no-op in this shim
},
}),
[navigate]
);
}
export function usePathname(): string {
const { pathname } = useLocation();
return pathname;
}
export function useSearchParams(): URLSearchParams {
const [searchParams] = useSearchParamsRR();
return searchParams;
}
-34
View File
@@ -1,34 +0,0 @@
import React from "react";
import { Link } from "react-router";
// ui
import { Button } from "@plane/propel/button";
// images
import Image404 from "@/app/assets/images/404.svg?url";
const PageNotFound = () => (
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>
<div className="grid h-full place-items-center p-4">
<div className="space-y-8 text-center">
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
<img src={Image404} alt="404 - Page not found" className="h-full w-full object-contain" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
<p className="text-sm text-custom-text-200">
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
temporarily unavailable.
</p>
</div>
<Link to="/general/">
<span className="flex justify-center py-4">
<Button variant="neutral-primary" size="md">
Go to general settings
</Button>
</span>
</Link>
</div>
</div>
</div>
);
export default PageNotFound;
+9
View File
@@ -0,0 +1,9 @@
"use client";
export default function RootErrorPage() {
return (
<div>
<p>Something went wrong.</p>
</div>
);
}
+39
View File
@@ -0,0 +1,39 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
// plane imports
import { ADMIN_BASE_PATH } from "@plane/constants";
// styles
import "@/styles/globals.css";
export const metadata: Metadata = {
title: "Plane | Simple, extensible, open-source project management tool.",
description:
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
openGraph: {
title: "Plane | Simple, extensible, open-source project management tool.",
description:
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
url: "https://plane.so/",
},
keywords:
"software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration",
twitter: {
site: "@planepowers",
},
};
export default function RootLayout({ children }: { children: ReactNode }) {
const ASSET_PREFIX = ADMIN_BASE_PATH;
return (
<html lang="en">
<head>
<link rel="apple-touch-icon" sizes="180x180" href={`${ASSET_PREFIX}/favicon/apple-touch-icon.png`} />
<link rel="icon" type="image/png" sizes="32x32" href={`${ASSET_PREFIX}/favicon/favicon-32x32.png`} />
<link rel="icon" type="image/png" sizes="16x16" href={`${ASSET_PREFIX}/favicon/favicon-16x16.png`} />
<link rel="manifest" href={`${ASSET_PREFIX}/site.webmanifest.json`} />
<link rel="shortcut icon" href={`${ASSET_PREFIX}/favicon/favicon.ico`} />
</head>
<body className={`antialiased`}>{children}</body>
</html>
);
}
-70
View File
@@ -1,70 +0,0 @@
import type { ReactNode } from "react";
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 globalStyles from "@/styles/globals.css?url";
import type { Route } from "./+types/root";
import { AppProviders } from "./providers";
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.";
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: "stylesheet", href: globalStyles },
];
export function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body className="antialiased" suppressHydrationWarning>
<AppProviders>{children}</AppProviders>
<Scripts />
</body>
</html>
);
}
export const meta: Route.MetaFunction = () => [
{ title: APP_TITLE },
{ name: "description", content: APP_DESCRIPTION },
{ property: "og:title", content: APP_TITLE },
{ property: "og:description", content: APP_DESCRIPTION },
{ property: "og:url", content: "https://plane.so/" },
{
name: "keywords",
content:
"software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration",
},
{ name: "twitter:site", content: "@planepowers" },
];
export default function Root() {
return <Outlet />;
}
export function HydrateFallback() {
return null;
}
export function ErrorBoundary() {
return (
<div>
<p>Something went wrong.</p>
</div>
);
}
-21
View File
@@ -1,21 +0,0 @@
import { index, layout, route } from "@react-router/dev/routes";
import type { RouteConfig } from "@react-router/dev/routes";
export default [
layout("./(all)/(home)/layout.tsx", [index("./(all)/(home)/page.tsx")]),
layout("./(all)/(dashboard)/layout.tsx", [
route("general", "./(all)/(dashboard)/general/page.tsx"),
route("workspace", "./(all)/(dashboard)/workspace/page.tsx"),
route("workspace/create", "./(all)/(dashboard)/workspace/create/page.tsx"),
route("email", "./(all)/(dashboard)/email/page.tsx"),
route("authentication", "./(all)/(dashboard)/authentication/page.tsx"),
route("authentication/github", "./(all)/(dashboard)/authentication/github/page.tsx"),
route("authentication/gitlab", "./(all)/(dashboard)/authentication/gitlab/page.tsx"),
route("authentication/google", "./(all)/(dashboard)/authentication/google/page.tsx"),
route("authentication/gitea", "./(all)/(dashboard)/authentication/gitea/page.tsx"),
route("ai", "./(all)/(dashboard)/ai/page.tsx"),
route("image", "./(all)/(dashboard)/image/page.tsx"),
]),
// Catch-all route for 404 handling - must be last
route("*", "./components/404.tsx"),
] satisfies RouteConfig;
-5
View File
@@ -1,5 +0,0 @@
declare module "next/image" {
type Props = React.ComponentProps<"img"> & { src: string };
const Image: React.FC<Props>;
export default Image;
}
-12
View File
@@ -1,12 +0,0 @@
declare module "next/link" {
type Props = React.ComponentProps<"a"> & {
href: string;
replace?: boolean;
prefetch?: boolean;
scroll?: boolean;
shallow?: boolean;
};
const Link: React.FC<Props>;
export default Link;
}
-13
View File
@@ -1,13 +0,0 @@
declare module "next/navigation" {
export function useRouter(): {
push: (to: string) => void;
replace: (to: string) => void;
back: () => void;
forward: () => void;
refresh: () => void;
prefetch: (to: string) => Promise<void> | void;
};
export function usePathname(): string;
export function useSearchParams(): URLSearchParams;
}
-5
View File
@@ -1,5 +0,0 @@
declare module "virtual:react-router/server-build" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const build: any;
export default build;
}
@@ -10,13 +10,6 @@ import type {
} from "@plane/types";
import { resolveGeneralTheme } from "@plane/utils";
// components
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
import OIDCLogo from "@/app/assets/logos/oidc-logo.svg?url";
import SAMLLogo from "@/app/assets/logos/saml-logo.svg?url";
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
import { GiteaConfiguration } from "@/components/authentication/gitea-config";
@@ -27,6 +20,13 @@ import { PasswordLoginConfiguration } from "@/components/authentication/password
// plane admin components
import { UpgradeButton } from "@/plane-admin/components/common";
// assets
import giteaLogo from "@/public/logos/gitea-logo.svg";
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
import GoogleLogo from "@/public/logos/google-logo.svg";
import OIDCLogo from "@/public/logos/oidc-logo.svg";
import SAMLLogo from "@/public/logos/saml-logo.svg";
export type TAuthenticationModeProps = {
disabled: boolean;
@@ -107,7 +107,7 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
},
];
export const AuthenticationModes = observer<React.FC<TAuthenticationModeProps>>((props) => {
export const AuthenticationModes: React.FC<TAuthenticationModeProps> = observer((props) => {
const { disabled, updateConfig } = props;
// next-themes
const { resolvedTheme } = useTheme();
@@ -1,5 +1,6 @@
"use client";
import type { FC } from "react";
// helpers
import { cn } from "@plane/utils";
@@ -13,7 +14,7 @@ type Props = {
unavailable?: boolean;
};
export const AuthenticationMethodCard: React.FC<Props> = (props) => {
export const AuthenticationMethodCard: FC<Props> = (props) => {
const { name, description, icon, config, disabled = false, withBorder = true, unavailable = false } = props;
return (
@@ -1,5 +1,6 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
@@ -1,5 +1,6 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
@@ -7,7 +7,7 @@ import { Button } from "@plane/propel/button";
type Props = {
title: string;
description?: React.ReactNode;
image?: string;
image?: any;
primaryButton?: {
icon?: any;
text: string;
@@ -1,7 +1,8 @@
import Image from "next/image";
import { useTheme } from "next-themes";
import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url";
// assets
import LogoSpinnerDark from "@/public/images/logo-spinner-dark.gif";
import LogoSpinnerLight from "@/public/images/logo-spinner-light.gif";
export const LogoSpinner = () => {
const { resolvedTheme } = useTheme();
@@ -1,14 +1,15 @@
"use client";
import type { FC } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Button } from "@plane/propel/button";
// assets
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
import InstanceFailureDarkImage from "@/app/assets/instance/instance-failure-dark.svg?url";
import InstanceFailureImage from "@/app/assets/instance/instance-failure.svg?url";
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
import InstanceFailureImage from "@/public/instance/instance-failure.svg";
export const InstanceFailureView: React.FC = observer(() => {
export const InstanceFailureView: FC = observer(() => {
const { resolvedTheme } = useTheme();
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
@@ -1,12 +1,13 @@
"use client";
import type { FC } from "react";
import Image from "next/image";
import Link from "next/link";
import { Button } from "@plane/propel/button";
// assets
import PlaneTakeOffImage from "@/app/assets/images/plane-takeoff.png?url";
import PlaneTakeOffImage from "@/public/images/plane-takeoff.png";
export const InstanceNotReady: React.FC = () => (
export const InstanceNotReady: FC = () => (
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
@@ -1,8 +1,8 @@
import Image from "next/image";
import { useTheme } from "next-themes";
// assets
import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url";
import LogoSpinnerDark from "@/public/images/logo-spinner-dark.gif";
import LogoSpinnerLight from "@/public/images/logo-spinner-light.gif";
export const InstanceLoading = () => {
const { resolvedTheme } = useTheme();
@@ -1,5 +1,6 @@
"use client";
import type { FC } from "react";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
// icons
@@ -53,7 +54,7 @@ const defaultFromData: TFormData = {
is_telemetry_enabled: true,
};
export const InstanceSetupForm: React.FC = (props) => {
export const InstanceSetupForm: FC = (props) => {
const {} = props;
// search params
const searchParams = useSearchParams();
@@ -1,23 +1,24 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
import { useTheme as useNextTheme } from "next-themes";
import { useTheme as nextUseTheme } from "next-themes";
// ui
import { Button, getButtonStyling } from "@plane/propel/button";
import { resolveGeneralTheme } from "@plane/utils";
// hooks
import TakeoffIconDark from "@/app/assets/logos/takeoff-icon-dark.svg?url";
import TakeoffIconLight from "@/app/assets/logos/takeoff-icon-light.svg?url";
import { useTheme } from "@/hooks/store";
// icons
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
export const NewUserPopup = observer(() => {
export const NewUserPopup: React.FC = observer(() => {
// hooks
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
// theme
const { resolvedTheme } = useNextTheme();
const { resolvedTheme } = nextUseTheme();
if (!isNewUserPopup) return <></>;
return (
@@ -1,140 +0,0 @@
"use client";
import { useEffect, useRef } from "react";
import { BProgress } from "@bprogress/core";
import { useNavigation } from "react-router";
import "@bprogress/core/css";
/**
* Progress bar configuration options
*/
interface ProgressConfig {
/** Whether to show the loading spinner */
showSpinner: boolean;
/** Minimum progress percentage (0-1) */
minimum: number;
/** Animation speed in milliseconds */
speed: number;
/** Auto-increment speed in milliseconds */
trickleSpeed: number;
/** CSS easing function */
easing: string;
/** Enable auto-increment */
trickle: boolean;
/** Delay before showing progress bar in milliseconds */
delay: number;
/** Whether to disable the progress bar */
isDisabled?: boolean;
}
/**
* Configuration for the progress bar
*/
const PROGRESS_CONFIG: Readonly<ProgressConfig> = {
showSpinner: false,
minimum: 0.1,
speed: 400,
trickleSpeed: 800,
easing: "ease",
trickle: true,
delay: 0,
isDisabled: import.meta.env.PROD, // Disable progress bar in production builds
} as const;
/**
* Navigation Progress Bar Component
*
* Automatically displays a progress bar at the top of the page during React Router navigation.
* Integrates with React Router's useNavigation hook to monitor route changes.
*
* Note: Progress bar is disabled in production builds.
*
* @returns null - This component doesn't render any visible elements
*
* @example
* ```tsx
* function App() {
* return (
* <>
* <AppProgressBar />
* <Outlet />
* </>
* );
* }
* ```
*/
export function AppProgressBar(): null {
const navigation = useNavigation();
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const startedRef = useRef<boolean>(false);
// Initialize BProgress once on mount
useEffect(() => {
// Skip initialization in production builds
if (PROGRESS_CONFIG.isDisabled) {
return;
}
// Configure BProgress with our settings
BProgress.configure({
showSpinner: PROGRESS_CONFIG.showSpinner,
minimum: PROGRESS_CONFIG.minimum,
speed: PROGRESS_CONFIG.speed,
trickleSpeed: PROGRESS_CONFIG.trickleSpeed,
easing: PROGRESS_CONFIG.easing,
trickle: PROGRESS_CONFIG.trickle,
});
// Render the progress bar element in the DOM
BProgress.render(true);
// Cleanup on unmount
return () => {
if (BProgress.isStarted()) {
BProgress.done();
}
};
}, []);
// Handle navigation state changes
useEffect(() => {
// Skip navigation tracking in production builds
if (PROGRESS_CONFIG.isDisabled) {
return;
}
if (navigation.state === "idle") {
// Navigation complete - clear any pending timer
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
// Complete progress if it was started
if (startedRef.current) {
BProgress.done();
startedRef.current = false;
}
} else {
// Navigation in progress (loading or submitting)
// Only start if not already started and no timer pending
if (timerRef.current === null && !startedRef.current) {
timerRef.current = setTimeout((): void => {
if (!BProgress.isStarted()) {
BProgress.start();
startedRef.current = true;
}
timerRef.current = null;
}, PROGRESS_CONFIG.delay);
}
}
return () => {
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
}
};
}, [navigation.state]);
return null;
}
-1
View File
@@ -1 +0,0 @@
export * from "./AppProgressBar";
-1
View File
@@ -1 +0,0 @@
export {};
-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
@@ -0,0 +1,5 @@
/// <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.
+29
View File
@@ -0,0 +1,29 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
trailingSlash: true,
reactStrictMode: false,
swcMinify: true,
output: "standalone",
images: {
unoptimized: true,
},
basePath: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "",
experimental: {
optimizePackageImports: [
"@plane/constants",
"@plane/editor",
"@plane/hooks",
"@plane/i18n",
"@plane/logger",
"@plane/propel",
"@plane/services",
"@plane/shared-state",
"@plane/types",
"@plane/ui",
"@plane/utils",
],
},
};
module.exports = nextConfig;
-29
View File
@@ -1,29 +0,0 @@
worker_processes 4;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
set_real_ip_from 0.0.0.0/0;
real_ip_recursive on;
real_ip_header X-Forward-For;
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
access_log /dev/stdout;
error_log /dev/stderr;
server {
listen 3000;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /god-mode/index.html;
}
}
}
+11 -30
View File
@@ -4,21 +4,19 @@
"version": "1.1.0",
"license": "AGPL-3.0",
"private": true,
"type": "module",
"scripts": {
"dev": "cross-env NODE_ENV=development PORT=3001 node server.mjs",
"build": "react-router build",
"preview": "react-router build && cross-env NODE_ENV=production PORT=3001 node server.mjs",
"start": "serve -s build/client -l 3001",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist && rm -rf build",
"dev": "next dev --port 3001",
"build": "next build",
"preview": "next build && next start",
"start": "next start",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist",
"check:lint": "eslint . --max-warnings 19",
"check:types": "react-router typegen && tsc --noEmit",
"check:types": "tsc --noEmit",
"check:format": "prettier --check \"**/*.{ts,tsx,md,json,css,scss}\"",
"fix:lint": "eslint . --fix",
"fix:format": "prettier --write \"**/*.{ts,tsx,md,json,css,scss}\""
},
"dependencies": {
"@bprogress/core": "catalog:",
"@headlessui/react": "^1.7.19",
"@plane/constants": "workspace:*",
"@plane/hooks": "workspace:*",
@@ -27,30 +25,19 @@
"@plane/types": "workspace:*",
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@react-router/express": "^7.9.3",
"@react-router/node": "^7.9.3",
"@tanstack/react-virtual": "^3.13.12",
"@tanstack/virtual-core": "^3.13.12",
"@vercel/edge": "1.2.2",
"autoprefixer": "10.4.14",
"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": "catalog:",
"next-themes": "^0.2.1",
"postcss": "^8.4.49",
"react": "catalog:",
"react-dom": "catalog:",
"react-hook-form": "7.51.5",
"react-router": "^7.9.1",
"react-router-dom": "^7.9.1",
"serve": "14.2.5",
"sharp": "catalog:",
"swr": "catalog:",
"uuid": "catalog:"
},
@@ -58,16 +45,10 @@
"@plane/eslint-config": "workspace:*",
"@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-tsconfig-paths": "^5.1.4"
"typescript": "catalog:"
}
}

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before

Width:  |  Height:  |  Size: 466 B

After

Width:  |  Height:  |  Size: 466 B

Before

Width:  |  Height:  |  Size: 761 B

After

Width:  |  Height:  |  Size: 761 B

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 954 KiB

After

Width:  |  Height:  |  Size: 954 KiB

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 418 KiB

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 742 B

After

Width:  |  Height:  |  Size: 742 B

Some files were not shown because too many files have changed in this diff Show More