Compare commits

..

1 Commits

Author SHA1 Message Date
Vihar Kurama 7743e59fff fix: allow command palette search results to open in new tab 2025-10-23 12:17:19 +01:00
3261 changed files with 35459 additions and 55490 deletions
+1 -1
View File
@@ -66,4 +66,4 @@ temp/
.react-router/
build/
node_modules/
README.md
README.md
-48
View File
@@ -1,48 +0,0 @@
---
description: Guidelines for bash commands and tooling in the monorepo
applyTo: "**/*.sh"
---
# Bash & Tooling Instructions
This document outlines the standard tools and commands used in this monorepo.
## Package Manager
We use **pnpm** for package management.
- **Do not use `npm` or `yarn`.**
- Lockfile: `pnpm-lock.yaml`
- Workspace configuration: `pnpm-workspace.yaml`
### Common Commands
- Install dependencies: `pnpm install`
- Run a script in a specific package: `pnpm --filter <package_name> run <script>`
- Run a script in all packages: `pnpm -r run <script>`
## Monorepo Tooling
We use **Turbo** for build system orchestration.
- Configuration: `turbo.json`
## Project Structure
- `apps/`: Contains application services (admin, api, live, proxy, space, web).
- `packages/`: Contains shared packages and libraries.
- `deployments/`: Deployment configurations.
## Running Tests
- To run tests in a specific package (e.g., codemods):
```bash
cd packages/codemods
pnpm run test
```
- Or from root:
```bash
pnpm --filter @plane/codemods run test
```
## Docker
- Local development uses `docker-compose-local.yml`.
- Production/Staging uses `docker-compose.yml`.
@@ -1,129 +0,0 @@
---
description: Guidelines for using modern TypeScript features (v5.0-v5.8)
applyTo: "**/*.{ts,tsx,mts,cts}"
---
# TypeScript Coding Guidelines & Modern Features (v5.0 - v5.8)
When writing TypeScript code, prioritize using modern features and best practices introduced in recent versions (up to 5.8).
## Global Themes Across 5.x
1. **Standard decorators are here; legacy decorators are legacy.**
New TC39-compliant decorators landed in 5.0 and were extended in 5.2 (metadata). Old `experimentalDecorators`-style behavior is still supported but should be treated as legacy.
2. **Type system is more precise and less noisy.**
Major work went into narrowing, control flow analysis, error messages, and new helpers like `NoInfer`, inferred predicates, and better `undefined`/`never`/uninitialized checks.
3. **Module / runtime interop has been modernized.**
Options like `--moduleResolution bundler`, `--module nodenext`/`node18`, `--rewriteRelativeImportExtensions`, `--erasableSyntaxOnly`, and `--verbatimModuleSyntax` are about playing nicely with ESM, Node 18+/22+, direct TypeScript execution, and bundlers.
4. **The standard library keeps tracking modern JS.**
Support for new ES features (iterator helpers, `Object.groupBy`/`Map.groupBy`, new Set/ES2024 APIs) shows up as type declarations and sometimes extra checks (regex syntax checking, etc.).
When generating or refactoring code, prefer these newer idioms, and avoid patterns that conflict with updated checks.
## Modern Features to Utilize
### Type System & Inference
- **`const` Type Parameters (5.0)**: Use `const` type parameters for more precise literal inference.
```typescript
declare function names<const T extends string[]>(...names: T): void;
```
- **`@satisfies` Operator (5.0)**: Use `satisfies` to validate types without widening them.
- **Inferred Type Predicates (5.5)**: Allow TypeScript to infer type predicates for functions that filter arrays or check types, reducing the need for explicit `is` return types.
- **`NoInfer` Utility (5.4)**: Use `NoInfer<T>` to block inference for specific type arguments when you want them to be determined by other arguments.
- **Narrowing**:
- **Switch(true) (5.3)**: Utilize narrowing in `switch(true)` blocks.
- **Boolean Comparisons (5.3)**: Rely on narrowing from direct boolean comparisons.
- **Closures (5.4)**: Trust preserved narrowing in closures when variables aren't modified after the check.
- **Constant Indexed Access (5.5)**: Use constant indices to narrow object/array properties.
### Syntax & Control Flow
- **Decorators (5.0)**: Use standard ECMAScript decorators (Stage 3).
- **`using` Declarations (5.2)**: Use `using` for explicit resource management (Disposable pattern) instead of manual cleanup.
```typescript
using resource = new Resource();
```
- **Import Attributes (5.3/5.8)**: Use `with { type: "json" }` for import attributes. Avoid the deprecated `assert` syntax.
- **`switch` Exhaustiveness**: Rely on TypeScript's exhaustiveness checking in switch statements.
### Modules & Imports
- **`verbatimModuleSyntax` (5.0)**: Respect this flag by using `import type` explicitly when importing types to ensure they are erased during compilation.
- **Type-Only Imports with Extensions (5.2)**: You can use `.ts`, `.mts`, `.cts` extensions in `import type` statements.
- **`resolution-mode` (5.3)**: Use `import type { Type } from "mod" with { "resolution-mode": "import" }` if needed for specific module resolution contexts.
- **JSDoc `@import` (5.5)**: Use `@import` tags in JSDoc for cleaner type imports in JS files if working in a mixed codebase.
### Standard Library & Built-ins
- **Iterator Helpers (5.6)**: Use new iterator methods (map, filter, etc.) if targeting modern environments.
- **Set Methods (5.5)**: Utilize new `Set` methods like `union`, `intersection`, etc., when available.
- **`Object.groupBy` / `Map.groupBy` (5.4)**: Use these standard methods for grouping instead of external libraries like Lodash when appropriate.
- **`Promise.withResolvers` (5.7)**: Use `Promise.withResolvers()` for creating promises with exposed resolve/reject functions.
### Configuration & Tooling
- **`--moduleResolution bundler` (5.0)**: Assume this resolution strategy for modern web projects (Vite, Next.js, etc.).
- **`--erasableSyntaxOnly` (5.8)**: Be aware of this flag; avoid TypeScript-specific syntax that cannot be simply erased (like `enum`s or `namespaces`) if the project aims for maximum compatibility with tools like Node.js's `--strip-types`. Prefer `const` objects or unions over `enum`s if requested.
## Specific Coding Patterns
### Arrays & Collections
- Use **Copying Array Methods (5.2)** (`toSorted`, `toSpliced`, `with`) for immutable array operations.
- **TypedArrays (5.7)**: Be aware that TypedArrays are now generic over `ArrayBufferLike`.
### Classes
- **Parameter Decorators (5.0/5.2)**: Use modern standard decorators.
- **`super` Property Access (5.3)**: Avoid accessing instance fields via `super`.
### Error Handling
- **Checks for Never-Initialized Variables (5.7)**: Ensure variables are initialized before use to avoid new errors.
## Deprecations to Avoid
- Avoid `import ... assert` (use `with`).
- Avoid implicit `any` returns in `undefined`-returning functions (though 5.1 makes this easier, explicit is better).
- Avoid `enum`s if the project prefers erasable syntax (5.8).
## Version-Specific Highlights
### TypeScript 5.0
- **Decorators**: Use standard decorators unless `experimentalDecorators` is explicitly enabled.
- **`const` Type Parameters**: Use for literal inference.
- **Enums**: All enums are union enums.
- **Modules**: `--moduleResolution bundler` and `--verbatimModuleSyntax` are key for modern bundlers.
### TypeScript 5.1
- **Returns**: `undefined`-returning functions don't need explicit returns.
- **Getters/Setters**: Can have unrelated types with explicit annotations.
### TypeScript 5.2
- **Resource Management**: `using` declarations for `Symbol.dispose`.
- **Decorator Metadata**: Use `context.metadata` for design-time metadata.
### TypeScript 5.3
- **Import Attributes**: Use `with { type: "json" }`.
- **Switch(true)**: Narrowing works in `switch(true)`.
### TypeScript 5.4
- **Closures**: Narrowing preserved in closures if last assignment is before creation.
- **`NoInfer`**: Block inference for specific arguments.
- **Grouping**: `Object.groupBy` / `Map.groupBy`.
### TypeScript 5.5
- **Inferred Predicates**: Functions checking types often don't need explicit `is` return types.
- **Constant Index Access**: Better narrowing for constant keys.
- **Regex**: Syntax checking for regex literals.
### TypeScript 5.6
- **Truthiness Checks**: Errors on always-truthy/falsy conditions (e.g., `if (/regex/)`).
- **Iterator Helpers**: `.map`, `.filter` on iterators.
### TypeScript 5.7
- **Uninitialized Variables**: Stricter checks for never-initialized variables.
- **Relative Imports**: `--rewriteRelativeImportExtensions` for `.ts` imports in output.
- **ES2024**: Support for `Promise.withResolvers`, `Atomics.waitAsync`.
### TypeScript 5.8
- **Return Checks**: Granular checks for conditional returns.
- **Node Modules**: `--module node18` stable; `require()` of ESM allowed in `nodenext`.
- **Erasable Syntax**: `--erasableSyntaxOnly` forbids enums, namespaces, etc.
When generating code, always prefer the most modern, standard, and type-safe approach available in TypeScript 5.8.
@@ -49,8 +49,5 @@ jobs:
- name: Check Affected format
run: pnpm turbo run check:format --affected
- name: Check Affected types
run: pnpm turbo run check:types --affected
- name: Build Affected
run: pnpm turbo run build --affected
-9
View File
@@ -102,12 +102,3 @@ dev-editor
storybook-static
CLAUDE.md
build/
.react-router/
AGENTS.md
build/
.react-router/
AGENTS.md
temp/
-6
View File
@@ -1,6 +0,0 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5",
"plugins": ["@prettier/plugin-oxc"]
}
-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
+8 -8
View File
@@ -1,12 +1,12 @@
VITE_API_BASE_URL="http://localhost:8000"
NEXT_PUBLIC_API_BASE_URL="http://localhost:8000"
VITE_WEB_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000"
VITE_ADMIN_BASE_URL="http://localhost:3001"
VITE_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001"
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
VITE_SPACE_BASE_URL="http://localhost:3002"
VITE_SPACE_BASE_PATH="/spaces"
NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002"
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
VITE_LIVE_BASE_URL="http://localhost:3100"
VITE_LIVE_BASE_PATH="/live"
NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100"
NEXT_PUBLIC_LIVE_BASE_PATH="/live"
-2
View File
@@ -1,6 +1,4 @@
.next/*
.react-router/*
.vite/*
out/*
public/*
dist/*
@@ -1,8 +1,8 @@
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 }],
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"@typescript-eslint/no-import-type-side-effects": "error",
-2
View File
@@ -1,6 +1,4 @@
.next
.react-router
.vite
.vercel
.tubro
out/
+1 -2
View File
@@ -1,6 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5",
"plugins": ["@prettier/plugin-oxc"]
"trailingComma": "es5"
}
+75 -58
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
# Public envs required at build time (pick up via process.env)
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 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 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 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 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 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
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
# Fetch dependencies to cache store, then install offline with dev deps
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 CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store 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 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
ENV NEXT_TELEMETRY_DISABLED=1
ENV TURBO_TELEMETRY_DISABLED=1
# Build only the admin package
RUN pnpm turbo run build --filter=admin
# =========================================================================== #
# *****************************************************************************
# STAGE 3: Copy the project and start it
# *****************************************************************************
FROM base AS runner
WORKDIR /app
FROM nginx:1.27-alpine AS production
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
COPY apps/admin/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=installer /app/apps/admin/build/client /usr/share/nginx/html/god-mode
# 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
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"]
+1 -1
View File
@@ -8,7 +8,7 @@ COPY . .
RUN corepack enable pnpm && pnpm add -g turbo
RUN pnpm install
ENV VITE_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
EXPOSE 3000
+4 -2
View File
@@ -1,3 +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";
@@ -15,7 +17,7 @@ type IInstanceAIForm = {
type AIFormValues = Record<TInstanceAIConfigurationKeys, string>;
export function InstanceAIForm(props: IInstanceAIForm) {
export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
const { config } = props;
// store
const { updateInstanceConfigurations } = useInstance();
@@ -131,4 +133,4 @@ export function InstanceAIForm(props: IInstanceAIForm) {
</div>
</div>
);
}
};
@@ -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}</>;
}
+3 -4
View File
@@ -1,13 +1,14 @@
"use client";
import { observer } from "mobx-react";
import useSWR from "swr";
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(function InstanceAIPage(_props: Route.ComponentProps) {
const InstanceAIPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig } = useInstance();
@@ -41,6 +42,4 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
);
});
export const meta: Route.MetaFunction = () => [{ title: "Artificial Intelligence Settings - God Mode" }];
export default InstanceAIPage;
@@ -1,207 +0,0 @@
import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
import { useForm } from "react-hook-form";
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceGiteaAuthenticationConfigurationKeys } from "@plane/types";
import { Button, getButtonStyling } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { CodeBlock } from "@/components/common/code-block";
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import { ControllerInput } from "@/components/common/controller-input";
import type { TCopyField } from "@/components/common/copy-field";
import { CopyField } from "@/components/common/copy-field";
// hooks
import { useInstance } from "@/hooks/store";
type Props = {
config: IFormattedInstanceConfiguration;
};
type GiteaConfigFormValues = Record<TInstanceGiteaAuthenticationConfigurationKeys, string>;
export function InstanceGiteaConfigForm(props: Props) {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
// store hooks
const { updateInstanceConfigurations } = useInstance();
// form data
const {
handleSubmit,
control,
reset,
formState: { errors, isDirty, isSubmitting },
} = useForm<GiteaConfigFormValues>({
defaultValues: {
GITEA_HOST: config["GITEA_HOST"] || "https://gitea.com",
GITEA_CLIENT_ID: config["GITEA_CLIENT_ID"],
GITEA_CLIENT_SECRET: config["GITEA_CLIENT_SECRET"],
},
});
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
const GITEA_FORM_FIELDS: TControllerInputFormField[] = [
{
key: "GITEA_HOST",
type: "text",
label: "Gitea Host",
description: (
<>Use the URL of your Gitea instance. For the official Gitea instance, use &quot;https://gitea.com&quot;.</>
),
placeholder: "https://gitea.com",
error: Boolean(errors.GITEA_HOST),
required: true,
},
{
key: "GITEA_CLIENT_ID",
type: "text",
label: "Client ID",
description: (
<>
You will get this from your{" "}
<a
tabIndex={-1}
href="https://gitea.com/user/settings/applications"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Gitea OAuth application settings.
</a>
</>
),
placeholder: "70a44354520df8bd9bcd",
error: Boolean(errors.GITEA_CLIENT_ID),
required: true,
},
{
key: "GITEA_CLIENT_SECRET",
type: "password",
label: "Client secret",
description: (
<>
Your client secret is also found in your{" "}
<a
tabIndex={-1}
href="https://gitea.com/user/settings/applications"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Gitea OAuth application settings.
</a>
</>
),
placeholder: "9b0050f94ec1b744e32ce79ea4ffacd40d4119cb",
error: Boolean(errors.GITEA_CLIENT_SECRET),
required: true,
},
];
const GITEA_SERVICE_FIELD: TCopyField[] = [
{
key: "Callback_URI",
label: "Callback URI",
url: `${originURL}/auth/gitea/callback/`,
description: (
<>
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
field{" "}
<a
tabIndex={-1}
href={`${control._formValues.GITEA_HOST || "https://gitea.com"}/user/settings/applications`}
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
</a>
</>
),
},
];
const onSubmit = async (formData: GiteaConfigFormValues) => {
const payload: Partial<GiteaConfigFormValues> = { ...formData };
await updateInstanceConfigurations(payload)
.then((response = []) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your Gitea authentication is configured. You should test it now.",
});
reset({
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value,
GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value,
});
})
.catch((err) => console.error(err));
};
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (isDirty) {
e.preventDefault();
setIsDiscardChangesModalOpen(true);
}
};
return (
<>
<ConfirmDiscardModal
isOpen={isDiscardChangesModalOpen}
onDiscardHref="/authentication"
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-xl font-medium">Gitea-provided details for Plane</div>
{GITEA_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
control={control}
type={field.type}
name={field.key}
label={field.label}
description={field.description}
placeholder={field.placeholder}
error={field.error}
required={field.required}
/>
))}
<div className="flex flex-col gap-1 pt-4">
<div className="flex items-center gap-4">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<Link
href="/authentication"
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back
</Link>
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
<div className="pt-2 text-xl font-medium">Plane-provided details for Gitea</div>
{GITEA_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
</div>
</div>
</div>
</>
);
}
@@ -1,99 +0,0 @@
import { useState } from "react";
import { observer } from "mobx-react";
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";
//local components
import type { Route } from "./+types/page";
import { InstanceGiteaConfigForm } from "./form";
const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthenticationPage() {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// config
const enableGiteaConfig = formattedConfig?.IS_GITEA_ENABLED ?? "";
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
const updateConfig = async (key: "IS_GITEA_ENABLED", value: string) => {
setIsSubmitting(true);
const payload = {
[key]: value,
};
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`,
},
error: {
title: "Error",
message: () => "Failed to save configuration",
},
});
await updateConfigPromise
.then(() => {
setIsSubmitting(false);
})
.catch((err) => {
console.error(err);
setIsSubmitting(false);
});
};
const isGiteaEnabled = enableGiteaConfig === "1";
return (
<>
<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">
<AuthenticationMethodCard
name="Gitea"
description="Allow members to login or sign up to plane with their Gitea accounts."
icon={<img src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
config={
<ToggleSwitch
value={isGiteaEnabled}
onChange={() => {
updateConfig("IS_GITEA_ENABLED", isGiteaEnabled ? "0" : "1");
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceGiteaConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</div>
</div>
</>
);
});
export const meta: Route.MetaFunction = () => [{ title: "Gitea Authentication - God Mode" }];
export default InstanceGiteaAuthenticationPage;
@@ -1,3 +1,6 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
@@ -26,7 +29,7 @@ type Props = {
type GithubConfigFormValues = Record<TInstanceGithubAuthenticationConfigurationKeys, string>;
export function InstanceGithubConfigForm(props: Props) {
export const InstanceGithubConfigForm: FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
@@ -243,4 +246,4 @@ export function InstanceGithubConfigForm(props: Props) {
</div>
</>
);
}
};
@@ -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}</>;
}
@@ -1,5 +1,8 @@
"use client";
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
@@ -7,19 +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(function InstanceGithubAuthenticationPage(
_props: Route.ComponentProps
) {
const InstanceGithubAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
@@ -44,7 +44,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`,
message: () => `GitHub authentication is now ${value ? "active" : "disabled"}.`,
},
error: {
title: "Error",
@@ -72,7 +72,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
name="GitHub"
description="Allow members to login or sign up to plane with their GitHub accounts."
icon={
<img
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={24}
width={24}
@@ -111,6 +111,4 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
);
});
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 function InstanceGitlabConfigForm(props: Props) {
export const InstanceGitlabConfigForm: FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
@@ -208,4 +209,4 @@ export function InstanceGitlabConfigForm(props: Props) {
</div>
</>
);
}
};
@@ -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}</>;
}
@@ -1,21 +1,21 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
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(function InstanceGitlabAuthenticationPage(
_props: Route.ComponentProps
) {
const InstanceGitlabAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
@@ -38,7 +38,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`,
message: () => `GitLab authentication is now ${value ? "active" : "disabled"}.`,
},
error: {
title: "Error",
@@ -62,7 +62,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
<AuthenticationMethodCard
name="GitLab"
description="Allow members to login or sign up to plane with their GitLab accounts."
icon={<img src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />}
icon={<Image src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />}
config={
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
@@ -99,6 +99,4 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
);
});
export const meta: Route.MetaFunction = () => [{ title: "GitLab Authentication - God Mode" }];
export default InstanceGitlabAuthenticationPage;
@@ -1,3 +1,5 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
@@ -25,7 +27,7 @@ type Props = {
type GoogleConfigFormValues = Record<TInstanceGoogleAuthenticationConfigurationKeys, string>;
export function InstanceGoogleConfigForm(props: Props) {
export const InstanceGoogleConfigForm: FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
@@ -230,4 +232,4 @@ export function InstanceGoogleConfigForm(props: Props) {
</div>
</>
);
}
};
@@ -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}</>;
}
@@ -1,21 +1,21 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
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(function InstanceGoogleAuthenticationPage(
_props: Route.ComponentProps
) {
const InstanceGoogleAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
@@ -38,7 +38,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`,
message: () => `Google authentication is now ${value ? "active" : "disabled"}.`,
},
error: {
title: "Error",
@@ -63,7 +63,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
name="Google"
description="Allow members to login or sign up to plane with their Google
accounts."
icon={<img src={GoogleLogo} height={24} width={24} alt="Google Logo" />}
icon={<Image src={GoogleLogo} height={24} width={24} alt="Google Logo" />}
config={
<ToggleSwitch
value={Boolean(parseInt(enableGoogleConfig))}
@@ -100,6 +100,4 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
);
});
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}</>;
}
@@ -1,3 +1,5 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
@@ -10,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(function InstanceAuthenticationPage(_props: Route.ComponentProps) {
const InstanceAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
@@ -110,6 +111,4 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
);
});
export const meta: Route.MetaFunction = () => [{ title: "Authentication Settings - Plane Web" }];
export default InstanceAuthenticationPage;
@@ -1,4 +1,7 @@
import { useMemo, useState } from "react";
"use client";
import type { FC } from "react";
import React, { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
// types
import { Button } from "@plane/propel/button";
@@ -28,7 +31,7 @@ const EMAIL_SECURITY_OPTIONS: { [key in TEmailSecurityKeys]: string } = {
NONE: "No email security",
};
export function InstanceEmailForm(props: IInstanceEmailForm) {
export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
const { config } = props;
// states
const [isSendTestEmailModalOpen, setIsSendTestEmailModalOpen] = useState(false);
@@ -222,4 +225,4 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
</div>
</div>
);
}
};
@@ -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}</>;
}
@@ -1,3 +1,5 @@
"use client";
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
@@ -6,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(function InstanceEmailPage(_props: Route.ComponentProps) {
const InstanceEmailPage: React.FC = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
@@ -90,6 +91,4 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
);
});
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 function SendTestEmailModal(props: Props) {
export const SendTestEmailModal: FC<Props> = (props) => {
const { isOpen, handleClose } = props;
// state
@@ -61,10 +62,10 @@ export function SendTestEmailModal(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 function SendTestEmailModal(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"
@@ -133,4 +134,4 @@ export function SendTestEmailModal(props: Props) {
</Dialog>
</Transition.Root>
);
}
};
@@ -1,3 +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";
@@ -18,7 +20,7 @@ export interface IGeneralConfigurationForm {
instanceAdmins: IInstanceAdmin[];
}
export const GeneralConfigurationForm = observer(function GeneralConfigurationForm(props: IGeneralConfigurationForm) {
export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer((props) => {
const { instance, instanceAdmins } = props;
// hooks
const { instanceConfigurations, updateInstanceInfo, updateInstanceConfigurations } = useInstance();
@@ -1,3 +1,6 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
@@ -11,7 +14,7 @@ type TIntercomConfig = {
isTelemetryEnabled: boolean;
};
export const IntercomConfig = observer(function IntercomConfig(props: TIntercomConfig) {
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}</>;
}
@@ -1,8 +1,8 @@
"use client";
import { observer } from "mobx-react";
// hooks
import { useInstance } from "@/hooks/store";
// components
import type { Route } from "./+types/page";
import { GeneralConfigurationForm } from "./form";
function GeneralPage() {
@@ -28,6 +28,4 @@ function GeneralPage() {
);
}
export const meta: Route.MetaFunction = () => [{ title: "General Settings - God Mode" }];
export default observer(GeneralPage);
+5 -4
View File
@@ -1,3 +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";
@@ -8,7 +11,7 @@ import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
// hooks
import { useTheme } from "@/hooks/store";
export const HamburgerToggle = observer(function HamburgerToggle() {
export const HamburgerToggle: FC = observer(() => {
const { isSidebarCollapsed, toggleSidebar } = useTheme();
return (
<div
@@ -20,7 +23,7 @@ export const HamburgerToggle = observer(function HamburgerToggle() {
);
});
export const AdminHeader = observer(function AdminHeader() {
export const AdminHeader: FC = observer(() => {
const pathName = usePathname();
const getHeaderTitle = (pathName: string) => {
@@ -41,8 +44,6 @@ export const AdminHeader = observer(function AdminHeader() {
return "GitHub";
case "gitlab":
return "GitLab";
case "gitea":
return "Gitea";
case "workspace":
return "Workspace";
case "create":
@@ -1,3 +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";
@@ -13,7 +15,7 @@ type IInstanceImageConfigForm = {
type ImageConfigFormValues = Record<TInstanceImageConfigurationKeys, string>;
export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
export const InstanceImageConfigForm: FC<IInstanceImageConfigForm> = (props) => {
const { config } = props;
// store hooks
const { updateInstanceConfigurations } = useInstance();
@@ -76,4 +78,4 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
</div>
</div>
);
}
};
@@ -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}</>;
}
@@ -1,13 +1,14 @@
"use client";
import { observer } from "mobx-react";
import useSWR from "swr";
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(function InstanceImagePage(_props: Route.ComponentProps) {
const InstanceImagePage = observer(() => {
// store
const { formattedConfig, fetchInstanceConfigurations } = useInstance();
@@ -37,6 +38,4 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
);
});
export const meta: Route.MetaFunction = () => [{ title: "Images Settings - God Mode" }];
export default InstanceImagePage;
+16 -10
View File
@@ -1,26 +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";
function AdminLayout(_props: 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 (
@@ -36,9 +44,7 @@ function AdminLayout(_props: 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>
@@ -46,6 +52,6 @@ function AdminLayout(_props: Route.ComponentProps) {
}
return <></>;
}
};
export default observer(AdminLayout);
@@ -1,3 +1,5 @@
"use client";
import { Fragment, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { useTheme as useNextTheme } from "next-themes";
@@ -14,7 +16,7 @@ import { useTheme, useUser } from "@/hooks/store";
// service initialization
const authService = new AuthService();
export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
export const AdminSidebarDropdown = observer(() => {
// store hooks
const { isSidebarCollapsed } = useTheme();
const { currentUser, signOut } = useUser();
@@ -1,3 +1,6 @@
"use client";
import type { FC } from "react";
import { useState, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
@@ -32,7 +35,7 @@ const helpOptions = [
},
];
export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection() {
export const AdminSidebarHelpSection: FC = observer(() => {
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// store
@@ -1,3 +1,5 @@
"use client";
import { observer } from "mobx-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -48,7 +50,7 @@ const INSTANCE_ADMIN_LINKS = [
},
];
export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
export const AdminSidebarMenu = observer(() => {
// store hooks
const { isSidebarCollapsed, toggleSidebar } = useTheme();
// router
+4 -1
View File
@@ -1,3 +1,6 @@
"use client";
import type { FC } from "react";
import { useEffect, useRef } from "react";
import { observer } from "mobx-react";
// plane helpers
@@ -9,7 +12,7 @@ import { AdminSidebarDropdown } from "./sidebar-dropdown";
import { AdminSidebarHelpSection } from "./sidebar-help-section";
import { AdminSidebarMenu } from "./sidebar-menu";
export const AdminSidebar = observer(function AdminSidebar() {
export const AdminSidebar: FC = observer(() => {
// store
const { isSidebarCollapsed, toggleSidebar } = useTheme();
@@ -15,7 +15,7 @@ import { useWorkspace } from "@/hooks/store";
const instanceWorkspaceService = new InstanceWorkspaceService();
export function WorkspaceCreateForm() {
export const WorkspaceCreateForm = () => {
// router
const router = useRouter();
// states
@@ -209,4 +209,4 @@ export function WorkspaceCreateForm() {
</div>
</div>
);
}
};
@@ -1,24 +1,21 @@
"use client";
import { observer } from "mobx-react";
// components
import type { Route } from "./+types/page";
import { WorkspaceCreateForm } from "./form";
const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.ComponentProps) {
return (
<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>
<div className="text-sm font-normal text-custom-text-300">
You will need to invite users from Workspace Settings after you create this workspace.
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<WorkspaceCreateForm />
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>
<div className="text-sm font-normal text-custom-text-300">
You will need to invite users from Workspace Settings after you create this workspace.
</div>
</div>
);
});
export const meta: Route.MetaFunction = () => [{ title: "Create Workspace - God Mode" }];
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<WorkspaceCreateForm />
</div>
</div>
));
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}</>;
}
@@ -1,3 +1,5 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
@@ -14,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(function WorkspaceManagementPage(_props: Route.ComponentProps) {
const WorkspaceManagementPage = observer(() => {
// states
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// store
@@ -166,6 +167,4 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
);
});
export const meta: Route.MetaFunction = () => [{ title: "Workspace Management - God Mode" }];
export default WorkspaceManagementPage;
+5 -6
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 function AuthBanner(props: TAuthBanner) {
export const AuthBanner: FC<TAuthBanner> = (props) => {
const { bannerData, handleBannerData } = props;
if (!bannerData) return <></>;
@@ -23,8 +22,8 @@ export function AuthBanner(props: TAuthBanner) {
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>
);
}
};
+9 -9
View File
@@ -1,12 +1,12 @@
"use client";
import Link from "next/link";
import { PlaneLockup } from "@plane/propel/icons";
export function AuthHeader() {
return (
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
<Link href="/">
<PlaneLockup height={20} width={95} className="text-custom-text-100" />
</Link>
</div>
);
}
export const AuthHeader = () => (
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
<Link href="/">
<PlaneLockup height={20} width={95} className="text-custom-text-100" />
</Link>
</div>
);
+10 -8
View File
@@ -1,3 +1,5 @@
import type { ReactNode } from "react";
import Image from "next/image";
import Link from "next/link";
import { KeyRound, Mails } from "lucide-react";
// plane packages
@@ -6,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",
@@ -26,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]: {
@@ -134,7 +136,7 @@ export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps
key: "google",
name: "Google",
description: "Allow members to log in or sign up for Plane with their Google accounts.",
icon: <img src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
icon: <Image src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
@@ -142,7 +144,7 @@ export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps
name: "GitHub",
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
icon: (
<img
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
@@ -155,7 +157,7 @@ export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps
key: "gitlab",
name: "GitLab",
description: "Allow members to log in or sign up to plane with their GitLab accounts.",
icon: <img src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
];
+3 -19
View File
@@ -1,25 +1,9 @@
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]);
"use client";
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);
+4 -8
View File
@@ -1,3 +1,5 @@
"use client";
import { observer } from "mobx-react";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
@@ -6,10 +8,9 @@ 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";
function HomePage() {
const HomePage = () => {
// store hooks
const { instance, error } = useInstance();
@@ -34,11 +35,6 @@ function HomePage() {
// if instance is fetched and setup is done, show sign in form
return <InstanceSignInForm />;
}
};
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." },
];
+5 -2
View File
@@ -1,3 +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";
@@ -43,7 +46,7 @@ const defaultFromData: TFormData = {
password: "",
};
export function InstanceSignInForm() {
export const InstanceSignInForm: FC = () => {
// search params
const searchParams = useSearchParams();
const emailParam = searchParams.get("email") || undefined;
@@ -190,4 +193,4 @@ export function InstanceSignInForm() {
</div>
</>
);
}
};
+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(function InstanceProvider(props: React.PropsWithChildren) {
type InstanceProviderProps = {
children: ReactNode;
};
export const InstanceProvider: FC<InstanceProviderProps> = observer((props) => {
const { children } = props;
// store hooks
const { fetchInstanceInfo } = useInstance();
@@ -1,24 +1,25 @@
"use client";
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>
+6 -3
View File
@@ -1,3 +1,6 @@
"use client";
import type { ReactNode } from "react";
import { createContext } from "react";
// plane admin store
import { RootStore } from "@/plane-admin/store/root.store";
@@ -21,12 +24,12 @@ function initializeStore(initialData = {}) {
}
export type StoreProviderProps = {
children: React.ReactNode;
children: ReactNode;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialState?: any;
};
export function StoreProvider({ children, initialState = {} }: StoreProviderProps) {
export const StoreProvider = ({ children, initialState = {} }: StoreProviderProps) => {
const store = initializeStore(initialState);
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
}
};
+4 -2
View File
@@ -1,8 +1,10 @@
"use client";
import { useTheme } from "next-themes";
import { Toast } from "@plane/propel/toast";
import { resolveGeneralTheme } from "@plane/utils";
export function ToastWithTheme() {
export const ToastWithTheme = () => {
const { resolvedTheme } = useTheme();
return <Toast theme={resolveGeneralTheme(resolvedTheme)} />;
}
};
+8 -1
View File
@@ -1,10 +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(function UserProvider({ children }: React.PropsWithChildren) {
interface IUserProvider {
children: ReactNode;
}
export const UserProvider: FC<IUserProvider> = observer(({ children }) => {
// hooks
const { isSidebarCollapsed, toggleSidebar } = useTheme();
const { currentUser, fetchCurrentUser } = useUser();
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

-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 @@
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;
};
function Image({ src, alt = "", ...rest }: NextImageProps) {
return <img src={src} alt={alt} {...rest} />;
}
export default Image;
-17
View File
@@ -1,17 +0,0 @@
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
};
function Link({ href, replace, prefetch: _prefetch, scroll: _scroll, shallow: _shallow, ...rest }: NextLinkProps) {
return <RRLink to={ensureTrailingSlash(href)} replace={replace} {...rest} />;
}
export default Link;
-32
View File
@@ -1,32 +0,0 @@
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;
}
-36
View File
@@ -1,36 +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";
function PageNotFound() {
return (
<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;
-34
View File
@@ -1,34 +0,0 @@
/* 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>
);
});
+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>
);
}
-80
View File
@@ -1,80 +0,0 @@
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";
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 (
<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 (
<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;
-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;
}
@@ -1,4 +1,5 @@
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { KeyRound, Mails } from "lucide-react";
// types
@@ -9,16 +10,8 @@ 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";
import { GithubConfiguration } from "@/components/authentication/github-config";
import { GitlabConfiguration } from "@/components/authentication/gitlab-config";
import { GoogleConfiguration } from "@/components/authentication/google-config";
@@ -26,6 +19,12 @@ import { PasswordLoginConfiguration } from "@/components/authentication/password
// plane admin components
import { UpgradeButton } from "@/plane-admin/components/common";
// assets
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;
@@ -57,7 +56,7 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
key: "google",
name: "Google",
description: "Allow members to log in or sign up for Plane with their Google accounts.",
icon: <img src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
icon: <Image src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
@@ -65,7 +64,7 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
name: "GitHub",
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
icon: (
<img
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
@@ -78,21 +77,14 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
key: "gitlab",
name: "GitLab",
description: "Allow members to log in or sign up to plane with their GitLab accounts.",
icon: <img src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "gitea",
name: "Gitea",
description: "Allow members to log in or sign up to plane with their Gitea accounts.",
icon: <img src={giteaLogo} height={20} width={20} alt="Gitea Logo" />,
config: <GiteaConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "oidc",
name: "OIDC",
description: "Authenticate your users via the OpenID Connect protocol.",
icon: <img src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
icon: <Image src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
config: <UpgradeButton />,
unavailable: true,
},
@@ -100,13 +92,13 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
key: "saml",
name: "SAML",
description: "Authenticate your users via the Security Assertion Markup Language protocol.",
icon: <img src={SAMLLogo} height={22} width={22} alt="SAML Logo" className="pl-0.5" />,
icon: <Image src={SAMLLogo} height={22} width={22} alt="SAML Logo" className="pl-0.5" />,
config: <UpgradeButton />,
unavailable: true,
},
];
export const AuthenticationModes = observer(function AuthenticationModes(props: TAuthenticationModeProps) {
export const AuthenticationModes: React.FC<TAuthenticationModeProps> = observer((props) => {
const { disabled, updateConfig } = props;
// next-themes
const { resolvedTheme } = useTheme();
@@ -1,3 +1,5 @@
"use client";
import React from "react";
// icons
import { SquareArrowOutUpRight } from "lucide-react";
@@ -5,15 +7,9 @@ import { SquareArrowOutUpRight } from "lucide-react";
import { getButtonStyling } from "@plane/propel/button";
import { cn } from "@plane/utils";
export function UpgradeButton() {
return (
<a
href="https://plane.so/pricing?mode=self-hosted"
target="_blank"
className={cn(getButtonStyling("primary", "sm"))}
>
Upgrade
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
</a>
);
}
export const UpgradeButton: React.FC = () => (
<a href="https://plane.so/pricing?mode=self-hosted" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
Upgrade
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
</a>
);
@@ -1,3 +1,6 @@
"use client";
import type { FC } from "react";
// helpers
import { cn } from "@plane/utils";
@@ -11,7 +14,7 @@ type Props = {
unavailable?: boolean;
};
export function AuthenticationMethodCard(props: Props) {
export const AuthenticationMethodCard: FC<Props> = (props) => {
const { name, description, icon, config, disabled = false, withBorder = true, unavailable = false } = props;
return (
@@ -50,4 +53,4 @@ export function AuthenticationMethodCard(props: Props) {
<div className={`shrink-0 ${disabled && "opacity-70"}`}>{config}</div>
</div>
);
}
};
@@ -1,3 +1,5 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
// hooks
@@ -12,7 +14,7 @@ type Props = {
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export const EmailCodesConfiguration = observer(function EmailCodesConfiguration(props: Props) {
export const EmailCodesConfiguration: React.FC<Props> = observer((props) => {
const { disabled, updateConfig } = props;
// store
const { formattedConfig } = useInstance();
@@ -1,56 +0,0 @@
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
// plane internal packages
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store";
type Props = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export const GiteaConfiguration = observer(function GiteaConfiguration(props: Props) {
const { disabled, updateConfig } = props;
// store
const { formattedConfig } = useInstance();
// derived values
const GiteaConfig = formattedConfig?.IS_GITEA_ENABLED ?? "";
const GiteaConfigured =
!!formattedConfig?.GITEA_HOST && !!formattedConfig?.GITEA_CLIENT_ID && !!formattedConfig?.GITEA_CLIENT_SECRET;
return (
<>
{GiteaConfigured ? (
<div className="flex items-center gap-4">
<Link href="/authentication/gitea" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
Edit
</Link>
<ToggleSwitch
value={Boolean(parseInt(GiteaConfig))}
onChange={() => {
Boolean(parseInt(GiteaConfig)) === true
? updateConfig("IS_GITEA_ENABLED", "0")
: updateConfig("IS_GITEA_ENABLED", "1");
}}
size="sm"
disabled={disabled}
/>
</div>
) : (
<Link
href="/authentication/gitea"
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
>
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
Configure
</Link>
)}
</>
);
});
@@ -1,3 +1,5 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
@@ -16,7 +18,7 @@ type Props = {
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export const GithubConfiguration = observer(function GithubConfiguration(props: Props) {
export const GithubConfiguration: React.FC<Props> = observer((props) => {
const { disabled, updateConfig } = props;
// store
const { formattedConfig } = useInstance();
@@ -1,3 +1,6 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
@@ -15,7 +18,7 @@ type Props = {
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export const GitlabConfiguration = observer(function GitlabConfiguration(props: Props) {
export const GitlabConfiguration: React.FC<Props> = observer((props) => {
const { disabled, updateConfig } = props;
// store
const { formattedConfig } = useInstance();
@@ -1,3 +1,6 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
@@ -15,7 +18,7 @@ type Props = {
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export const GoogleConfiguration = observer(function GoogleConfiguration(props: Props) {
export const GoogleConfiguration: React.FC<Props> = observer((props) => {
const { disabled, updateConfig } = props;
// store
const { formattedConfig } = useInstance();
@@ -1,3 +1,5 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
// hooks
@@ -12,7 +14,7 @@ type Props = {
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export const PasswordLoginConfiguration = observer(function PasswordLoginConfiguration(props: Props) {
export const PasswordLoginConfiguration: React.FC<Props> = observer((props) => {
const { disabled, updateConfig } = props;
// store
const { formattedConfig } = useInstance();
+3 -2
View File
@@ -1,3 +1,4 @@
import type { FC } from "react";
import { AlertCircle, CheckCircle2 } from "lucide-react";
type TBanner = {
@@ -5,7 +6,7 @@ type TBanner = {
message: string;
};
export function Banner(props: TBanner) {
export const Banner: FC<TBanner> = (props) => {
const { type, message } = props;
return (
@@ -28,4 +29,4 @@ export function Banner(props: TBanner) {
</div>
</div>
);
}
};
@@ -1,3 +1,5 @@
"use client";
import Link from "next/link";
import { Tooltip } from "@plane/propel/tooltip";
@@ -7,7 +9,7 @@ type Props = {
icon?: React.ReactNode | undefined;
};
export function BreadcrumbLink(props: Props) {
export const BreadcrumbLink: React.FC<Props> = (props) => {
const { href, label, icon } = props;
return (
<Tooltip tooltipContent={label} position="bottom">
@@ -33,4 +35,4 @@ export function BreadcrumbLink(props: Props) {
</li>
</Tooltip>
);
}
};
@@ -6,18 +6,16 @@ type TProps = {
darkerShade?: boolean;
};
export function CodeBlock({ children, className, darkerShade }: TProps) {
return (
<span
className={cn(
"px-0.5 text-xs text-custom-text-300 bg-custom-background-90 font-semibold rounded-md border border-custom-border-100",
{
"text-custom-text-200 bg-custom-background-80 border-custom-border-200": darkerShade,
},
className
)}
>
{children}
</span>
);
}
export const CodeBlock = ({ children, className, darkerShade }: TProps) => (
<span
className={cn(
"px-0.5 text-xs text-custom-text-300 bg-custom-background-90 font-semibold rounded-md border border-custom-border-100",
{
"text-custom-text-200 bg-custom-background-80 border-custom-border-200": darkerShade,
},
className
)}
>
{children}
</span>
);
@@ -1,3 +1,5 @@
"use client";
import React from "react";
import Link from "next/link";
// headless ui
@@ -11,7 +13,7 @@ type Props = {
onDiscardHref: string;
};
export function ConfirmDiscardModal(props: Props) {
export const ConfirmDiscardModal: React.FC<Props> = (props) => {
const { isOpen, handleClose, onDiscardHref } = props;
return (
@@ -69,4 +71,4 @@ export function ConfirmDiscardModal(props: Props) {
</Dialog>
</Transition.Root>
);
}
};
@@ -1,3 +1,5 @@
"use client";
import React, { useState } from "react";
import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form";
@@ -28,7 +30,7 @@ export type TControllerInputFormField = {
required: boolean;
};
export function ControllerInput(props: Props) {
export const ControllerInput: React.FC<Props> = (props) => {
const { name, control, type, label, description, placeholder, error, required } = props;
// states
const [showPassword, setShowPassword] = useState(false);
@@ -79,4 +81,4 @@ export function ControllerInput(props: Props) {
{description && <p className="pt-0.5 text-xs text-custom-text-300">{description}</p>}
</div>
);
}
};
@@ -1,3 +1,5 @@
"use client";
import React from "react";
// ui
import { Copy } from "lucide-react";
@@ -17,7 +19,7 @@ export type TCopyField = {
description: string | React.ReactNode;
};
export function CopyField(props: Props) {
export const CopyField: React.FC<Props> = (props) => {
const { label, url, description } = props;
return (
@@ -41,4 +43,4 @@ export function CopyField(props: Props) {
<div className="text-xs text-custom-text-300">{description}</div>
</div>
);
}
};
@@ -1,10 +1,13 @@
"use client";
import React from "react";
import Image from "next/image";
import { Button } from "@plane/propel/button";
type Props = {
title: string;
description?: React.ReactNode;
image?: string;
image?: any;
primaryButton?: {
icon?: any;
text: string;
@@ -14,27 +17,32 @@ type Props = {
disabled?: boolean;
};
export function EmptyState({ title, description, image, primaryButton, secondaryButton, disabled = false }: Props) {
return (
<div className={`flex h-full w-full items-center justify-center`}>
<div className="flex w-full flex-col items-center text-center">
{image && <img src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
<div className="flex items-center gap-4">
{primaryButton && (
<Button
variant="primary"
prependIcon={primaryButton.icon}
onClick={primaryButton.onClick}
disabled={disabled}
>
{primaryButton.text}
</Button>
)}
{secondaryButton}
</div>
export const EmptyState: React.FC<Props> = ({
title,
description,
image,
primaryButton,
secondaryButton,
disabled = false,
}) => (
<div className={`flex h-full w-full items-center justify-center`}>
<div className="flex w-full flex-col items-center text-center">
{image && <Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
<div className="flex items-center gap-4">
{primaryButton && (
<Button
variant="primary"
prependIcon={primaryButton.icon}
onClick={primaryButton.onClick}
disabled={disabled}
>
{primaryButton.text}
</Button>
)}
{secondaryButton}
</div>
</div>
);
}
</div>
);
@@ -1,15 +1,17 @@
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 function LogoSpinner() {
export const LogoSpinner = () => {
const { resolvedTheme } = useTheme();
const logoSrc = resolvedTheme === "dark" ? LogoSpinnerLight : LogoSpinnerDark;
return (
<div className="flex items-center justify-center">
<img src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
<Image src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
</div>
);
}
};
@@ -1,9 +1,11 @@
"use client";
type TPageHeader = {
title?: string;
description?: string;
};
export function PageHeader(props: TPageHeader) {
export const PageHeader: React.FC<TPageHeader> = (props) => {
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
return (
@@ -12,4 +14,4 @@ export function PageHeader(props: TPageHeader) {
<meta name="description" content={description} />
</>
);
}
};
@@ -1,12 +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 = observer(function InstanceFailureView() {
export const InstanceFailureView: FC = observer(() => {
const { resolvedTheme } = useTheme();
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
@@ -21,7 +24,7 @@ export const InstanceFailureView = observer(function InstanceFailureView() {
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
<div className="relative flex flex-col justify-center items-center space-y-4">
<img src={instanceImage} alt="Instance failure illustration" />
<Image src={instanceImage} alt="Plane Logo" />
<h3 className="font-medium text-2xl text-white text-center">Unable to fetch instance details.</h3>
<p className="font-medium text-base text-center">
We were unable to fetch the details of the instance. Fret not, it might just be a connectivity issue.
@@ -1,8 +1,8 @@
export function FormHeader({ heading, subHeading }: { heading: string; subHeading: string }) {
return (
<div className="flex flex-col gap-1">
<span className="text-2xl font-semibold text-custom-text-100 leading-7">{heading}</span>
<span className="text-lg font-semibold text-custom-text-400 leading-7">{subHeading}</span>
</div>
);
}
"use client";
export const FormHeader = ({ heading, subHeading }: { heading: string; subHeading: string }) => (
<div className="flex flex-col gap-1">
<span className="text-2xl font-semibold text-custom-text-100 leading-7">{heading}</span>
<span className="text-lg font-semibold text-custom-text-400 leading-7">{subHeading}</span>
</div>
);
@@ -1,28 +1,30 @@
"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 function InstanceNotReady() {
return (
<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">
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
<img src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="font-medium text-base text-custom-text-400">
Get started by setting up your instance and workspace
</p>
</div>
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">
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="font-medium text-base text-custom-text-400">
Get started by setting up your instance and workspace
</p>
</div>
<div>
<Link href={"/setup/?auth_enabled=0"}>
<Button size="lg" className="w-full">
Get started
</Button>
</Link>
</div>
<div>
<Link href={"/setup/?auth_enabled=0"}>
<Button size="lg" className="w-full">
Get started
</Button>
</Link>
</div>
</div>
);
}
</div>
);
@@ -1,16 +1,17 @@
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 function InstanceLoading() {
export const InstanceLoading = () => {
const { resolvedTheme } = useTheme();
const logoSrc = resolvedTheme === "dark" ? LogoSpinnerLight : LogoSpinnerDark;
return (
<div className="flex items-center justify-center">
<img src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
<Image src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
</div>
);
}
};
@@ -1,3 +1,6 @@
"use client";
import type { FC } from "react";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
// icons
@@ -51,7 +54,8 @@ const defaultFromData: TFormData = {
is_telemetry_enabled: true,
};
export function InstanceSetupForm() {
export const InstanceSetupForm: FC = (props) => {
const {} = props;
// search params
const searchParams = useSearchParams();
const firstNameParam = searchParams.get("first_name") || undefined;
@@ -348,4 +352,4 @@ export function InstanceSetupForm() {
</div>
</>
);
}
};
+10 -6
View File
@@ -1,20 +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(function NewUserPopup() {
export const NewUserPopup: React.FC = observer(() => {
// hooks
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
// theme
const { resolvedTheme } = useNextTheme();
const { resolvedTheme } = nextUseTheme();
if (!isNewUserPopup) return <></>;
return (
@@ -36,7 +40,7 @@ export const NewUserPopup = observer(function NewUserPopup() {
</div>
</div>
<div className="shrink-0 flex items-center justify-center">
<img
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? TakeoffIconDark : TakeoffIconLight}
height={80}
width={80}

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