Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1d6282d23 | |||
| bec90c2eb9 | |||
| 60cab0c964 |
@@ -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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"plugins": ["@prettier/plugin-oxc"]
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
.next/*
|
||||
.react-router/*
|
||||
.vite/*
|
||||
out/*
|
||||
public/*
|
||||
dist/*
|
||||
|
||||
@@ -3,6 +3,7 @@ module.exports = {
|
||||
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",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
.next
|
||||
.react-router
|
||||
.vite
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"plugins": ["@prettier/plugin-oxc"]
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
|
||||
+25
-25
@@ -28,35 +28,35 @@ 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/ .
|
||||
@@ -66,7 +66,7 @@ COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store 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 --prod=false
|
||||
|
||||
# Build only the admin package
|
||||
RUN pnpm turbo run build --filter=admin
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
@@ -222,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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -209,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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { KeyRound, Mails } from "lucide-react";
|
||||
// plane packages
|
||||
@@ -134,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} />,
|
||||
},
|
||||
{
|
||||
@@ -142,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}
|
||||
@@ -155,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} />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)} />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useLocation, useNavigate, useSearchParams as useSearchParamsRR } from "react-router";
|
||||
import { ensureTrailingSlash } from "./helper";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { SWRConfig } from "swr";
|
||||
import { AppProgressBar } from "@/lib/b-progress";
|
||||
|
||||
+2
-12
@@ -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>
|
||||
|
||||
Vendored
+5
@@ -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,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,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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
}
|
||||
+18
-10
@@ -6,9 +6,9 @@
|
||||
"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 19",
|
||||
@@ -27,22 +27,29 @@
|
||||
"@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:",
|
||||
"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:"
|
||||
@@ -51,15 +58,16 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@prettier/plugin-oxc": "0.0.4",
|
||||
"@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:",
|
||||
"dotenv": "^16.4.5",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite": "7.1.7",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
+49
-31
@@ -1,41 +1,59 @@
|
||||
import path from "node:path";
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import dotenv from "dotenv";
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .issue import IssueExpandSerializer
|
||||
from plane.db.models import IntakeIssue, Issue, State, StateGroup
|
||||
from plane.db.models import IntakeIssue, Issue
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
@@ -103,52 +103,6 @@ class IntakeIssueUpdateSerializer(BaseSerializer):
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
Validate that if status is being changed to accepted (1),
|
||||
the project has a default state to transition to.
|
||||
"""
|
||||
|
||||
# Check if status is being updated to accepted
|
||||
if attrs.get("status") == 1:
|
||||
intake_issue = self.instance
|
||||
issue = intake_issue.issue
|
||||
|
||||
# Check if issue is in TRIAGE state
|
||||
if issue.state and issue.state.group == StateGroup.TRIAGE.value:
|
||||
# Verify default state exists before allowing the update
|
||||
default_state = State.objects.filter(
|
||||
workspace=intake_issue.workspace, project=intake_issue.project, default=True
|
||||
).first()
|
||||
|
||||
if not default_state:
|
||||
raise serializers.ValidationError(
|
||||
{"status": "Cannot accept intake issue: No default state found for the project"}
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""
|
||||
Update intake issue and transition associated issue state if accepted.
|
||||
"""
|
||||
|
||||
# Update the intake issue with validated data
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
# If status is accepted (1), update the associated issue state from TRIAGE to default
|
||||
if validated_data.get("status") == 1:
|
||||
issue = instance.issue
|
||||
if issue.state and issue.state.group == StateGroup.TRIAGE.value:
|
||||
# Get the default project state
|
||||
default_state = State.objects.filter(
|
||||
workspace=instance.workspace, project=instance.project, default=True
|
||||
).first()
|
||||
if default_state:
|
||||
issue.state = default_state
|
||||
issue.save()
|
||||
return instance
|
||||
|
||||
|
||||
class IssueDataSerializer(serializers.Serializer):
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import State, StateGroup
|
||||
from rest_framework import serializers
|
||||
from plane.db.models import State
|
||||
|
||||
|
||||
class StateSerializer(BaseSerializer):
|
||||
@@ -16,9 +15,6 @@ class StateSerializer(BaseSerializer):
|
||||
# If the default is being provided then make all other states default False
|
||||
if data.get("default", False):
|
||||
State.objects.filter(project_id=self.context.get("project_id")).update(default=False)
|
||||
|
||||
if data.get("group", None) == StateGroup.TRIAGE.value:
|
||||
raise serializers.ValidationError("Cannot create triage state")
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -23,7 +23,7 @@ from plane.api.serializers import (
|
||||
)
|
||||
from plane.app.permissions import ProjectLitePermission
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State, StateGroup
|
||||
from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State
|
||||
from plane.utils.host import base_host
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models.intake import SourceType
|
||||
@@ -165,20 +165,6 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
]:
|
||||
return Response({"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# get the triage state
|
||||
triage_state = State.triage_objects.filter(project_id=project_id, workspace__slug=slug).first()
|
||||
|
||||
if not triage_state:
|
||||
triage_state = State.objects.create(
|
||||
name="Triage",
|
||||
group=StateGroup.TRIAGE.value,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
color="#4E5355",
|
||||
sequence=65000,
|
||||
default=False,
|
||||
)
|
||||
|
||||
# create an issue
|
||||
issue = Issue.objects.create(
|
||||
name=request.data.get("issue", {}).get("name"),
|
||||
@@ -186,7 +172,6 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
description_html=request.data.get("issue", {}).get("description_html", "<p></p>"),
|
||||
priority=request.data.get("issue", {}).get("priority", "none"),
|
||||
project_id=project_id,
|
||||
state_id=triage_state.id,
|
||||
)
|
||||
|
||||
# create an intake issue
|
||||
@@ -335,10 +320,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Get issue data
|
||||
issue_data = request.data.pop("issue", False)
|
||||
issue_serializer = None
|
||||
intake_serializer = None
|
||||
|
||||
# Validate issue data if provided
|
||||
if bool(issue_data):
|
||||
issue = Issue.objects.annotate(
|
||||
label_ids=Coalesce(
|
||||
@@ -362,7 +344,6 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
).get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
|
||||
# Only allow guests to edit name and description
|
||||
if project_member.role <= 5:
|
||||
issue_data = {
|
||||
@@ -373,55 +354,71 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
|
||||
issue_serializer = IssueSerializer(issue, data=issue_data, partial=True)
|
||||
|
||||
if not issue_serializer.is_valid():
|
||||
if issue_serializer.is_valid():
|
||||
current_instance = issue
|
||||
# Log all the updates
|
||||
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
||||
if issue is not None:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
intake=(intake_issue.id),
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Only project admins and members can edit intake issue attributes
|
||||
if project_member.role > 15:
|
||||
intake_serializer = IntakeIssueUpdateSerializer(intake_issue, data=request.data, partial=True)
|
||||
|
||||
if not intake_serializer.is_valid():
|
||||
return Response(intake_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Both serializers are valid, now save them
|
||||
if issue_serializer:
|
||||
current_instance = issue
|
||||
# Log all the updates
|
||||
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
issue_serializer.save()
|
||||
|
||||
# Save intake issue (state transition happens in serializer's update method)
|
||||
if intake_serializer:
|
||||
serializer = IntakeIssueUpdateSerializer(intake_issue, data=request.data, partial=True)
|
||||
current_instance = json.dumps(IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder)
|
||||
intake_serializer.save()
|
||||
|
||||
# create a activity for status change
|
||||
issue_activity.delay(
|
||||
type="intake.activity.created",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=False,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
return Response(IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
# Update the issue state if the issue is rejected or marked as duplicate
|
||||
if serializer.data["status"] in [-1, 2]:
|
||||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
state = State.objects.filter(group="cancelled", workspace__slug=slug, project_id=project_id).first()
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
issue.save()
|
||||
|
||||
# Update the issue state if it is accepted
|
||||
if serializer.data["status"] in [1]:
|
||||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
|
||||
# Update the issue state only if it is in triage state
|
||||
if issue.state.is_triage:
|
||||
# Move to default state
|
||||
state = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).first()
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
issue.save()
|
||||
|
||||
# create a activity for status change
|
||||
issue_activity.delay(
|
||||
type="intake.activity.created",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=False,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
serializer = IntakeIssueSerializer(intake_issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ from plane.db.models import (
|
||||
DeployBoard,
|
||||
ProjectMember,
|
||||
State,
|
||||
DEFAULT_STATES,
|
||||
Workspace,
|
||||
UserFavorite,
|
||||
)
|
||||
@@ -233,6 +232,41 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
user_id=serializer.instance.project_lead,
|
||||
)
|
||||
|
||||
# Default states
|
||||
states = [
|
||||
{
|
||||
"name": "Backlog",
|
||||
"color": "#60646C",
|
||||
"sequence": 15000,
|
||||
"group": "backlog",
|
||||
"default": True,
|
||||
},
|
||||
{
|
||||
"name": "Todo",
|
||||
"color": "#60646C",
|
||||
"sequence": 25000,
|
||||
"group": "unstarted",
|
||||
},
|
||||
{
|
||||
"name": "In Progress",
|
||||
"color": "#F59E0B",
|
||||
"sequence": 35000,
|
||||
"group": "started",
|
||||
},
|
||||
{
|
||||
"name": "Done",
|
||||
"color": "#46A758",
|
||||
"sequence": 45000,
|
||||
"group": "completed",
|
||||
},
|
||||
{
|
||||
"name": "Cancelled",
|
||||
"color": "#9AA4BC",
|
||||
"sequence": 55000,
|
||||
"group": "cancelled",
|
||||
},
|
||||
]
|
||||
|
||||
State.objects.bulk_create(
|
||||
[
|
||||
State(
|
||||
@@ -245,7 +279,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
default=state.get("default", False),
|
||||
created_by=request.user,
|
||||
)
|
||||
for state in DEFAULT_STATES
|
||||
for state in states
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ from .project import (
|
||||
ProjectMemberAdminSerializer,
|
||||
ProjectPublicMemberSerializer,
|
||||
ProjectMemberRoleSerializer,
|
||||
ProjectMemberPreferenceSerializer,
|
||||
)
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
from .view import IssueViewSerializer, ViewIssueListSerializer
|
||||
|
||||
@@ -7,7 +7,7 @@ from .issue import IssueIntakeSerializer, LabelLiteSerializer, IssueDetailSerial
|
||||
from .project import ProjectLiteSerializer
|
||||
from .state import StateLiteSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from plane.db.models import Intake, IntakeIssue, Issue, StateGroup, State
|
||||
from plane.db.models import Intake, IntakeIssue, Issue
|
||||
|
||||
|
||||
class IntakeSerializer(BaseSerializer):
|
||||
@@ -36,49 +36,6 @@ class IntakeIssueSerializer(BaseSerializer):
|
||||
]
|
||||
read_only_fields = ["project", "workspace"]
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
Validate that if status is being changed to accepted (1),
|
||||
the project has a default state to transition to.
|
||||
"""
|
||||
|
||||
# Check if status is being updated to accepted
|
||||
if attrs.get("status") == 1:
|
||||
intake_issue = self.instance
|
||||
issue = intake_issue.issue
|
||||
|
||||
# Check if issue is in TRIAGE state
|
||||
if issue.state and issue.state.group == StateGroup.TRIAGE.value:
|
||||
# Verify default state exists before allowing the update
|
||||
default_state = State.objects.filter(
|
||||
workspace=intake_issue.workspace, project=intake_issue.project, default=True
|
||||
).first()
|
||||
|
||||
if not default_state:
|
||||
raise serializers.ValidationError(
|
||||
{"status": "Cannot accept intake issue: No default state found for the project"}
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# Update the intake issue
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
# If status is accepted (1), transition the issue state from TRIAGE to default
|
||||
if validated_data.get("status") == 1:
|
||||
issue = instance.issue
|
||||
if issue.state and issue.state.group == StateGroup.TRIAGE.value:
|
||||
# Get the default project state
|
||||
default_state = State.objects.filter(
|
||||
workspace=instance.workspace, project=instance.project, default=True
|
||||
).first()
|
||||
if default_state:
|
||||
issue.state = default_state
|
||||
issue.save()
|
||||
|
||||
return instance
|
||||
|
||||
def to_representation(self, instance):
|
||||
# Pass the annotated fields to the Issue instance if they exist
|
||||
if hasattr(instance, "label_ids"):
|
||||
|
||||
@@ -78,7 +78,7 @@ class IssueProjectLiteSerializer(BaseSerializer):
|
||||
class IssueCreateSerializer(BaseSerializer):
|
||||
# ids
|
||||
state_id = serializers.PrimaryKeyRelatedField(
|
||||
source="state", queryset=State.all_state_objects.all(), required=False, allow_null=True
|
||||
source="state", queryset=State.objects.all(), required=False, allow_null=True
|
||||
)
|
||||
parent_id = serializers.PrimaryKeyRelatedField(
|
||||
source="parent", queryset=Issue.objects.all(), required=False, allow_null=True
|
||||
@@ -117,9 +117,6 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
return data
|
||||
|
||||
def validate(self, attrs):
|
||||
allow_triage = self.context.get("allow_triage_state", False)
|
||||
state_manager = State.triage_objects if allow_triage else State.objects
|
||||
|
||||
if (
|
||||
attrs.get("start_date", None) is not None
|
||||
and attrs.get("target_date", None) is not None
|
||||
@@ -163,7 +160,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
# Check state is from the project only else raise validation error
|
||||
if (
|
||||
attrs.get("state")
|
||||
and not state_manager.filter(
|
||||
and not State.objects.filter(
|
||||
project_id=self.context.get("project_id"),
|
||||
pk=attrs.get("state").id,
|
||||
).exists()
|
||||
@@ -367,19 +364,6 @@ class LabelSerializer(BaseSerializer):
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
def validate_name(self, value):
|
||||
project_id = self.context.get("project_id")
|
||||
|
||||
label = Label.objects.filter(project_id=project_id, name__iexact=value)
|
||||
|
||||
if self.instance:
|
||||
label = label.exclude(id=self.instance.pk)
|
||||
|
||||
if label.exists():
|
||||
raise serializers.ValidationError(detail="LABEL_NAME_ALREADY_EXISTS")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class LabelLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
@@ -798,14 +782,6 @@ class IssueSerializer(DynamicBaseSerializer):
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
data.get("state_id")
|
||||
and not State.objects.filter(project_id=self.context.get("project_id"), pk=data.get("state_id")).exists()
|
||||
):
|
||||
raise serializers.ValidationError("State is not valid please pass a valid state_id")
|
||||
return data
|
||||
|
||||
|
||||
class IssueListDetailSerializer(serializers.Serializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -142,18 +142,6 @@ class ProjectMemberSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProjectMemberPreferenceSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = ProjectMember
|
||||
fields = ["preferences", "project_id", "member_id", "workspace_id"]
|
||||
|
||||
def validate_preferences(self, value):
|
||||
preferences = self.instance.preferences
|
||||
|
||||
preferences.update(value)
|
||||
return preferences
|
||||
|
||||
|
||||
class ProjectMemberAdminSerializer(BaseSerializer):
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
project = ProjectLiteSerializer(read_only=True)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from .base import BaseSerializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from plane.db.models import State, StateGroup
|
||||
from plane.db.models import State
|
||||
|
||||
|
||||
class StateSerializer(BaseSerializer):
|
||||
@@ -24,11 +24,6 @@ class StateSerializer(BaseSerializer):
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs.get("group") == StateGroup.TRIAGE.value:
|
||||
raise serializers.ValidationError("Cannot create triage state")
|
||||
return attrs
|
||||
|
||||
|
||||
class StateLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
|
||||
@@ -13,7 +13,6 @@ from plane.app.views import (
|
||||
ProjectAssetEndpoint,
|
||||
ProjectBulkAssetEndpoint,
|
||||
AssetCheckEndpoint,
|
||||
DuplicateAssetEndpoint,
|
||||
WorkspaceAssetDownloadEndpoint,
|
||||
ProjectAssetDownloadEndpoint,
|
||||
)
|
||||
@@ -92,11 +91,6 @@ urlpatterns = [
|
||||
AssetCheckEndpoint.as_view(),
|
||||
name="asset-check",
|
||||
),
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/duplicate-assets/<uuid:asset_id>/",
|
||||
DuplicateAssetEndpoint.as_view(),
|
||||
name="duplicate-assets",
|
||||
),
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/download/<uuid:asset_id>/",
|
||||
WorkspaceAssetDownloadEndpoint.as_view(),
|
||||
|
||||
@@ -14,7 +14,6 @@ from plane.app.views import (
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
UserProjectRolesEndpoint,
|
||||
ProjectArchiveUnarchiveEndpoint,
|
||||
ProjectMemberPreferenceEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -126,9 +125,4 @@ urlpatterns = [
|
||||
ProjectArchiveUnarchiveEndpoint.as_view(),
|
||||
name="project-archive-unarchive",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/preferences/member/<uuid:member_id>/",
|
||||
ProjectMemberPreferenceEndpoint.as_view(),
|
||||
name="project-member-preference",
|
||||
),
|
||||
]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user