Compare commits

..
2799 changed files with 36236 additions and 48322 deletions
-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.
@@ -31,7 +31,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12.x"
python-version: "3.x"
- name: Install Pylint
run: python -m pip install ruff
- name: Install API Dependencies
@@ -43,14 +43,11 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Affected
run: pnpm turbo run build --affected
- name: Lint Affected
run: pnpm turbo run check:lint --affected
- 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
-1
View File
@@ -111,4 +111,3 @@ build/
.react-router/
AGENTS.md
temp/
scripts/
-1
View File
@@ -1 +0,0 @@
pnpm lint-staged
+25 -45
View File
@@ -1,54 +1,34 @@
# ------------------------------
# Core Workspace Behavior
# ------------------------------
# Enforce pnpm workspace behavior and allow Turbo's lifecycle hooks if scripts are disabled
# This repo uses pnpm with workspaces.
# Always prefer using local workspace packages when available
prefer-workspace-packages = true
# Prefer linking local workspace packages when available
prefer-workspace-packages=true
link-workspace-packages=true
shared-workspace-lockfile=true
# Symlink workspace packages instead of duplicating them
link-workspace-packages = true
# Make peer installs smoother across the monorepo
auto-install-peers=true
strict-peer-dependencies=false
# Use a single lockfile across the whole monorepo
shared-workspace-lockfile = true
# If scripts are disabled (e.g., CI with --ignore-scripts), allowlisted packages can still run their hooks
# Turbo occasionally performs postinstall tasks for optimal performance
# moved to pnpm-workspace.yaml: onlyBuiltDependencies (e.g., allow turbo)
# Ensure packages added from workspace save using workspace: protocol
save-workspace-protocol = true
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=prettier
public-hoist-pattern[]=typescript
# Reproducible installs across CI and dev
prefer-frozen-lockfile=true
# ------------------------------
# Dependency Resolution
# ------------------------------
# Prefer resolving to highest versions in monorepo to reduce duplication
resolution-mode=highest
# Choose the highest compatible version across the workspace
# → reduces fragmentation & node_modules bloat
resolution-mode = highest
# Speed up native module builds by caching side effects
side-effects-cache=true
# Automatically install peer dependencies instead of forcing every package to declare them
auto-install-peers = true
# Speed up local dev by reusing local store when possible
prefer-offline=true
# Don't break the install if peers are missing
strict-peer-dependencies = false
# ------------------------------
# Performance Optimizations
# ------------------------------
# Use cached artifacts for native modules (sharp, esbuild, etc.)
side-effects-cache = true
# Prefer local cached packages rather than hitting network
prefer-offline = true
# In CI, refuse to modify lockfile (prevents drift)
prefer-frozen-lockfile = true
# Use isolated linker (best compatibility with Node ecosystem tools)
node-linker = isolated
# Hoist commonly used tools to the root to prevent duplicates and speed up resolution
public-hoist-pattern[] = typescript
public-hoist-pattern[] = eslint
public-hoist-pattern[] = *@plane/*
public-hoist-pattern[] = vite
public-hoist-pattern[] = turbo
# Ensure workspace protocol is used when adding internal deps
save-workspace-protocol=true
-10
View File
@@ -1,10 +0,0 @@
.next/
.react-router/
.turbo/
.vite/
build/
dist/
node_modules/
out/
pnpm-lock.yaml
storybook-static/
-15
View File
@@ -1,15 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"overrides": [
{
"files": ["packages/codemods/**/*"],
"options": {
"printWidth": 80
}
}
],
"plugins": ["@prettier/plugin-oxc"],
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
-1
View File
@@ -1 +0,0 @@
eslint.config.mjs @lifeiscontent
+8 -10
View File
@@ -91,7 +91,7 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
- All features or bug fixes must be tested by one or more specs (unit-tests).
- We lint with [ESLint 9](https://eslint.org/docs/latest/) using the shared `eslint.config.mjs` (type-aware via `typescript-eslint`) and format with [Prettier](https://prettier.io/) using `prettier.config.cjs`.
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
## Ways to contribute
@@ -187,19 +187,18 @@ Adding a new language involves several steps to ensure it integrates seamlessly
Add the new language to the TLanguage type in the language definitions file:
```ts
// packages/i18n/src/types/language.ts
export type TLanguage = "en" | "fr" | "your-lang";
// packages/i18n/src/types/language.ts
export type TLanguage = "en" | "fr" | "your-lang";
```
1. **Add language configuration**
Include the new language in the list of supported languages:
```ts
// packages/i18n/src/constants/language.ts
export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "English", value: "en" },
{ label: "Your Language", value: "your-lang" },
];
// packages/i18n/src/constants/language.ts
export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "English", value: "en" },
{ label: "Your Language", value: "your-lang" }
];
```
2. **Create translation files**
@@ -211,7 +210,6 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
3. **Update import logic**
Modify the language import logic to include your new language:
```ts
private importLanguageFile(language: TLanguage): Promise<any> {
switch (language) {
+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"
+12
View File
@@ -0,0 +1,12 @@
.next/*
out/*
public/*
dist/*
node_modules/*
.turbo/*
.env*
.env
.env.local
.env.development
.env.production
.env.test
+19
View File
@@ -0,0 +1,19 @@
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",
"@typescript-eslint/consistent-type-imports": [
"error",
{
prefer: "type-imports",
fixStyle: "separate-type-imports",
disallowTypeAnnotations: false,
},
],
},
};
+5 -9
View File
@@ -1,10 +1,6 @@
.next/
.react-router/
.turbo/
.vite/
build/
dist/
node_modules/
.next
.vercel
.tubro
out/
pnpm-lock.yaml
storybook-static/
dist/
build/
+5
View File
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
+29 -31
View File
@@ -13,7 +13,7 @@ RUN corepack enable pnpm
FROM base AS builder
RUN pnpm add -g turbo@2.6.3
RUN pnpm add -g turbo@2.5.8
COPY . .
@@ -28,54 +28,52 @@ FROM base AS installer
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 NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_API_BASE_PATH="/api"
ENV NEXT_PUBLIC_API_BASE_PATH=$NEXT_PUBLIC_API_BASE_PATH
ARG VITE_ADMIN_BASE_URL=""
ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL
ARG VITE_ADMIN_BASE_PATH="/god-mode"
ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG VITE_SPACE_BASE_URL=""
ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL
ARG VITE_SPACE_BASE_PATH="/spaces"
ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ARG VITE_LIVE_BASE_URL=""
ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL
ARG VITE_LIVE_BASE_PATH="/live"
ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH
ARG NEXT_PUBLIC_LIVE_BASE_URL=""
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live"
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH
ARG VITE_WEB_BASE_URL=""
ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL
ARG VITE_WEB_BASE_PATH=""
ENV VITE_WEB_BASE_PATH=$VITE_WEB_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_PATH=""
ENV NEXT_PUBLIC_WEB_BASE_PATH=$NEXT_PUBLIC_WEB_BASE_PATH
ARG VITE_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
ARG NEXT_PUBLIC_WEBSITE_URL="https://plane.so"
ENV NEXT_PUBLIC_WEBSITE_URL=$NEXT_PUBLIC_WEBSITE_URL
ARG NEXT_PUBLIC_SUPPORT_EMAIL="support@plane.so"
ENV NEXT_PUBLIC_SUPPORT_EMAIL=$NEXT_PUBLIC_SUPPORT_EMAIL
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
# Copy full directory structure before fetch to ensure all package.json files are available
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
# Fetch dependencies to cache store, then install offline with dev deps
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
# Build only the admin package
RUN pnpm turbo run build --filter=admin
# =========================================================================== #
FROM nginx:1.29-alpine AS production
FROM nginx:1.27-alpine AS production
COPY apps/admin/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=installer /app/apps/admin/build/client /usr/share/nginx/html/god-mode
+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 { 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: React.FC<IInstanceAIForm> = (props) => {
const { config } = props;
// store
const { updateInstanceConfigurations } = useInstance();
@@ -131,4 +133,4 @@ export function InstanceAIForm(props: IInstanceAIForm) {
</div>
</div>
);
}
};
+3 -1
View File
@@ -1,3 +1,5 @@
"use client";
import { observer } from "mobx-react";
import useSWR from "swr";
import { Loader } from "@plane/ui";
@@ -7,7 +9,7 @@ import { useInstance } from "@/hooks/store";
import type { Route } from "./+types/page";
import { InstanceAIForm } from "./form";
const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentProps) {
const InstanceAIPage = observer<React.FC<Route.ComponentProps>>(() => {
// store
const { fetchInstanceConfigurations, formattedConfig } = useInstance();
@@ -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";
@@ -24,7 +27,7 @@ type Props = {
type GiteaConfigFormValues = Record<TInstanceGiteaAuthenticationConfigurationKeys, string>;
export function InstanceGiteaConfigForm(props: Props) {
export const InstanceGiteaConfigForm: FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
@@ -204,4 +207,4 @@ export function InstanceGiteaConfigForm(props: Props) {
</div>
</>
);
}
};
@@ -1,5 +1,8 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import useSWR from "swr";
// plane internal packages
import { setPromiseToast } from "@plane/propel/toast";
@@ -13,7 +16,7 @@ import { useInstance } from "@/hooks/store";
import type { Route } from "./+types/page";
import { InstanceGiteaConfigForm } from "./form";
const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthenticationPage() {
const InstanceGiteaAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
@@ -62,7 +65,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
<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" />}
icon={<Image src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
config={
<ToggleSwitch
value={isGiteaEnabled}
@@ -1,3 +1,5 @@
"use client";
import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
@@ -26,7 +28,7 @@ type Props = {
type GithubConfigFormValues = Record<TInstanceGithubAuthenticationConfigurationKeys, string>;
export function InstanceGithubConfigForm(props: Props) {
export const InstanceGithubConfigForm: React.FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
@@ -243,4 +245,4 @@ export function InstanceGithubConfigForm(props: Props) {
</div>
</>
);
}
};
@@ -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
@@ -17,9 +20,7 @@ import { useInstance } from "@/hooks/store";
import type { Route } from "./+types/page";
import { InstanceGithubConfigForm } from "./form";
const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthenticationPage(
_props: Route.ComponentProps
) {
const InstanceGithubAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
@@ -72,7 +73,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}
@@ -24,7 +24,7 @@ type Props = {
type GitlabConfigFormValues = Record<TInstanceGitlabAuthenticationConfigurationKeys, string>;
export function InstanceGitlabConfigForm(props: Props) {
export const InstanceGitlabConfigForm: React.FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
@@ -208,4 +208,4 @@ export function InstanceGitlabConfigForm(props: Props) {
</div>
</>
);
}
};
@@ -1,5 +1,8 @@
"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";
@@ -13,9 +16,7 @@ import { useInstance } from "@/hooks/store";
import type { Route } from "./+types/page";
import { InstanceGitlabConfigForm } from "./form";
const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthenticationPage(
_props: Route.ComponentProps
) {
const InstanceGitlabAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
@@ -62,7 +63,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))}
@@ -1,3 +1,5 @@
"use client";
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: React.FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
@@ -230,4 +232,4 @@ export function InstanceGoogleConfigForm(props: Props) {
</div>
</>
);
}
};
@@ -1,5 +1,8 @@
"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";
@@ -13,9 +16,7 @@ import { useInstance } from "@/hooks/store";
import type { Route } from "./+types/page";
import { InstanceGoogleConfigForm } from "./form";
const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthenticationPage(
_props: Route.ComponentProps
) {
const InstanceGoogleAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
@@ -63,7 +64,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))}
@@ -1,3 +1,5 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
@@ -12,7 +14,7 @@ import { useInstance } from "@/hooks/store";
import { AuthenticationModes } from "@/plane-admin/components/authentication";
import type { Route } from "./+types/page";
const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(_props: Route.ComponentProps) {
const InstanceAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
@@ -1,3 +1,5 @@
"use client";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
// types
@@ -28,7 +30,7 @@ const EMAIL_SECURITY_OPTIONS: { [key in TEmailSecurityKeys]: string } = {
NONE: "No email security",
};
export function InstanceEmailForm(props: IInstanceEmailForm) {
export const InstanceEmailForm: React.FC<IInstanceEmailForm> = (props) => {
const { config } = props;
// states
const [isSendTestEmailModalOpen, setIsSendTestEmailModalOpen] = useState(false);
@@ -163,6 +165,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
onChange={handleEmailSecurityChange}
buttonClassName="rounded-md border-custom-border-200"
optionsClassName="w-full"
input
>
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
@@ -221,4 +224,4 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
</div>
</div>
);
}
};
@@ -1,3 +1,5 @@
"use client";
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
@@ -9,7 +11,7 @@ import { useInstance } from "@/hooks/store";
import type { Route } from "./+types/page";
import { InstanceEmailForm } from "./email-config-form";
const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.ComponentProps) {
const InstanceEmailPage = observer<React.FC<Route.ComponentProps>>(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
@@ -19,7 +19,7 @@ enum ESendEmailSteps {
const instanceService = new InstanceService();
export function SendTestEmailModal(props: Props) {
export const SendTestEmailModal: React.FC<Props> = (props) => {
const { isOpen, handleClose } = props;
// state
@@ -133,4 +133,4 @@ export function SendTestEmailModal(props: Props) {
</Dialog>
</Transition.Root>
);
}
};
@@ -1,3 +1,4 @@
"use client";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { Telescope } from "lucide-react";
@@ -18,7 +19,7 @@ export interface IGeneralConfigurationForm {
instanceAdmins: IInstanceAdmin[];
}
export const GeneralConfigurationForm = observer(function GeneralConfigurationForm(props: IGeneralConfigurationForm) {
export const GeneralConfigurationForm: React.FC<IGeneralConfigurationForm> = observer((props) => {
const { instance, instanceAdmins } = props;
// hooks
const { instanceConfigurations, updateInstanceInfo, updateInstanceConfigurations } = useInstance();
@@ -1,3 +1,5 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
@@ -11,7 +13,7 @@ type TIntercomConfig = {
isTelemetryEnabled: boolean;
};
export const IntercomConfig = observer(function IntercomConfig(props: TIntercomConfig) {
export const IntercomConfig: React.FC<TIntercomConfig> = observer((props) => {
const { isTelemetryEnabled } = props;
// hooks
const { instanceConfigurations, updateInstanceConfigurations, fetchInstanceConfigurations } = useInstance();
@@ -1,3 +1,4 @@
"use client";
import { observer } from "mobx-react";
// hooks
import { useInstance } from "@/hooks/store";
+4 -2
View File
@@ -1,3 +1,5 @@
"use client";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { Menu, Settings } from "lucide-react";
@@ -8,7 +10,7 @@ import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
// hooks
import { useTheme } from "@/hooks/store";
export const HamburgerToggle = observer(function HamburgerToggle() {
export const HamburgerToggle = observer(() => {
const { isSidebarCollapsed, toggleSidebar } = useTheme();
return (
<div
@@ -20,7 +22,7 @@ export const HamburgerToggle = observer(function HamburgerToggle() {
);
});
export const AdminHeader = observer(function AdminHeader() {
export const AdminHeader = observer(() => {
const pathName = usePathname();
const getHeaderTitle = (pathName: string) => {
@@ -1,3 +1,4 @@
"use client";
import { useForm } from "react-hook-form";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
@@ -13,7 +14,7 @@ type IInstanceImageConfigForm = {
type ImageConfigFormValues = Record<TInstanceImageConfigurationKeys, string>;
export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
export const InstanceImageConfigForm: React.FC<IInstanceImageConfigForm> = (props) => {
const { config } = props;
// store hooks
const { updateInstanceConfigurations } = useInstance();
@@ -76,4 +77,4 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
</div>
</div>
);
}
};
@@ -1,3 +1,5 @@
"use client";
import { observer } from "mobx-react";
import useSWR from "swr";
import { Loader } from "@plane/ui";
@@ -7,7 +9,7 @@ import { useInstance } from "@/hooks/store";
import type { Route } from "./+types/page";
import { InstanceImageConfigForm } from "./form";
const InstanceImagePage = observer(function InstanceImagePage(_props: Route.ComponentProps) {
const InstanceImagePage = observer<React.FC<Route.ComponentProps>>(() => {
// store
const { formattedConfig, fetchInstanceConfigurations } = useInstance();
+4 -2
View File
@@ -1,3 +1,5 @@
"use client";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
@@ -12,7 +14,7 @@ import type { Route } from "./+types/layout";
import { AdminHeader } from "./header";
import { AdminSidebar } from "./sidebar";
function AdminLayout(_props: Route.ComponentProps) {
const AdminLayout: React.FC<Route.ComponentProps> = () => {
// router
const { replace } = useRouter();
// store hooks
@@ -46,6 +48,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,5 @@
"use client";
import { useState, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
@@ -11,7 +13,7 @@ import { cn } from "@plane/utils";
// hooks
import { useTheme } from "@/hooks/store";
// assets
// eslint-disable-next-line import/order
import packageJson from "package.json";
const helpOptions = [
@@ -32,7 +34,7 @@ const helpOptions = [
},
];
export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection() {
export const AdminSidebarHelpSection: React.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
+3 -1
View File
@@ -1,3 +1,5 @@
"use client";
import { useEffect, useRef } from "react";
import { observer } from "mobx-react";
// plane helpers
@@ -9,7 +11,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 = 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
@@ -177,6 +177,7 @@ export function WorkspaceCreateForm() {
}
buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none"
input
optionsClassName="w-full"
>
{ORGANIZATION_SIZE.map((item) => (
<CustomSelect.Option key={item} value={item}>
@@ -208,4 +209,4 @@ export function WorkspaceCreateForm() {
</div>
</div>
);
}
};
@@ -1,23 +1,23 @@
"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<React.FC<Route.ComponentProps>>(() => (
<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 />
</div>
</div>
));
export const meta: Route.MetaFunction = () => [{ title: "Create Workspace - God Mode" }];
@@ -1,3 +1,5 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
@@ -16,7 +18,7 @@ import { WorkspaceListItem } from "@/components/workspace/list-item";
import { useInstance, useWorkspace } from "@/hooks/store";
import type { Route } from "./+types/page";
const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props: Route.ComponentProps) {
const WorkspaceManagementPage = observer<React.FC<Route.ComponentProps>>(() => {
// states
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// store
+2 -2
View File
@@ -9,7 +9,7 @@ type TAuthBanner = {
handleBannerData?: (bannerData: TAdminAuthErrorInfo | undefined) => void;
};
export function AuthBanner(props: TAuthBanner) {
export const AuthBanner: React.FC<TAuthBanner> = (props) => {
const { bannerData, handleBannerData } = props;
if (!bannerData) return <></>;
@@ -27,4 +27,4 @@ export function AuthBanner(props: TAuthBanner) {
</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 -6
View File
@@ -1,3 +1,4 @@
import Image from "next/image";
import Link from "next/link";
import { KeyRound, Mails } from "lucide-react";
// plane packages
@@ -26,7 +27,7 @@ export enum EErrorAlertType {
}
const errorCodeMessages: {
[key in EAdminAuthErrorCodes]: { title: string; message: (email?: string) => React.ReactNode };
[key in EAdminAuthErrorCodes]: { title: string; message: (email?: string | undefined) => React.ReactNode };
} = {
// admin
[EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST]: {
@@ -79,11 +80,14 @@ const errorCodeMessages: {
},
[EAdminAuthErrorCodes.ADMIN_USER_DEACTIVATED]: {
title: `User account deactivated`,
message: () => `User account deactivated. Please contact ${SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
message: () => `User account deactivated. Please contact ${!!SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
},
};
export const authErrorHandler = (errorCode: EAdminAuthErrorCodes, email?: string): TAdminAuthErrorInfo | undefined => {
export const authErrorHandler = (
errorCode: EAdminAuthErrorCodes,
email?: string | undefined
): TAdminAuthErrorInfo | undefined => {
const bannerAlertErrorCodes = [
EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST,
EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
@@ -131,7 +135,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} />,
},
{
@@ -139,7 +143,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}
@@ -152,7 +156,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} />,
},
];
+2
View File
@@ -1,3 +1,5 @@
"use client";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
+4 -2
View File
@@ -1,3 +1,5 @@
"use client";
import { observer } from "mobx-react";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
@@ -9,7 +11,7 @@ import { useInstance } from "@/hooks/store";
import type { Route } from "./+types/page";
import { InstanceSignInForm } from "./sign-in-form";
function HomePage() {
const HomePage = () => {
// store hooks
const { instance, error } = useInstance();
@@ -34,7 +36,7 @@ function HomePage() {
// if instance is fetched and setup is done, show sign in form
return <InstanceSignInForm />;
}
};
export default observer(HomePage);
+4 -2
View File
@@ -1,3 +1,5 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { Eye, EyeOff } from "lucide-react";
@@ -43,7 +45,7 @@ const defaultFromData: TFormData = {
password: "",
};
export function InstanceSignInForm() {
export const InstanceSignInForm: React.FC = () => {
// search params
const searchParams = useSearchParams();
const emailParam = searchParams.get("email") || undefined;
@@ -190,4 +192,4 @@ export function InstanceSignInForm() {
</div>
</>
);
}
};
+1 -1
View File
@@ -3,7 +3,7 @@ import useSWR from "swr";
// hooks
import { useInstance } from "@/hooks/store";
export const InstanceProvider = observer(function InstanceProvider(props: React.PropsWithChildren) {
export const InstanceProvider = observer<React.FC<React.PropsWithChildren>>((props) => {
const { children } = props;
// store hooks
const { fetchInstanceInfo } = useInstance();
+4 -2
View File
@@ -1,3 +1,5 @@
"use client";
import { createContext } from "react";
// plane admin store
import { RootStore } from "@/plane-admin/store/root.store";
@@ -26,7 +28,7 @@ export type StoreProviderProps = {
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)} />;
}
};
+3 -1
View File
@@ -1,10 +1,12 @@
"use client";
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) {
export const UserProvider = observer<React.FC<React.PropsWithChildren>>(({ children }) => {
// hooks
const { isSidebarCollapsed, toggleSidebar } = useTheme();
const { currentUser, fetchCurrentUser } = useUser();
+3 -3
View File
@@ -1,3 +1,5 @@
"use client";
import React from "react";
// Minimal shim so code using next/image compiles under React Router + Vite
@@ -7,8 +9,6 @@ type NextImageProps = React.ImgHTMLAttributes<HTMLImageElement> & {
src: string;
};
function Image({ src, alt = "", ...rest }: NextImageProps) {
return <img src={src} alt={alt} {...rest} />;
}
const Image: React.FC<NextImageProps> = ({ src, alt = "", ...rest }) => <img src={src} alt={alt} {...rest} />;
export default Image;
+10 -3
View File
@@ -1,3 +1,5 @@
"use client";
import React from "react";
import { Link as RRLink } from "react-router";
import { ensureTrailingSlash } from "./helper";
@@ -10,8 +12,13 @@ type NextLinkProps = React.ComponentProps<"a"> & {
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} />;
}
const Link: React.FC<NextLinkProps> = ({
href,
replace,
prefetch: _prefetch,
scroll: _scroll,
shallow: _shallow,
...rest
}) => <RRLink to={ensureTrailingSlash(href)} replace={replace} {...rest} />;
export default Link;
+2
View File
@@ -1,3 +1,5 @@
"use client";
import { useMemo } from "react";
import { useLocation, useNavigate, useSearchParams as useSearchParamsRR } from "react-router";
import { ensureTrailingSlash } from "./helper";
+22 -24
View File
@@ -5,32 +5,30 @@ 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>
const PageNotFound = () => (
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>
<div className="grid h-full place-items-center p-4">
<div className="space-y-8 text-center">
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
<img src={Image404} alt="404 - Page not found" className="h-full w-full object-contain" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
<p className="text-sm text-custom-text-200">
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
temporarily unavailable.
</p>
</div>
<Link to="/general/">
<span className="flex justify-center py-4">
<Button variant="neutral-primary" size="md">
Go to general settings
</Button>
</span>
</Link>
</div>
</div>
);
}
</div>
);
export default PageNotFound;
-33
View File
@@ -1,33 +0,0 @@
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>
);
});
+2
View File
@@ -1,3 +1,5 @@
"use client";
import { ThemeProvider } from "next-themes";
import { SWRConfig } from "swr";
import { AppProgressBar } from "@/lib/b-progress";
+2 -12
View File
@@ -1,12 +1,10 @@
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";
@@ -60,18 +58,10 @@ export default function Root() {
}
export function HydrateFallback() {
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />
</div>
);
return null;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (error) {
Sentry.captureException(error);
}
export function ErrorBoundary() {
return (
<div>
<p>Something went wrong.</p>
+5
View File
@@ -0,0 +1,5 @@
declare module "next/image" {
type Props = React.ComponentProps<"img"> & { src: string };
const Image: React.FC<Props>;
export default Image;
}
@@ -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
@@ -57,7 +58,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 +66,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 +79,21 @@ 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" />,
icon: <Image 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 +101,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 = observer<React.FC<TAuthenticationModeProps>>((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,16 +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"))}
rel="noreferrer"
>
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,5 @@
"use client";
// helpers
import { cn } from "@plane/utils";
@@ -11,7 +13,7 @@ type Props = {
unavailable?: boolean;
};
export function AuthenticationMethodCard(props: Props) {
export const AuthenticationMethodCard: React.FC<Props> = (props) => {
const { name, description, icon, config, disabled = false, withBorder = true, unavailable = false } = props;
return (
@@ -50,4 +52,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,3 +1,5 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
@@ -15,7 +17,7 @@ type Props = {
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export const GiteaConfiguration = observer(function GiteaConfiguration(props: Props) {
export const GiteaConfiguration: 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";
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,5 @@
"use client";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
@@ -15,7 +17,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,5 @@
"use client";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
@@ -15,7 +17,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,4 +1,7 @@
"use client";
import React from "react";
import Image from "next/image";
import { Button } from "@plane/propel/button";
type Props = {
@@ -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,16 @@
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";
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,4 +1,6 @@
"use client";
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Button } from "@plane/propel/button";
// assets
@@ -6,7 +8,7 @@ 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";
export const InstanceFailureView = observer(function InstanceFailureView() {
export const InstanceFailureView: React.FC = observer(() => {
const { resolvedTheme } = useTheme();
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
@@ -21,7 +23,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,29 @@
"use client";
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";
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: React.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";
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,5 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
// icons
@@ -51,7 +53,8 @@ const defaultFromData: TFormData = {
is_telemetry_enabled: true,
};
export function InstanceSetupForm() {
export const InstanceSetupForm: React.FC = (props) => {
const {} = props;
// search params
const searchParams = useSearchParams();
const firstNameParam = searchParams.get("first_name") || undefined;
@@ -348,4 +351,4 @@ export function InstanceSetupForm() {
</div>
</>
);
}
};
@@ -1,4 +1,7 @@
"use client";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
import { useTheme as useNextTheme } from "next-themes";
// ui
@@ -10,7 +13,7 @@ import TakeoffIconLight from "@/app/assets/logos/takeoff-icon-light.svg?url";
import { useTheme } from "@/hooks/store";
// icons
export const NewUserPopup = observer(function NewUserPopup() {
export const NewUserPopup = observer(() => {
// hooks
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
// theme
@@ -36,7 +39,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}
@@ -11,7 +11,7 @@ type TWorkspaceListItemProps = {
workspaceId: string;
};
export const WorkspaceListItem = observer(function WorkspaceListItem({ workspaceId }: TWorkspaceListItemProps) {
export const WorkspaceListItem = observer(({ workspaceId }: TWorkspaceListItemProps) => {
// store hooks
const { getWorkspaceById } = useWorkspace();
// derived values
@@ -24,7 +24,6 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`}
target="_blank"
className="group flex items-center justify-between p-4 gap-2.5 truncate border border-custom-border-200/70 hover:border-custom-border-200 hover:bg-custom-background-90 rounded-md"
rel="noreferrer"
>
<div className="flex items-start gap-4">
<span
@@ -1,3 +1,5 @@
"use client";
import { useEffect, useRef } from "react";
import { BProgress } from "@bprogress/core";
import { useNavigation } from "react-router";
@@ -36,6 +38,7 @@ const PROGRESS_CONFIG: Readonly<ProgressConfig> = {
easing: "ease",
trickle: true,
delay: 0,
isDisabled: import.meta.env.PROD, // Disable progress bar in production builds
} as const;
/**
+14
View File
@@ -0,0 +1,14 @@
import { next } from '@vercel/edge';
export default function middleware() {
return next({
headers: {
'Referrer-Policy': 'origin-when-cross-origin',
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
'X-DNS-Prefetch-Control': 'on',
'Strict-Transport-Security':
'max-age=31536000; includeSubDomains; preload',
},
});
}
+25 -15
View File
@@ -1,21 +1,21 @@
{
"name": "admin",
"description": "Admin UI for Plane",
"version": "1.2.1",
"version": "1.1.0",
"license": "AGPL-3.0",
"private": true,
"type": "module",
"scripts": {
"dev": "react-router dev --port 3001",
"dev": "cross-env NODE_ENV=development PORT=3001 node server.mjs",
"build": "react-router build",
"preview": "react-router build && serve -s build/client -l 3001",
"preview": "react-router build && cross-env NODE_ENV=production PORT=3001 node server.mjs",
"start": "serve -s build/client -l 3001",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist && rm -rf build",
"check:lint": "eslint . --max-warnings=485",
"check:lint": "eslint . --max-warnings 19",
"check:types": "react-router typegen && tsc --noEmit",
"check:format": "prettier --check .",
"fix:lint": "eslint . --fix --max-warnings=485",
"fix:format": "prettier --write ."
"check:format": "prettier --check \"**/*.{ts,tsx,md,json,css,scss}\"",
"fix:lint": "eslint . --fix",
"fix:format": "prettier --write \"**/*.{ts,tsx,md,json,css,scss}\""
},
"dependencies": {
"@bprogress/core": "catalog:",
@@ -27,37 +27,47 @@
"@plane/types": "workspace:*",
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@react-router/node": "catalog:",
"@sentry/react-router": "catalog:",
"@react-router/express": "^7.9.3",
"@react-router/node": "^7.9.3",
"@tanstack/react-virtual": "^3.13.12",
"@tanstack/virtual-core": "^3.13.12",
"@vercel/edge": "1.2.2",
"axios": "catalog:",
"compression": "^1.8.1",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
"express": "^5.1.0",
"http-proxy-middleware": "^3.0.5",
"isbot": "^5.1.31",
"lodash-es": "catalog:",
"lucide-react": "catalog:",
"mobx": "catalog:",
"mobx-react": "catalog:",
"next-themes": "0.4.6",
"morgan": "^1.10.1",
"next-themes": "^0.2.1",
"react": "catalog:",
"react-dom": "catalog:",
"react-hook-form": "7.51.5",
"react-router": "catalog:",
"react-router-dom": "catalog:",
"react-router": "^7.9.1",
"react-router-dom": "^7.9.1",
"serve": "14.2.5",
"swr": "catalog:",
"uuid": "catalog:"
},
"devDependencies": {
"@dotenvx/dotenvx": "catalog:",
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@react-router/dev": "catalog:",
"@react-router/dev": "^7.9.1",
"@types/compression": "^1.8.1",
"@types/express": "4.17.23",
"@types/lodash-es": "catalog:",
"@types/morgan": "^1.9.10",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite": "7.1.7",
"vite-tsconfig-paths": "^5.1.4"
}
}
+1
View File
@@ -1 +1,2 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
module.exports = require("@plane/tailwind-config/postcss.config.js");
+1 -4
View File
@@ -1,11 +1,8 @@
import type { Config } from "@react-router/dev/config";
import { joinUrlPath } from "@plane/utils";
const basePath = joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ?? "", "/") ?? "/";
export default {
appDirectory: "app",
basename: basePath,
basename: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH,
// Admin runs as a client-side app; build a static client bundle only
ssr: false,
} satisfies Config;
+76
View File
@@ -0,0 +1,76 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import compression from "compression";
import dotenv from "dotenv";
import express from "express";
import morgan from "morgan";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, ".env") });
const BUILD_PATH = "./build/server/index.js";
const DEVELOPMENT = process.env.NODE_ENV !== "production";
// Derive the port from NEXT_PUBLIC_ADMIN_BASE_URL when available, otherwise
// default to http://localhost:3001 and fall back to PORT env if explicitly set.
const DEFAULT_BASE_URL = "http://localhost:3001";
const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || DEFAULT_BASE_URL;
let parsedBaseUrl;
try {
parsedBaseUrl = new URL(ADMIN_BASE_URL);
} catch {
parsedBaseUrl = new URL(DEFAULT_BASE_URL);
}
const PORT = Number.parseInt(parsedBaseUrl.port, 10);
async function start() {
const app = express();
app.use(compression());
app.disable("x-powered-by");
if (DEVELOPMENT) {
console.log("Starting development server");
const vite = await import("vite").then((vite) =>
vite.createServer({
server: { middlewareMode: true },
appType: "custom",
})
);
app.use(vite.middlewares);
app.use(async (req, res, next) => {
try {
const source = await vite.ssrLoadModule("./server/app.ts");
return source.app(req, res, next);
} catch (error) {
if (error instanceof Error) {
vite.ssrFixStacktrace(error);
}
next(error);
}
});
} else {
console.log("Starting production server");
app.use("/assets", express.static("build/client/assets", { immutable: true, maxAge: "1y" }));
app.use(morgan("tiny"));
app.use(express.static("build/client", { maxAge: "1h" }));
app.use(await import(BUILD_PATH).then((mod) => mod.app));
}
app.listen(PORT, () => {
const origin = `${parsedBaseUrl.protocol}//${parsedBaseUrl.hostname}:${PORT}`;
console.log(`Server is running on ${origin}`);
});
}
start().catch((error) => {
console.error(error);
process.exit(1);
});
+46
View File
@@ -0,0 +1,46 @@
import "react-router";
import { createRequestHandler } from "@react-router/express";
import express from "express";
import type { Express } from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
const NEXT_PUBLIC_API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
? process.env.NEXT_PUBLIC_API_BASE_URL.replace(/\/$/, "")
: "http://127.0.0.1:8000";
const NEXT_PUBLIC_API_BASE_PATH = process.env.NEXT_PUBLIC_API_BASE_PATH
? process.env.NEXT_PUBLIC_API_BASE_PATH.replace(/\/+$/, "")
: "/api";
const NORMALIZED_API_BASE_PATH = NEXT_PUBLIC_API_BASE_PATH.startsWith("/")
? NEXT_PUBLIC_API_BASE_PATH
: `/${NEXT_PUBLIC_API_BASE_PATH}`;
const NEXT_PUBLIC_ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH
? process.env.NEXT_PUBLIC_ADMIN_BASE_PATH.replace(/\/$/, "")
: "/";
export const app: Express = express();
// Ensure proxy-aware hostname/URL handling (e.g., X-Forwarded-Host/Proto)
// so generated URLs/redirects reflect the public host when behind Nginx.
// See related fix in Remix Express adapter.
app.set("trust proxy", true);
app.use(
"/api",
createProxyMiddleware({
target: NEXT_PUBLIC_API_BASE_URL,
changeOrigin: true,
secure: false,
pathRewrite: (path: string) =>
NORMALIZED_API_BASE_PATH === "/api" ? path : path.replace(/^\/api/, NORMALIZED_API_BASE_PATH),
})
);
const router = express.Router();
router.use(
createRequestHandler({
build: () => import("virtual:react-router/server-build"),
})
);
app.use(NEXT_PUBLIC_ADMIN_BASE_PATH, router);
-21
View File
@@ -296,10 +296,6 @@ body {
}
/* scrollbar style */
::-webkit-scrollbar {
display: none;
}
@-moz-document url-prefix() {
* {
scrollbar-width: none;
@@ -360,23 +356,6 @@ body {
margin-top: 44px;
}
/* scrollbar xs size */
.scrollbar-xs::-webkit-scrollbar {
height: 10px;
width: 10px;
}
.scrollbar-xs::-webkit-scrollbar-thumb {
border: 3px solid rgba(0, 0, 0, 0);
}
.shadow-custom {
box-shadow: 2px 2px 8px 2px rgba(234, 231, 250, 0.3); /* Convert #EAE7FA4D to rgba */
}
/* backdrop filter */
.backdrop-blur-custom {
@apply backdrop-filter blur-[9px];
}
/* scrollbar sm size */
.scrollbar-sm::-webkit-scrollbar {
height: 12px;
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const sharedConfig = require("@plane/tailwind-config/tailwind.config.js");
module.exports = {
+8 -13
View File
@@ -1,21 +1,16 @@
{
"extends": "@plane/typescript-config/react-router.json",
"compilerOptions": {
"noImplicitOverride": false,
"exactOptionalPropertyTypes": false,
"noUnusedParameters": false,
"noUnusedLocals": false,
"baseUrl": ".",
"rootDirs": [".", "./.react-router/types"],
"types": ["vite/client"],
"types": ["node", "vite/client"],
"paths": {
"package.json": ["./package.json"],
"ce/*": ["./ce/*"],
"@/app/*": ["./app/*"],
"@/*": ["./core/*"],
"@/plane-admin/*": ["./ce/*"],
"@/ce/*": ["./ce/*"],
"@/styles/*": ["./styles/*"]
}
"@/app/*": ["app/*"],
"@/*": ["core/*"],
"@/plane-admin/*": ["ce/*"],
"@/styles/*": ["styles/*"]
},
"strictNullChecks": true
},
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
"exclude": ["node_modules"]
+49 -31
View File
@@ -1,41 +1,59 @@
import path from "node:path";
import * as dotenv from "@dotenvx/dotenvx";
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { joinUrlPath } from "@plane/utils";
dotenv.config({ path: path.resolve(__dirname, ".env") });
const PUBLIC_ENV_KEYS = [
"NEXT_PUBLIC_API_BASE_URL",
"NEXT_PUBLIC_API_BASE_PATH",
"NEXT_PUBLIC_ADMIN_BASE_URL",
"NEXT_PUBLIC_ADMIN_BASE_PATH",
"NEXT_PUBLIC_SPACE_BASE_URL",
"NEXT_PUBLIC_SPACE_BASE_PATH",
"NEXT_PUBLIC_LIVE_BASE_URL",
"NEXT_PUBLIC_LIVE_BASE_PATH",
"NEXT_PUBLIC_WEB_BASE_URL",
"NEXT_PUBLIC_WEB_BASE_PATH",
"NEXT_PUBLIC_WEBSITE_URL",
"NEXT_PUBLIC_SUPPORT_EMAIL",
];
// Expose only vars starting with VITE_
const viteEnv = Object.keys(process.env)
.filter((k) => k.startsWith("VITE_"))
.reduce<Record<string, string>>((a, k) => {
a[k] = process.env[k] ?? "";
return a;
}, {});
const publicEnv = PUBLIC_ENV_KEYS.reduce<Record<string, string>>((acc, key) => {
acc[key] = process.env[key] ?? "";
return acc;
}, {});
const basePath = joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ?? "", "/") ?? "/";
export default defineConfig(({ isSsrBuild }) => {
// Only produce an SSR bundle when explicitly enabled.
// For static deployments (default), we skip the server build entirely.
const enableSsrBuild = process.env.ADMIN_ENABLE_SSR_BUILD === "true";
const basePath = joinUrlPath(process.env.NEXT_PUBLIC_ADMIN_BASE_PATH ?? "", "/") ?? "/";
export default defineConfig(() => ({
base: basePath,
define: {
"process.env": JSON.stringify(viteEnv),
},
build: {
assetsInlineLimit: 0,
},
plugins: [reactRouter(), tsconfigPaths({ projects: [path.resolve(__dirname, "tsconfig.json")] })],
resolve: {
alias: {
// Next.js compatibility shims used within admin
"next/link": path.resolve(__dirname, "app/compat/next/link.tsx"),
"next/navigation": path.resolve(__dirname, "app/compat/next/navigation.ts"),
return {
base: basePath,
define: {
"process.env": JSON.stringify(publicEnv),
},
dedupe: ["react", "react-dom"],
},
server: {
host: "127.0.0.1",
},
// No SSR-specific overrides needed; alias resolves to ESM build
}));
build: {
assetsInlineLimit: 0,
rollupOptions:
isSsrBuild && enableSsrBuild
? {
input: path.resolve(__dirname, "server/app.ts"),
}
: undefined,
},
plugins: [reactRouter(), tsconfigPaths({ projects: [path.resolve(__dirname, "tsconfig.json")] })],
resolve: {
alias: {
// Next.js compatibility shims used within admin
"next/image": path.resolve(__dirname, "app/compat/next/image.tsx"),
"next/link": path.resolve(__dirname, "app/compat/next/link.tsx"),
"next/navigation": path.resolve(__dirname, "app/compat/next/navigation.ts"),
},
dedupe: ["react", "react-dom"],
},
// No SSR-specific overrides needed; alias resolves to ESM build
};
});

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