Compare commits

..

1 Commits

Author SHA1 Message Date
Palanikannan M 202336dd9c fix: rendering node views reliably 2025-12-01 16:15:33 +05:30
1969 changed files with 26287 additions and 32572 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
name: Bug report
description: Create a bug report to help us improve Plane
title: "[bug]: "
labels: [🐛bug, plane]
labels: [🐛bug]
assignees: [vihar, pushya22]
body:
- type: markdown
@@ -1,7 +1,7 @@
name: Feature request
description: Suggest a feature to improve Plane
title: "[feature]: "
labels: [✨feature, plane]
labels: [✨feature]
assignees: [vihar, pushya22]
body:
- type: markdown
@@ -27,13 +27,11 @@ jobs:
github.event.pull_request.draft == false &&
github.event.pull_request.requested_reviewers != null
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: "3.12.x"
cache: 'pip'
cache-dependency-path: 'apps/api/requirements.txt'
python-version: "3.x"
- name: Install Pylint
run: python -m pip install ruff
- name: Install API Dependencies
@@ -17,11 +17,10 @@ concurrency:
cancel-in-progress: true
jobs:
# Format check has no build dependencies - run immediately in parallel
check-format:
name: check:format
build-and-lint:
name: Build and lint web apps
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 25
if: |
github.event.pull_request.draft == false &&
github.event.pull_request.requested_reviewers != null
@@ -30,140 +29,28 @@ jobs:
TURBO_SCM_HEAD: ${{ github.sha }}
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 50
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
- name: Enable Corepack and pnpm
run: corepack enable pnpm
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check formatting
- name: Lint Affected
run: pnpm turbo run check:lint --affected
- name: Check Affected format
run: pnpm turbo run check:format --affected
# Build packages - required for lint and type checks
build:
name: Build packages
runs-on: ubuntu-latest
timeout-minutes: 15
if: |
github.event.pull_request.draft == false &&
github.event.pull_request.requested_reviewers != null
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
TURBO_SCM_HEAD: ${{ github.sha }}
NODE_OPTIONS: "--max-old-space-size=4096"
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 50
filter: blob:none
- name: Check Affected types
run: pnpm turbo run check:types --affected
- name: Set up Node.js
uses: actions/setup-node@v6
- name: Enable Corepack and pnpm
run: corepack enable pnpm
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Restore Turbo cache
uses: actions/cache/restore@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-
turbo-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
- name: Build Affected
run: pnpm turbo run build --affected
- name: Save Turbo cache
uses: actions/cache/save@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
# Lint and type checks depend on build artifacts
check:
name: ${{ matrix.task }}
runs-on: ubuntu-latest
needs: build
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
task: [check:lint, check:types]
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
TURBO_SCM_HEAD: ${{ github.sha }}
NODE_OPTIONS: "--max-old-space-size=4096"
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 50
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v6
- name: Enable Corepack and pnpm
run: corepack enable pnpm
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Restore Turbo cache
uses: actions/cache/restore@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ${{ matrix.task }}
run: pnpm turbo run ${{ matrix.task }} --affected
+2 -1
View File
@@ -105,8 +105,9 @@ CLAUDE.md
build/
.react-router/
AGENTS.md
build/
.react-router/
AGENTS.md
temp/
scripts/
-1
View File
@@ -1 +0,0 @@
pnpm lint-staged
+25 -45
View File
@@ -1,54 +1,34 @@
# ------------------------------
# Core Workspace Behavior
# ------------------------------
# Enforce pnpm workspace behavior and allow Turbo's lifecycle hooks if scripts are disabled
# This repo uses pnpm with workspaces.
# Always prefer using local workspace packages when available
prefer-workspace-packages = true
# Prefer linking local workspace packages when available
prefer-workspace-packages=true
link-workspace-packages=true
shared-workspace-lockfile=true
# Symlink workspace packages instead of duplicating them
link-workspace-packages = true
# Make peer installs smoother across the monorepo
auto-install-peers=true
strict-peer-dependencies=false
# Use a single lockfile across the whole monorepo
shared-workspace-lockfile = true
# If scripts are disabled (e.g., CI with --ignore-scripts), allowlisted packages can still run their hooks
# Turbo occasionally performs postinstall tasks for optimal performance
# moved to pnpm-workspace.yaml: onlyBuiltDependencies (e.g., allow turbo)
# Ensure packages added from workspace save using workspace: protocol
save-workspace-protocol = true
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=prettier
public-hoist-pattern[]=typescript
# Reproducible installs across CI and dev
prefer-frozen-lockfile=true
# ------------------------------
# Dependency Resolution
# ------------------------------
# Prefer resolving to highest versions in monorepo to reduce duplication
resolution-mode=highest
# Choose the highest compatible version across the workspace
# → reduces fragmentation & node_modules bloat
resolution-mode = highest
# Speed up native module builds by caching side effects
side-effects-cache=true
# Automatically install peer dependencies instead of forcing every package to declare them
auto-install-peers = true
# Speed up local dev by reusing local store when possible
prefer-offline=true
# Don't break the install if peers are missing
strict-peer-dependencies = false
# ------------------------------
# Performance Optimizations
# ------------------------------
# Use cached artifacts for native modules (sharp, esbuild, etc.)
side-effects-cache = true
# Prefer local cached packages rather than hitting network
prefer-offline = true
# In CI, refuse to modify lockfile (prevents drift)
prefer-frozen-lockfile = true
# Use isolated linker (best compatibility with Node ecosystem tools)
node-linker = isolated
# Hoist commonly used tools to the root to prevent duplicates and speed up resolution
public-hoist-pattern[] = typescript
public-hoist-pattern[] = eslint
public-hoist-pattern[] = *@plane/*
public-hoist-pattern[] = vite
public-hoist-pattern[] = turbo
# Ensure workspace protocol is used when adding internal deps
save-workspace-protocol=true
-10
View File
@@ -1,10 +0,0 @@
.next/
.react-router/
.turbo/
.vite/
build/
dist/
node_modules/
out/
pnpm-lock.yaml
storybook-static/
+2 -11
View File
@@ -1,15 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"overrides": [
{
"files": ["packages/codemods/**/*"],
"options": {
"printWidth": 80
}
}
],
"plugins": ["@prettier/plugin-oxc"],
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
"trailingComma": "es5",
"plugins": ["@prettier/plugin-oxc"]
}
-24
View File
@@ -1,24 +0,0 @@
# Agent Development Guide
## Commands
- `pnpm dev` - Start all dev servers (web:3000, admin:3001)
- `pnpm build` - Build all packages and apps
- `pnpm check` - Run all checks (format, lint, types)
- `pnpm check:lint` - ESLint across all packages
- `pnpm check:types` - TypeScript type checking
- `pnpm fix` - Auto-fix format and lint issues
- `pnpm turbo run <command> --filter=<package>` - Target specific package/app
- `pnpm --filter=@plane/ui storybook` - Start Storybook on port 6006
## Code Style
- **Imports**: Use `workspace:*` for internal packages, `catalog:` for external deps
- **TypeScript**: Strict mode enabled, all files must be typed
- **Formatting**: Prettier with Tailwind plugin, run `pnpm fix:format`
- **Linting**: ESLint with shared config, max warnings vary by package
- **Naming**: camelCase for variables/functions, PascalCase for components/types
- **Error Handling**: Use try-catch with proper error types, log errors appropriately
- **State Management**: MobX stores in `packages/shared-state`, reactive patterns
- **Testing**: All features require unit tests, use existing test framework per package
- **Components**: Build in `@plane/ui` with Storybook for isolated development
-1
View File
@@ -1 +0,0 @@
eslint.config.mjs @lifeiscontent
+8 -10
View File
@@ -91,7 +91,7 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
- All features or bug fixes must be tested by one or more specs (unit-tests).
- We lint with [ESLint 9](https://eslint.org/docs/latest/) using the shared `eslint.config.mjs` (type-aware via `typescript-eslint`) and format with [Prettier](https://prettier.io/) using `prettier.config.cjs`.
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
## Ways to contribute
@@ -187,19 +187,18 @@ Adding a new language involves several steps to ensure it integrates seamlessly
Add the new language to the TLanguage type in the language definitions file:
```ts
// packages/i18n/src/types/language.ts
export type TLanguage = "en" | "fr" | "your-lang";
// packages/i18n/src/types/language.ts
export type TLanguage = "en" | "fr" | "your-lang";
```
1. **Add language configuration**
Include the new language in the list of supported languages:
```ts
// packages/i18n/src/constants/language.ts
export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "English", value: "en" },
{ label: "Your Language", value: "your-lang" },
];
// packages/i18n/src/constants/language.ts
export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "English", value: "en" },
{ label: "Your Language", value: "your-lang" }
];
```
2. **Create translation files**
@@ -211,7 +210,6 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
3. **Update import logic**
Modify the language import logic to include your new language:
```ts
private importLanguageFile(language: TLanguage): Promise<any> {
switch (language) {
+4 -2
View File
@@ -54,7 +54,7 @@ Getting started with Plane is simple. Choose the setup that works best for you:
## 🌟 Features
- **Work Items**
- **Issues**
Efficiently create and manage tasks with a robust rich text editor that supports file uploads. Enhance organization and tracking by adding sub-properties and referencing related issues.
- **Cycles**
@@ -72,13 +72,15 @@ Getting started with Plane is simple. Choose the setup that works best for you:
- **Analytics**
Access real-time insights across all your Plane data. Visualize trends, remove blockers, and keep your projects moving forward.
- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution.
## 🛠️ Local development
See [CONTRIBUTING](./CONTRIBUTING.md)
## ⚙️ Built with
[![React Router](https://img.shields.io/badge/-React%20Router-CA4245?logo=react-router&style=for-the-badge&logoColor=white)](https://reactrouter.com/)
[![Next JS](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white)](https://nextjs.org/)
[![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=green)](https://www.djangoproject.com/)
[![Node JS](https://img.shields.io/badge/node.js-339933?style=for-the-badge&logo=Node.js&logoColor=white)](https://nodejs.org/en)
+14
View File
@@ -0,0 +1,14 @@
.next/*
.react-router/*
.vite/*
out/*
public/*
dist/*
node_modules/*
.turbo/*
.env*
.env
.env.local
.env.development
.env.production
.env.test
+18
View File
@@ -0,0 +1,18 @@
module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
ignorePatterns: ["build/**", "dist/**", ".vite/**"],
rules: {
"import/no-duplicates": ["error", { "prefer-inline": false }],
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/consistent-type-imports": [
"error",
{
prefer: "type-imports",
fixStyle: "separate-type-imports",
disallowTypeAnnotations: false,
},
],
},
};
+7 -9
View File
@@ -1,10 +1,8 @@
.next/
.react-router/
.turbo/
.vite/
build/
dist/
node_modules/
.next
.react-router
.vite
.vercel
.tubro
out/
pnpm-lock.yaml
storybook-static/
dist/
build/
+6
View File
@@ -0,0 +1,6 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5",
"plugins": ["@prettier/plugin-oxc"]
}
+4 -6
View File
@@ -13,7 +13,7 @@ RUN corepack enable pnpm
FROM base AS builder
RUN pnpm add -g turbo@2.6.3
RUN pnpm add -g turbo@2.5.8
COPY . .
@@ -62,12 +62,10 @@ COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
# Copy full directory structure before fetch to ensure all package.json files are available
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
# Fetch dependencies to cache store, then install offline with dev deps
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
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
# Build only the admin package
@@ -75,7 +73,7 @@ RUN pnpm turbo run build --filter=admin
# =========================================================================== #
FROM nginx:1.29-alpine AS production
FROM nginx:1.27-alpine AS production
COPY apps/admin/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=installer /app/apps/admin/build/client /usr/share/nginx/html/god-mode
+9 -9
View File
@@ -42,7 +42,7 @@ export function InstanceAIForm(props: IInstanceAIForm) {
<a
href="https://platform.openai.com/docs/models/overview"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Learn more
@@ -63,7 +63,7 @@ export function InstanceAIForm(props: IInstanceAIForm) {
<a
href="https://platform.openai.com/api-keys"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
@@ -94,8 +94,8 @@ export function InstanceAIForm(props: IInstanceAIForm) {
<div className="space-y-8">
<div className="space-y-3">
<div>
<div className="pb-1 text-18 font-medium text-primary">OpenAI</div>
<div className="text-13 font-regular text-tertiary">If you use ChatGPT, this is for you.</div>
<div className="pb-1 text-xl font-medium text-custom-text-100">OpenAI</div>
<div className="text-sm font-normal text-custom-text-300">If you use ChatGPT, this is for you.</div>
</div>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-x-12 gap-y-8 lg:grid-cols-3">
{aiFormFields.map((field) => (
@@ -114,13 +114,13 @@ export function InstanceAIForm(props: IInstanceAIForm) {
</div>
</div>
<div className="flex flex-col gap-4 items-start">
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving" : "Save changes"}
<div className="space-y-4">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<div className="relative inline-flex items-center gap-1.5 rounded-sm border border-accent-subtle bg-accent-subtle px-4 py-2 text-caption-sm-regular text-accent-secondary ">
<Lightbulb className="size-4" />
<div className="relative inline-flex items-center gap-2 rounded border border-custom-primary-100/20 bg-custom-primary-100/10 px-4 py-2 text-xs text-custom-primary-200">
<Lightbulb height="14" width="14" />
<div>
If you have a preferred AI models vendor, please get in{" "}
<a className="underline font-medium" href="https://plane.so/contact">
+24 -22
View File
@@ -1,13 +1,10 @@
import { observer } from "mobx-react";
import useSWR from "swr";
import { Loader } from "@plane/ui";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks
import { useInstance } from "@/hooks/store";
// types
// components
import type { Route } from "./+types/page";
// local
import { InstanceAIForm } from "./form";
const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentProps) {
@@ -17,25 +14,30 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return (
<PageWrapper
header={{
title: "AI features for all your workspaces",
description: "Configure your AI API credentials so Plane AI features are turned on for all your workspaces.",
}}
>
{formattedConfig ? (
<InstanceAIForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="40%" />
<div className="w-2/3 grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<>
<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">AI features for all your workspaces</div>
<div className="text-sm font-normal text-custom-text-300">
Configure your AI API credentials so Plane AI features are turned on for all your workspaces.
</div>
<Loader.Item height="50px" width="20%" />
</Loader>
)}
</PageWrapper>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceAIForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="40%" />
<div className="w-2/3 grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</div>
<Loader.Item height="50px" width="20%" />
</Loader>
)}
</div>
</div>
</>
);
});
@@ -4,16 +4,15 @@ import Link from "next/link";
import { useForm } from "react-hook-form";
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceGiteaAuthenticationConfigurationKeys } from "@plane/types";
import { Button, getButtonStyling } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { CodeBlock } from "@/components/common/code-block";
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import { ControllerInput } from "@/components/common/controller-input";
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
import { ControllerSwitch } from "@/components/common/controller-switch";
import type { TCopyField } from "@/components/common/copy-field";
import { CopyField } from "@/components/common/copy-field";
// hooks
@@ -42,7 +41,6 @@ export function InstanceGiteaConfigForm(props: Props) {
GITEA_HOST: config["GITEA_HOST"] || "https://gitea.com",
GITEA_CLIENT_ID: config["GITEA_CLIENT_ID"],
GITEA_CLIENT_SECRET: config["GITEA_CLIENT_SECRET"],
ENABLE_GITEA_SYNC: config["ENABLE_GITEA_SYNC"] || "0",
},
});
@@ -71,7 +69,7 @@ export function InstanceGiteaConfigForm(props: Props) {
tabIndex={-1}
href="https://gitea.com/user/settings/applications"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Gitea OAuth application settings.
@@ -93,7 +91,7 @@ export function InstanceGiteaConfigForm(props: Props) {
tabIndex={-1}
href="https://gitea.com/user/settings/applications"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Gitea OAuth application settings.
@@ -106,11 +104,6 @@ export function InstanceGiteaConfigForm(props: Props) {
},
];
const GITEA_FORM_SWITCH_FIELD: TControllerSwitchFormField<GiteaConfigFormValues> = {
name: "ENABLE_GITEA_SYNC",
label: "Gitea",
};
const GITEA_SERVICE_FIELD: TCopyField[] = [
{
key: "Callback_URI",
@@ -124,7 +117,7 @@ export function InstanceGiteaConfigForm(props: Props) {
tabIndex={-1}
href={`${control._formValues.GITEA_HOST || "https://gitea.com"}/user/settings/applications`}
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
@@ -137,22 +130,20 @@ export function InstanceGiteaConfigForm(props: Props) {
const onSubmit = async (formData: GiteaConfigFormValues) => {
const payload: Partial<GiteaConfigFormValues> = { ...formData };
try {
const response = await updateInstanceConfigurations(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your Gitea authentication is configured. You should test it now.",
});
reset({
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value,
GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value,
ENABLE_GITEA_SYNC: response.find((item) => item.key === "ENABLE_GITEA_SYNC")?.value,
});
} catch (err) {
console.error(err);
}
await updateInstanceConfigurations(payload)
.then((response = []) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your Gitea authentication is configured. You should test it now.",
});
reset({
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value,
GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value,
});
})
.catch((err) => console.error(err));
};
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
@@ -172,7 +163,7 @@ export function InstanceGiteaConfigForm(props: Props) {
<div className="flex flex-col gap-8">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">Gitea-provided details for Plane</div>
<div className="pt-2.5 text-xl font-medium">Gitea-provided details for Plane</div>
{GITEA_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
@@ -186,27 +177,24 @@ export function InstanceGiteaConfigForm(props: Props) {
required={field.required}
/>
))}
<ControllerSwitch control={control} field={GITEA_FORM_SWITCH_FIELD} />
<div className="flex flex-col gap-1 pt-4">
<div className="flex items-center gap-4">
<Button
variant="primary"
size="lg"
onClick={(e) => void handleSubmit(onSubmit)(e)}
loading={isSubmitting}
disabled={!isDirty}
>
{isSubmitting ? "Saving" : "Save changes"}
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
<Link
href="/authentication"
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back
</Link>
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-1 rounded-lg">
<div className="pt-2 text-18 font-medium">Plane-provided details for Gitea</div>
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
<div className="pt-2 text-xl font-medium">Plane-provided details for Gitea</div>
{GITEA_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -4,16 +4,13 @@ import useSWR from "swr";
// plane internal packages
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// assets
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
// components
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks
import { useInstance } from "@/hooks/store";
// types
//local components
import type { Route } from "./+types/page";
// local
import { InstanceGiteaConfigForm } from "./form";
const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthenticationPage() {
@@ -35,7 +32,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration",
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`,
@@ -59,39 +56,42 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
const isGiteaEnabled = enableGiteaConfig === "1";
return (
<PageWrapper
customHeader={
<AuthenticationMethodCard
name="Gitea"
description="Allow members to login or sign up to plane with their Gitea accounts."
icon={<img src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
config={
<ToggleSwitch
value={isGiteaEnabled}
onChange={() => {
updateConfig("IS_GITEA_ENABLED", isGiteaEnabled ? "0" : "1");
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
}
>
{formattedConfig ? (
<InstanceGiteaConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</PageWrapper>
<>
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
name="Gitea"
description="Allow members to login or sign up to plane with their Gitea accounts."
icon={<img src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
config={
<ToggleSwitch
value={isGiteaEnabled}
onChange={() => {
updateConfig("IS_GITEA_ENABLED", isGiteaEnabled ? "0" : "1");
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceGiteaConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</div>
</div>
</>
);
});
export const meta: Route.MetaFunction = () => [{ title: "Gitea Authentication - God Mode" }];
@@ -8,12 +8,12 @@ import { API_BASE_URL } from "@plane/constants";
import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigurationKeys } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { CodeBlock } from "@/components/common/code-block";
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
import { ControllerSwitch } from "@/components/common/controller-switch";
import { ControllerInput } from "@/components/common/controller-input";
import type { TCopyField } from "@/components/common/copy-field";
import { CopyField } from "@/components/common/copy-field";
@@ -43,7 +43,6 @@ export function InstanceGithubConfigForm(props: Props) {
GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"],
GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"],
GITHUB_ORGANIZATION_ID: config["GITHUB_ORGANIZATION_ID"],
ENABLE_GITHUB_SYNC: config["ENABLE_GITHUB_SYNC"] || "0",
},
});
@@ -61,7 +60,7 @@ export function InstanceGithubConfigForm(props: Props) {
tabIndex={-1}
href="https://github.com/settings/applications/new"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitHub OAuth application settings.
@@ -83,7 +82,7 @@ export function InstanceGithubConfigForm(props: Props) {
tabIndex={-1}
href="https://github.com/settings/applications/new"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitHub OAuth application settings.
@@ -105,11 +104,6 @@ export function InstanceGithubConfigForm(props: Props) {
},
];
const GITHUB_FORM_SWITCH_FIELD: TControllerSwitchFormField<GithubConfigFormValues> = {
name: "ENABLE_GITHUB_SYNC",
label: "GitHub",
};
const GITHUB_COMMON_SERVICE_DETAILS: TCopyField[] = [
{
key: "Origin_URL",
@@ -122,7 +116,7 @@ export function InstanceGithubConfigForm(props: Props) {
tabIndex={-1}
href="https://github.com/settings/applications/new"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
@@ -145,7 +139,7 @@ export function InstanceGithubConfigForm(props: Props) {
tabIndex={-1}
href="https://github.com/settings/applications/new"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
@@ -158,22 +152,20 @@ export function InstanceGithubConfigForm(props: Props) {
const onSubmit = async (formData: GithubConfigFormValues) => {
const payload: Partial<GithubConfigFormValues> = { ...formData };
try {
const response = await updateInstanceConfigurations(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your GitHub authentication is configured. You should test it now.",
});
reset({
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value,
GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value,
ENABLE_GITHUB_SYNC: response.find((item) => item.key === "ENABLE_GITHUB_SYNC")?.value,
});
} catch (err) {
console.error(err);
}
await updateInstanceConfigurations(payload)
.then((response = []) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your GitHub authentication is configured. You should test it now.",
});
reset({
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value,
GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value,
});
})
.catch((err) => console.error(err));
};
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
@@ -193,7 +185,7 @@ export function InstanceGithubConfigForm(props: Props) {
<div className="flex flex-col gap-8">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">GitHub-provided details for Plane</div>
<div className="pt-2.5 text-xl font-medium">GitHub-provided details for Plane</div>
{GITHUB_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
@@ -207,30 +199,27 @@ export function InstanceGithubConfigForm(props: Props) {
required={field.required}
/>
))}
<ControllerSwitch control={control} field={GITHUB_FORM_SWITCH_FIELD} />
<div className="flex flex-col gap-1 pt-4">
<div className="flex items-center gap-4">
<Button
variant="primary"
size="lg"
onClick={(e) => void handleSubmit(onSubmit)(e)}
loading={isSubmitting}
disabled={!isDirty}
>
{isSubmitting ? "Saving" : "Save changes"}
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
<Link
href="/authentication"
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back
</Link>
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
<div className="pt-2 text-18 font-medium">Plane-provided details for GitHub</div>
<div className="pt-2 text-xl font-medium">Plane-provided details for GitHub</div>
<div className="flex flex-col gap-y-4">
{/* common service details */}
<div className="flex flex-col gap-y-4 px-6 py-4 bg-layer-1 rounded-lg">
<div className="flex flex-col gap-y-4 px-6 py-4 bg-custom-background-80 rounded-lg">
{GITHUB_COMMON_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -238,11 +227,11 @@ export function InstanceGithubConfigForm(props: Props) {
{/* web service details */}
<div className="flex flex-col rounded-lg overflow-hidden">
<div className="px-6 py-3 bg-layer-3 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary">
<div className="px-6 py-3 bg-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
<Monitor className="w-3 h-3" />
Web
</div>
<div className="px-6 py-4 flex flex-col gap-y-4 bg-layer-1">
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
{GITHUB_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -6,17 +6,15 @@ import useSWR from "swr";
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
import { resolveGeneralTheme } from "@plane/utils";
// assets
// components
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
// components
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks
import { useInstance } from "@/hooks/store";
// types
// icons
// local components
import type { Route } from "./+types/page";
// local
import { InstanceGithubConfigForm } from "./form";
const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthenticationPage(
@@ -43,7 +41,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration",
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`,
@@ -67,46 +65,49 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
const isGithubEnabled = enableGithubConfig === "1";
return (
<PageWrapper
customHeader={
<AuthenticationMethodCard
name="GitHub"
description="Allow members to login or sign up to plane with their GitHub accounts."
icon={
<img
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={24}
width={24}
alt="GitHub Logo"
/>
}
config={
<ToggleSwitch
value={isGithubEnabled}
onChange={() => {
updateConfig("IS_GITHUB_ENABLED", isGithubEnabled ? "0" : "1");
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
}
>
{formattedConfig ? (
<InstanceGithubConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</PageWrapper>
<>
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
name="GitHub"
description="Allow members to login or sign up to plane with their GitHub accounts."
icon={
<img
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={24}
width={24}
alt="GitHub Logo"
/>
}
config={
<ToggleSwitch
value={isGithubEnabled}
onChange={() => {
updateConfig("IS_GITHUB_ENABLED", isGithubEnabled ? "0" : "1");
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceGithubConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</div>
</div>
</>
);
});
@@ -7,12 +7,11 @@ import { API_BASE_URL } from "@plane/constants";
import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { CodeBlock } from "@/components/common/code-block";
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
import { ControllerSwitch } from "@/components/common/controller-switch";
import { ControllerInput } from "@/components/common/controller-input";
import type { TCopyField } from "@/components/common/copy-field";
import { CopyField } from "@/components/common/copy-field";
@@ -42,7 +41,6 @@ export function InstanceGitlabConfigForm(props: Props) {
GITLAB_HOST: config["GITLAB_HOST"],
GITLAB_CLIENT_ID: config["GITLAB_CLIENT_ID"],
GITLAB_CLIENT_SECRET: config["GITLAB_CLIENT_SECRET"],
ENABLE_GITLAB_SYNC: config["ENABLE_GITLAB_SYNC"] || "0",
},
});
@@ -73,7 +71,7 @@ export function InstanceGitlabConfigForm(props: Props) {
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitLab OAuth application settings
@@ -96,7 +94,7 @@ export function InstanceGitlabConfigForm(props: Props) {
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitLab OAuth application settings
@@ -110,11 +108,6 @@ export function InstanceGitlabConfigForm(props: Props) {
},
];
const GITLAB_FORM_SWITCH_FIELD: TControllerSwitchFormField<GitlabConfigFormValues> = {
name: "ENABLE_GITLAB_SYNC",
label: "GitLab",
};
const GITLAB_SERVICE_FIELD: TCopyField[] = [
{
key: "Callback_URL",
@@ -127,7 +120,7 @@ export function InstanceGitlabConfigForm(props: Props) {
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitLab OAuth application
@@ -141,22 +134,20 @@ export function InstanceGitlabConfigForm(props: Props) {
const onSubmit = async (formData: GitlabConfigFormValues) => {
const payload: Partial<GitlabConfigFormValues> = { ...formData };
try {
const response = await updateInstanceConfigurations(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your GitLab authentication is configured. You should test it now.",
});
reset({
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value,
GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.value,
ENABLE_GITLAB_SYNC: response.find((item) => item.key === "ENABLE_GITLAB_SYNC")?.value,
});
} catch (err) {
console.error(err);
}
await updateInstanceConfigurations(payload)
.then((response = []) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your GitLab authentication is configured. You should test it now.",
});
reset({
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value,
GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.value,
});
})
.catch((err) => console.error(err));
};
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
@@ -176,7 +167,7 @@ export function InstanceGitlabConfigForm(props: Props) {
<div className="flex flex-col gap-8">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">GitLab-provided details for Plane</div>
<div className="pt-2.5 text-xl font-medium">GitLab-provided details for Plane</div>
{GITLAB_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
@@ -190,27 +181,24 @@ export function InstanceGitlabConfigForm(props: Props) {
required={field.required}
/>
))}
<ControllerSwitch control={control} field={GITLAB_FORM_SWITCH_FIELD} />
<div className="flex flex-col gap-1 pt-4">
<div className="flex items-center gap-4">
<Button
variant="primary"
size="lg"
onClick={(e) => void handleSubmit(onSubmit)(e)}
loading={isSubmitting}
disabled={!isDirty}
>
{isSubmitting ? "Saving" : "Save changes"}
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
<Link
href="/authentication"
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back
</Link>
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-3 rounded-lg">
<div className="pt-2 text-18 font-medium">Plane-provided details for GitLab</div>
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
<div className="pt-2 text-xl font-medium">Plane-provided details for GitLab</div>
{GITLAB_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -3,16 +3,14 @@ import { observer } from "mobx-react";
import useSWR from "swr";
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// assets
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
// components
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks
import { useInstance } from "@/hooks/store";
// types
// icons
// local components
import type { Route } from "./+types/page";
// local
import { InstanceGitlabConfigForm } from "./form";
const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthenticationPage(
@@ -37,7 +35,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration",
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`,
@@ -58,43 +56,46 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
});
};
return (
<PageWrapper
customHeader={
<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" />}
config={
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
onChange={() => {
if (Boolean(parseInt(enableGitlabConfig)) === true) {
updateConfig("IS_GITLAB_ENABLED", "0");
} else {
updateConfig("IS_GITLAB_ENABLED", "1");
}
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
}
>
{formattedConfig ? (
<InstanceGitlabConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</PageWrapper>
<>
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
name="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" />}
config={
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
onChange={() => {
if (Boolean(parseInt(enableGitlabConfig)) === true) {
updateConfig("IS_GITLAB_ENABLED", "0");
} else {
updateConfig("IS_GITLAB_ENABLED", "1");
}
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceGitlabConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</div>
</div>
</>
);
});
@@ -8,12 +8,11 @@ import { API_BASE_URL } from "@plane/constants";
import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigurationKeys } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { CodeBlock } from "@/components/common/code-block";
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
import { ControllerSwitch } from "@/components/common/controller-switch";
import { ControllerInput } from "@/components/common/controller-input";
import type { TCopyField } from "@/components/common/copy-field";
import { CopyField } from "@/components/common/copy-field";
@@ -42,7 +41,6 @@ export function InstanceGoogleConfigForm(props: Props) {
defaultValues: {
GOOGLE_CLIENT_ID: config["GOOGLE_CLIENT_ID"],
GOOGLE_CLIENT_SECRET: config["GOOGLE_CLIENT_SECRET"],
ENABLE_GOOGLE_SYNC: config["ENABLE_GOOGLE_SYNC"] || "0",
},
});
@@ -60,7 +58,7 @@ export function InstanceGoogleConfigForm(props: Props) {
tabIndex={-1}
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Learn more
@@ -82,7 +80,7 @@ export function InstanceGoogleConfigForm(props: Props) {
tabIndex={-1}
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Learn more
@@ -95,11 +93,6 @@ export function InstanceGoogleConfigForm(props: Props) {
},
];
const GOOGLE_FORM_SWITCH_FIELD: TControllerSwitchFormField<GoogleConfigFormValues> = {
name: "ENABLE_GOOGLE_SYNC",
label: "Google",
};
const GOOGLE_COMMON_SERVICE_DETAILS: TCopyField[] = [
{
key: "Origin_URL",
@@ -112,7 +105,7 @@ export function InstanceGoogleConfigForm(props: Props) {
<a
href="https://console.cloud.google.com/apis/credentials/oauthclient"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
@@ -134,7 +127,7 @@ export function InstanceGoogleConfigForm(props: Props) {
<a
href="https://console.cloud.google.com/apis/credentials/oauthclient"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
@@ -147,21 +140,19 @@ export function InstanceGoogleConfigForm(props: Props) {
const onSubmit = async (formData: GoogleConfigFormValues) => {
const payload: Partial<GoogleConfigFormValues> = { ...formData };
try {
const response = await updateInstanceConfigurations(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your Google authentication is configured. You should test it now.",
});
reset({
GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value,
GOOGLE_CLIENT_SECRET: response.find((item) => item.key === "GOOGLE_CLIENT_SECRET")?.value,
ENABLE_GOOGLE_SYNC: response.find((item) => item.key === "ENABLE_GOOGLE_SYNC")?.value,
});
} catch (err) {
console.error(err);
}
await updateInstanceConfigurations(payload)
.then((response = []) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your Google authentication is configured. You should test it now.",
});
reset({
GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value,
GOOGLE_CLIENT_SECRET: response.find((item) => item.key === "GOOGLE_CLIENT_SECRET")?.value,
});
})
.catch((err) => console.error(err));
};
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
@@ -181,7 +172,7 @@ export function InstanceGoogleConfigForm(props: Props) {
<div className="flex flex-col gap-8">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">Google-provided details for Plane</div>
<div className="pt-2.5 text-xl font-medium">Google-provided details for Plane</div>
{GOOGLE_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
@@ -195,30 +186,27 @@ export function InstanceGoogleConfigForm(props: Props) {
required={field.required}
/>
))}
<ControllerSwitch control={control} field={GOOGLE_FORM_SWITCH_FIELD} />
<div className="flex flex-col gap-1 pt-4">
<div className="flex items-center gap-4">
<Button
variant="primary"
size="lg"
onClick={(e) => void handleSubmit(onSubmit)(e)}
loading={isSubmitting}
disabled={!isDirty}
>
{isSubmitting ? "Saving" : "Save changes"}
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
<Link
href="/authentication"
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back
</Link>
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
<div className="pt-2 text-18 font-medium">Plane-provided details for Google</div>
<div className="pt-2 text-xl font-medium">Plane-provided details for Google</div>
<div className="flex flex-col gap-y-4">
{/* common service details */}
<div className="flex flex-col gap-y-4 px-6 py-4 bg-layer-1 rounded-lg">
<div className="flex flex-col gap-y-4 px-6 py-4 bg-custom-background-80 rounded-lg">
{GOOGLE_COMMON_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -226,11 +214,11 @@ export function InstanceGoogleConfigForm(props: Props) {
{/* web service details */}
<div className="flex flex-col rounded-lg overflow-hidden">
<div className="px-6 py-3 bg-layer-3 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary">
<div className="px-6 py-3 bg-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
<Monitor className="w-3 h-3" />
Web
</div>
<div className="px-6 py-4 flex flex-col gap-y-4 bg-layer-1">
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
{GOOGLE_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -3,16 +3,14 @@ import { observer } from "mobx-react";
import useSWR from "swr";
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// assets
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
// components
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks
import { useInstance } from "@/hooks/store";
// types
// icons
// local components
import type { Route } from "./+types/page";
// local
import { InstanceGoogleConfigForm } from "./form";
const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthenticationPage(
@@ -37,7 +35,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration",
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`,
@@ -58,44 +56,47 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
});
};
return (
<PageWrapper
customHeader={
<AuthenticationMethodCard
name="Google"
description="Allow members to login or sign up to plane with their Google
<>
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
name="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" />}
config={
<ToggleSwitch
value={Boolean(parseInt(enableGoogleConfig))}
onChange={() => {
if (Boolean(parseInt(enableGoogleConfig)) === true) {
updateConfig("IS_GOOGLE_ENABLED", "0");
} else {
updateConfig("IS_GOOGLE_ENABLED", "1");
}
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
}
>
{formattedConfig ? (
<InstanceGoogleConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</PageWrapper>
icon={<img src={GoogleLogo} height={24} width={24} alt="Google Logo" />}
config={
<ToggleSwitch
value={Boolean(parseInt(enableGoogleConfig))}
onChange={() => {
if (Boolean(parseInt(enableGoogleConfig)) === true) {
updateConfig("IS_GOOGLE_ENABLED", "0");
} else {
updateConfig("IS_GOOGLE_ENABLED", "1");
}
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceGoogleConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</div>
</div>
</>
);
});
@@ -1,33 +1,27 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { useTheme } from "next-themes";
import useSWR from "swr";
// plane internal packages
import { setPromiseToast } from "@plane/propel/toast";
import type { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch } from "@plane/ui";
import { cn, resolveGeneralTheme } from "@plane/utils";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
import { cn } from "@plane/utils";
// hooks
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { useAuthenticationModes } from "@/hooks/oauth";
import { useInstance } from "@/hooks/store";
// types
// plane admin components
import { AuthenticationModes } from "@/plane-admin/components/authentication";
import type { Route } from "./+types/page";
const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(_props: Route.ComponentProps) {
// theme
const { resolvedTheme: resolvedThemeAdmin } = useTheme();
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// derived values
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
const resolvedTheme = resolveGeneralTheme(resolvedThemeAdmin);
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
setIsSubmitting(true);
@@ -60,65 +54,59 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
});
};
const authenticationModes = useAuthenticationModes({ disabled: isSubmitting, updateConfig, resolvedTheme });
return (
<PageWrapper
header={{
title: "Manage authentication modes for your instance",
description: "Configure authentication modes for your team and restrict sign-ups to be invite only.",
}}
>
{formattedConfig ? (
<div className="space-y-3">
<div className={cn("w-full flex items-center gap-14 rounded-sm")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="text-16 font-medium pb-1">Allow anyone to sign up even without an invite</div>
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
Toggling this off will only let users sign up when they are invited.
<>
<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">Manage authentication modes for your instance</div>
<div className="text-sm font-normal text-custom-text-300">
Configure authentication modes for your team and restrict sign-ups to be invite only.
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<div className="space-y-3">
<div className={cn("w-full flex items-center gap-14 rounded")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="text-lg font-medium pb-1">Allow anyone to sign up even without an invite</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
Toggling this off will only let users sign up when they are invited.
</div>
</div>
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<div className="flex items-center gap-4">
<ToggleSwitch
value={Boolean(parseInt(enableSignUpConfig))}
onChange={() => {
if (Boolean(parseInt(enableSignUpConfig)) === true) {
updateConfig("ENABLE_SIGNUP", "0");
} else {
updateConfig("ENABLE_SIGNUP", "1");
}
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
</div>
<div className="text-lg font-medium pt-6">Available authentication modes</div>
<AuthenticationModes disabled={isSubmitting} updateConfig={updateConfig} />
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<div className="flex items-center gap-4">
<ToggleSwitch
value={Boolean(parseInt(enableSignUpConfig))}
onChange={() => {
if (Boolean(parseInt(enableSignUpConfig)) === true) {
updateConfig("ENABLE_SIGNUP", "0");
} else {
updateConfig("ENABLE_SIGNUP", "1");
}
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
</div>
<div className="text-lg font-medium pt-6">Available authentication modes</div>
{authenticationModes.map((method) => (
<AuthenticationMethodCard
key={method.key}
name={method.name}
description={method.description}
icon={method.icon}
config={method.config}
disabled={isSubmitting}
unavailable={method.unavailable}
/>
))}
) : (
<Loader className="space-y-10">
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="20%" />
</Loader>
)}
</div>
) : (
<Loader className="space-y-10">
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="20%" />
</Loader>
)}
</PageWrapper>
</div>
</>
);
});
@@ -157,12 +157,13 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
/>
))}
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Email security</h4>
<h4 className="text-sm text-custom-text-300">Email security</h4>
<CustomSelect
value={emailSecurityKey}
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
onChange={handleEmailSecurityChange}
buttonClassName="rounded-md border-subtle"
buttonClassName="rounded-md border-custom-border-200"
optionsClassName="w-full"
input
>
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
@@ -173,12 +174,12 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
</CustomSelect>
</div>
</div>
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-subtle">
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-custom-border-100">
<div className="flex w-full max-w-xl flex-col gap-y-10 px-1">
<div className="mr-8 flex items-center gap-10 pt-4">
<div className="grow">
<div className="text-13 font-medium text-primary">Authentication</div>
<div className="text-11 font-regular text-tertiary">
<div className="text-sm font-medium text-custom-text-100">Authentication</div>
<div className="text-xs font-normal text-custom-text-300">
This is optional, but we recommend setting up a username and a password for your SMTP server.
</div>
</div>
@@ -204,16 +205,14 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
<div className="flex max-w-4xl items-center py-1 gap-4">
<Button
variant="primary"
size="lg"
onClick={handleSubmit(onSubmit)}
loading={isSubmitting}
disabled={!isValid || !isDirty}
>
{isSubmitting ? "Saving" : "Save changes"}
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<Button
variant="secondary"
size="lg"
variant="outline-primary"
onClick={() => setIsSendTestEmailModalOpen(true)}
loading={isSubmitting}
disabled={!isValid}
+36 -38
View File
@@ -3,13 +3,10 @@ import { observer } from "mobx-react";
import useSWR from "swr";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks
import { useInstance } from "@/hooks/store";
// types
// components
import type { Route } from "./+types/page";
// local
import { InstanceEmailForm } from "./email-config-form";
const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.ComponentProps) {
@@ -52,43 +49,44 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
}, [formattedConfig]);
return (
<PageWrapper
header={{
title: "Secure emails from your own instance",
description: (
<>
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
<div className="text-13 font-regular text-tertiary">
Set it up below and please test your settings before you save them.&nbsp;
<span className="text-danger">Misconfigs can lead to email bounces and errors.</span>
<>
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
<div className="text-sm font-normal text-custom-text-300">
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
<div className="text-sm font-normal text-custom-text-300">
Set it up below and please test your settings before you save them.&nbsp;
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
</div>
</div>
</>
),
actions: isLoading ? (
<Loader>
<Loader.Item width="24px" height="16px" className="rounded-full" />
</Loader>
) : (
<ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
),
}}
>
{isSMTPEnabled && !isLoading && (
<>
{formattedConfig ? (
<InstanceEmailForm config={formattedConfig} />
) : (
<Loader className="space-y-10">
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="20%" />
</div>
{isLoading ? (
<Loader>
<Loader.Item width="24px" height="16px" className="rounded-full" />
</Loader>
) : (
<ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
)}
</>
)}
</PageWrapper>
</div>
{isSMTPEnabled && !isLoading && (
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceEmailForm config={formattedConfig} />
) : (
<Loader className="space-y-10">
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="20%" />
</Loader>
)}
</div>
)}
</div>
</>
);
});
@@ -72,7 +72,7 @@ export function SendTestEmailModal(props: Props) {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="my-10 flex justify-center p-4 text-center sm:p-0 md:my-20">
@@ -85,8 +85,8 @@ export function SendTestEmailModal(props: Props) {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg bg-surface-1 p-5 px-4 text-left shadow-raised-200 transition-all w-full sm:max-w-xl">
<h3 className="text-16 font-medium leading-6 text-primary">
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all w-full sm:max-w-xl">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">
{sendEmailStep === ESendEmailSteps.SEND_EMAIL
? "Send test email"
: sendEmailStep === ESendEmailSteps.SUCCESS
@@ -101,12 +101,12 @@ export function SendTestEmailModal(props: Props) {
value={receiverEmail}
onChange={(e) => setReceiverEmail(e.target.value)}
placeholder="Receiver email"
className="w-full resize-none text-16"
className="w-full resize-none text-lg"
tabIndex={1}
/>
)}
{sendEmailStep === ESendEmailSteps.SUCCESS && (
<div className="flex flex-col gap-y-4 text-13">
<div className="flex flex-col gap-y-4 text-sm">
<p>
We have sent the test email to {receiverEmail}. Please check your spam folder if you cannot find
it.
@@ -114,14 +114,14 @@ export function SendTestEmailModal(props: Props) {
<p>If you still cannot find it, recheck your SMTP configuration and trigger a new test email.</p>
</div>
)}
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-13">{error}</div>}
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-sm">{error}</div>}
<div className="flex items-center gap-2 justify-end mt-5">
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={2}>
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={2}>
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Cancel" : "Close"}
</Button>
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
<Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
{isLoading ? "Sending email" : "Send email"}
<Button variant="primary" size="sm" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
{isLoading ? "Sending email..." : "Send email"}
</Button>
)}
</div>
@@ -1,17 +1,17 @@
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { Telescope } from "lucide-react";
// plane imports
// types
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IInstance, IInstanceAdmin } from "@plane/types";
// ui
import { Input, ToggleSwitch } from "@plane/ui";
// components
import { ControllerInput } from "@/components/common/controller-input";
// hooks
import { useInstance } from "@/hooks/store";
// components
import { IntercomConfig } from "./intercom";
// hooks
export interface IGeneralConfigurationForm {
instance: IInstance;
@@ -27,8 +27,8 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
watch,
formState: { errors, isSubmitting },
} = useForm<Partial<IInstance>>({
defaultValues: {
instance_name: instance?.instance_name,
@@ -63,8 +63,8 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
return (
<div className="space-y-8">
<div className="space-y-4">
<div className="text-16 font-medium text-primary">Instance details</div>
<div className="space-y-3">
<div className="text-lg font-medium">Instance details</div>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-8 md:grid-cols-2 lg:grid-cols-3">
<ControllerInput
key="instance_name"
@@ -78,52 +78,54 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
/>
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Email</h4>
<h4 className="text-sm text-custom-text-300">Email</h4>
<Input
id="email"
name="email"
type="email"
value={instanceAdmins[0]?.user_detail?.email ?? ""}
placeholder="Admin email"
className="w-full cursor-not-allowed !text-placeholder"
className="w-full cursor-not-allowed !text-custom-text-400"
autoComplete="on"
disabled
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Instance ID</h4>
<h4 className="text-sm text-custom-text-300">Instance ID</h4>
<Input
id="instance_id"
name="instance_id"
type="text"
value={instance.instance_id}
className="w-full cursor-not-allowed rounded-md font-medium !text-placeholder"
className="w-full cursor-not-allowed rounded-md font-medium !text-custom-text-400"
disabled
/>
</div>
</div>
</div>
<div className="space-y-6">
<div className="text-16 font-medium text-primary pb-1.5 border-b border-subtle">Chat + telemetry</div>
<div className="space-y-3">
<div className="text-lg font-medium">Chat + telemetry</div>
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
<div className="flex items-center gap-14">
<div className="flex items-center gap-14 px-4 py-3 border border-custom-border-200 rounded">
<div className="grow flex items-center gap-4">
<div className="shrink-0">
<div className="flex items-center justify-center size-11 bg-layer-1 rounded-lg">
<Telescope className="size-5 text-tertiary" />
<div className="flex items-center justify-center w-10 h-10 bg-custom-background-80 rounded-full">
<Telescope className="w-6 h-6 text-custom-text-300/80 p-0.5" />
</div>
</div>
<div className="grow">
<div className="text-13 font-medium text-primary leading-5">Let Plane collect anonymous usage data</div>
<div className="text-11 font-regular text-tertiary leading-5">
<div className="text-sm font-medium text-custom-text-100 leading-5">
Let Plane collect anonymous usage data
</div>
<div className="text-xs font-normal text-custom-text-300 leading-5">
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
in line with{" "}
<a
href="https://developers.plane.so/self-hosting/telemetry"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
our Telemetry Policy.
@@ -144,15 +146,8 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
</div>
<div>
<Button
variant="primary"
size="lg"
onClick={() => {
void handleSubmit(onSubmit)();
}}
loading={isSubmitting}
>
{isSubmitting ? "Saving" : "Save changes"}
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
</div>
</div>
@@ -44,22 +44,22 @@ export const IntercomConfig = observer(function IntercomConfig(props: TIntercomC
};
const enableIntercomConfig = () => {
void submitInstanceConfigurations({ IS_INTERCOM_ENABLED: isIntercomEnabled ? "0" : "1" });
submitInstanceConfigurations({ IS_INTERCOM_ENABLED: isIntercomEnabled ? "0" : "1" });
};
return (
<>
<div className="flex items-center gap-14">
<div className="flex items-center gap-14 px-4 py-3 border border-custom-border-200 rounded">
<div className="grow flex items-center gap-4">
<div className="shrink-0">
<div className="flex items-center justify-center size-11 bg-layer-1 rounded-lg">
<MessageSquare className="size-5 text-tertiary p-0.5" />
<div className="flex items-center justify-center w-10 h-10 bg-custom-background-80 rounded-full">
<MessageSquare className="w-6 h-6 text-custom-text-300/80 p-0.5" />
</div>
</div>
<div className="grow">
<div className="text-13 font-medium text-primary leading-5">Chat with us</div>
<div className="text-11 font-regular text-tertiary leading-5">
<div className="text-sm font-medium text-custom-text-100 leading-5">Chat with us</div>
<div className="text-xs font-normal text-custom-text-300 leading-5">
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
automatically.
</div>
@@ -1,26 +1,30 @@
import { observer } from "mobx-react";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks
import { useInstance } from "@/hooks/store";
// local imports
import { GeneralConfigurationForm } from "./form";
// types
// components
import type { Route } from "./+types/page";
import { GeneralConfigurationForm } from "./form";
function GeneralPage() {
const { instance, instanceAdmins } = useInstance();
return (
<PageWrapper
header={{
title: "General settings",
description:
"Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your instance.",
}}
>
{instance && instanceAdmins && <GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />}
</PageWrapper>
<>
<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">General settings</div>
<div className="text-sm font-normal text-custom-text-300">
Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your
instance.
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{instance && instanceAdmins && (
<GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />
)}
</div>
</div>
</>
);
}
@@ -7,30 +7,51 @@ import { Breadcrumbs } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
// hooks
import { useTheme } from "@/hooks/store";
// local imports
import { CORE_HEADER_SEGMENT_LABELS } from "./core";
import { EXTENDED_HEADER_SEGMENT_LABELS } from "./extended";
export const HamburgerToggle = observer(function HamburgerToggle() {
const { isSidebarCollapsed, toggleSidebar } = useTheme();
return (
<button
className="size-7 rounded-sm flex justify-center items-center bg-layer-1 transition-all hover:bg-layer-1-hover cursor-pointer group md:hidden"
<div
className="w-7 h-7 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<Menu size={14} className="text-secondary group-hover:text-primary transition-all" />
</button>
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />
</div>
);
});
const HEADER_SEGMENT_LABELS = {
...CORE_HEADER_SEGMENT_LABELS,
...EXTENDED_HEADER_SEGMENT_LABELS,
};
export const AdminHeader = observer(function AdminHeader() {
const pathName = usePathname();
const getHeaderTitle = (pathName: string) => {
switch (pathName) {
case "general":
return "General";
case "ai":
return "Artificial Intelligence";
case "email":
return "Email";
case "authentication":
return "Authentication";
case "image":
return "Image";
case "google":
return "Google";
case "github":
return "GitHub";
case "gitlab":
return "GitLab";
case "gitea":
return "Gitea";
case "workspace":
return "Workspace";
case "create":
return "Create";
default:
return pathName.toUpperCase();
}
};
// Function to dynamically generate breadcrumb items based on pathname
const generateBreadcrumbItems = (pathname: string) => {
const pathSegments = pathname.split("/").slice(1); // removing the first empty string.
@@ -40,17 +61,17 @@ export const AdminHeader = observer(function AdminHeader() {
const breadcrumbItems = pathSegments.map((segment) => {
currentUrl += "/" + segment;
return {
title: HEADER_SEGMENT_LABELS[segment] ?? segment.toUpperCase(),
title: getHeaderTitle(segment),
href: currentUrl,
};
});
return breadcrumbItems;
};
const breadcrumbItems = generateBreadcrumbItems(pathName || "");
const breadcrumbItems = generateBreadcrumbItems(pathName);
return (
<div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4">
<div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-sidebar-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HamburgerToggle />
{breadcrumbItems.length >= 0 && (
@@ -61,7 +82,7 @@ export const AdminHeader = observer(function AdminHeader() {
<BreadcrumbLink
href="/general/"
label="Settings"
icon={<Settings className="h-4 w-4 text-tertiary" />}
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
@@ -56,7 +56,7 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
<a
href="https://unsplash.com/documentation#creating-a-developer-account"
target="_blank"
className="text-accent-primary hover:underline"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Learn more.
@@ -70,8 +70,8 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
</div>
<div>
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving" : "Save changes"}
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
</div>
</div>
+21 -19
View File
@@ -1,13 +1,10 @@
import { observer } from "mobx-react";
import useSWR from "swr";
import { Loader } from "@plane/ui";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks
import { useInstance } from "@/hooks/store";
// types
import type { Route } from "./+types/page";
// local
import type { Route } from "./+types/page";
import { InstanceImageConfigForm } from "./form";
const InstanceImagePage = observer(function InstanceImagePage(_props: Route.ComponentProps) {
@@ -17,21 +14,26 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return (
<PageWrapper
header={{
title: "Third-party image libraries",
description: "Let your users search and choose images from third-party libraries",
}}
>
{formattedConfig ? (
<InstanceImageConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="50%" />
<Loader.Item height="50px" width="20%" />
</Loader>
)}
</PageWrapper>
<>
<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">Third-party image libraries</div>
<div className="text-sm font-normal text-custom-text-300">
Let your users search and choose images from third-party libraries
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceImageConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="50%" />
<Loader.Item height="50px" width="20%" />
</Loader>
)}
</div>
</div>
</>
);
});
+3 -3
View File
@@ -3,13 +3,13 @@ import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
import { Outlet } from "react-router";
// components
import { AdminHeader } from "@/components/common/header";
import { LogoSpinner } from "@/components/common/logo-spinner";
import { NewUserPopup } from "@/components/new-user-popup";
// hooks
import { useUser } from "@/hooks/store";
// local components
import type { Route } from "./+types/layout";
import { AdminHeader } from "./header";
import { AdminSidebar } from "./sidebar";
function AdminLayout(_props: Route.ComponentProps) {
@@ -34,9 +34,9 @@ function AdminLayout(_props: Route.ComponentProps) {
return (
<div className="relative flex h-screen w-screen overflow-hidden">
<AdminSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
<AdminHeader />
<div className="h-full w-full overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md">
<div className="h-full w-full overflow-hidden">
<Outlet />
</div>
</main>
@@ -33,20 +33,20 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
const getSidebarMenuItems = () => (
<Menu.Items
className={cn(
"absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-subtle rounded-md border border-subtle bg-surface-1 px-1 py-2 text-11 shadow-lg outline-none",
"absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-custom-sidebar-border-100 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none",
{
"left-4": isSidebarCollapsed,
}
)}
>
<div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-secondary truncate">{currentUser?.email}</span>
<span className="px-2 text-custom-sidebar-text-200 truncate">{currentUser?.email}</span>
</div>
<div className="py-2">
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleThemeSwitch}
>
<Palette className="h-4 w-4 stroke-[1.5]" />
@@ -59,7 +59,7 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
<Menu.Item
as="button"
type="submit"
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
>
<LogOut className="h-4 w-4 stroke-[1.5]" />
Sign out
@@ -71,14 +71,14 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
useEffect(() => {
if (csrfToken === undefined)
void authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
return (
<div className="flex max-h-header items-center gap-x-5 gap-y-2 border-b border-subtle px-4 py-2.5">
<div className="flex max-h-header items-center gap-x-5 gap-y-2 border-b border-custom-sidebar-border-200 px-4 py-3.5">
<div className="h-full w-full truncate">
<div
className={`flex flex-grow items-center gap-x-2 truncate rounded-sm ${
className={`flex flex-grow items-center gap-x-2 truncate rounded py-1 ${
isSidebarCollapsed ? "justify-center" : ""
}`}
>
@@ -88,8 +88,8 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
"cursor-default": !isSidebarCollapsed,
})}
>
<div className="flex size-8 flex-shrink-0 items-center justify-center rounded-sm bg-layer-1">
<UserCog2 className="size-5 text-primary" />
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded bg-custom-sidebar-background-80">
<UserCog2 className="h-5 w-5 text-custom-text-200" />
</div>
</Menu.Button>
{isSidebarCollapsed && (
@@ -109,7 +109,7 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
{!isSidebarCollapsed && (
<div className="flex w-full gap-2">
<h4 className="grow truncate text-body-md-medium text-primary">Instance admin</h4>
<h4 className="grow truncate text-base font-medium text-custom-text-200">Instance admin</h4>
</div>
)}
</div>
@@ -123,7 +123,7 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
src={getFileURL(currentUser.avatar_url)}
size={24}
shape="square"
className="!text-body-sm-medium"
className="!text-base"
/>
</Menu.Button>
@@ -9,8 +9,10 @@ import { DiscordIcon, GithubIcon, PageIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// hooks
import { useInstance, useTheme } from "@/hooks/store";
import { useTheme } from "@/hooks/store";
// assets
// eslint-disable-next-line import/order
import packageJson from "package.json";
const helpOptions = [
{
@@ -34,7 +36,6 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// store
const { instance } = useInstance();
const { isSidebarCollapsed, toggleSidebar } = useTheme();
// refs
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
@@ -44,7 +45,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
return (
<div
className={cn(
"flex w-full items-center justify-between gap-1 self-baseline border-t border-subtle bg-surface-1 px-4 h-14 flex-shrink-0",
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
{
"flex-col h-auto py-1.5": isSidebarCollapsed,
}
@@ -54,32 +55,32 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
<Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<a
href={redirectionLink}
className={`relative px-2 py-1 flex items-center gap-1 rounded-sm bg-layer-1 text-body-xs-medium text-secondary whitespace-nowrap`}
className={`relative px-2 py-1.5 flex items-center gap-2 font-medium rounded border border-custom-primary-100/20 bg-custom-primary-100/10 text-xs text-custom-primary-200 whitespace-nowrap`}
>
<ExternalLink size={16} />
<ExternalLink size={14} />
{!isSidebarCollapsed && "Redirect to Plane"}
</a>
</Tooltip>
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<button
type="button"
className={`ml-auto grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
className={`ml-auto grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
>
<HelpCircle className="size-4" />
<HelpCircle className="h-3.5 w-3.5" />
</button>
</Tooltip>
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<button
type="button"
className={`grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<MoveLeft className={`size-4 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} />
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} />
</button>
</Tooltip>
</div>
@@ -97,7 +98,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
<div
className={`absolute bottom-2 min-w-[10rem] z-[15] ${
isSidebarCollapsed ? "left-full" : "-left-[75px]"
} divide-y divide-subtle-1 whitespace-nowrap rounded-sm bg-surface-1 p-1 shadow-raised-100`}
} divide-y divide-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
ref={helpOptionsRef}
>
<div className="space-y-1 pb-2">
@@ -105,11 +106,11 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
if (href)
return (
<Link href={href} key={name} target="_blank">
<div className="flex items-center gap-x-2 rounded-sm px-2 py-1 text-11 hover:bg-layer-1-hover">
<div className="flex items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80">
<div className="grid flex-shrink-0 place-items-center">
<Icon className="h-3.5 w-3.5 text-secondary" />
<Icon className="h-3.5 w-3.5 text-custom-text-200" width={14} height={14} />
</div>
<span className="text-11">{name}</span>
<span className="text-xs">{name}</span>
</div>
</Link>
);
@@ -118,17 +119,17 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
<button
key={name}
type="button"
className="flex w-full items-center gap-x-2 rounded-sm px-2 py-1 text-11 hover:bg-layer-1"
className="flex w-full items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80"
>
<div className="grid flex-shrink-0 place-items-center">
<Icon className="h-3.5 w-3.5 text-secondary" />
<Icon className="h-3.5 w-3.5 text-custom-text-200" />
</div>
<span className="text-11">{name}</span>
<span className="text-xs">{name}</span>
</button>
);
})}
</div>
<div className="px-2 pb-1 pt-2 text-10">Version: v{instance?.current_version}</div>
<div className="px-2 pb-1 pt-2 text-[10px]">Version: v{packageJson.version}</div>
</div>
</Transition>
</div>
@@ -1,20 +1,58 @@
import { observer } from "mobx-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// plane internal packages
import { WorkspaceIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// hooks
import { useTheme } from "@/hooks/store";
import { useSidebarMenu } from "@/hooks/use-sidebar-menu";
const INSTANCE_ADMIN_LINKS = [
{
Icon: Cog,
name: "General",
description: "Identify your instances and get key details.",
href: `/general/`,
},
{
Icon: WorkspaceIcon,
name: "Workspaces",
description: "Manage all workspaces on this instance.",
href: `/workspace/`,
},
{
Icon: Mail,
name: "Email",
description: "Configure your SMTP controls.",
href: `/email/`,
},
{
Icon: Lock,
name: "Authentication",
description: "Configure authentication modes.",
href: `/authentication/`,
},
{
Icon: BrainCog,
name: "Artificial intelligence",
description: "Configure your OpenAI creds.",
href: `/ai/`,
},
{
Icon: Image,
name: "Images in Plane",
description: "Allow third-party image libraries.",
href: `/image/`,
},
];
export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
// router
const pathName = usePathname();
// store hooks
const { isSidebarCollapsed, toggleSidebar } = useTheme();
// derived values
const sidebarMenu = useSidebarMenu();
// router
const pathName = usePathname();
const handleItemClick = () => {
if (window.innerWidth < 768) {
@@ -24,27 +62,40 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
return (
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 py-4">
{sidebarMenu.map((item, index) => {
const isActive = item.href === pathName || pathName?.includes(item.href);
{INSTANCE_ADMIN_LINKS.map((item, index) => {
const isActive = item.href === pathName || pathName.includes(item.href);
return (
<Link key={index} href={item.href} onClick={handleItemClick}>
<div>
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
<div
className={cn(
"group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none transition-colors",
{
"text-primary !bg-layer-transparent-active": isActive,
"text-secondary hover:bg-layer-transparent-hover active:bg-layer-transparent-active": !isActive,
},
`group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none transition-colors`,
isActive
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80",
isSidebarCollapsed ? "justify-center" : "w-[260px]"
)}
>
{<item.Icon className="h-4 w-4 flex-shrink-0" />}
{!isSidebarCollapsed && (
<div className="w-full ">
<div className={cn(`text-body-xs-medium transition-colors`)}>{item.name}</div>
<div className={cn(`text-caption-sm-regular transition-colors`)}>{item.description}</div>
<div
className={cn(
`text-sm font-medium transition-colors`,
isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-200"
)}
>
{item.name}
</div>
<div
className={cn(
`text-[10px] transition-colors`,
isActive ? "text-custom-primary-90" : "text-custom-sidebar-text-400"
)}
>
{item.description}
</div>
</div>
)}
</div>
+1 -1
View File
@@ -38,7 +38,7 @@ export const AdminSidebar = observer(function AdminSidebar() {
return (
<div
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-subtle bg-surface-1 duration-300
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300
fixed md:relative
${isSidebarCollapsed ? "-ml-[290px]" : ""}
sm:${isSidebarCollapsed ? "-ml-[290px]" : ""}
@@ -84,7 +84,7 @@ export function WorkspaceCreateForm() {
<div className="space-y-8">
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-x-10 gap-y-6 lg:grid-cols-2">
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Name your workspace</h4>
<h4 className="text-sm text-custom-text-300">Name your workspace</h4>
<div className="flex flex-col gap-1">
<Controller
control={control}
@@ -118,13 +118,13 @@ export function WorkspaceCreateForm() {
/>
)}
/>
<span className="text-11 text-red-500">{errors?.name?.message}</span>
<span className="text-xs text-red-500">{errors?.name?.message}</span>
</div>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Set your workspace&apos;s URL</h4>
<div className="flex gap-0.5 w-full items-center rounded-md border-[0.5px] border-subtle px-3">
<span className="whitespace-nowrap text-13 text-secondary">{workspaceBaseURL}</span>
<h4 className="text-sm text-custom-text-300">Set your workspace&apos;s URL</h4>
<div className="flex gap-0.5 w-full items-center rounded-md border-[0.5px] border-custom-border-200 px-3">
<span className="whitespace-nowrap text-sm text-custom-text-200">{workspaceBaseURL}</span>
<Controller
control={control}
name="slug"
@@ -148,19 +148,19 @@ export function WorkspaceCreateForm() {
ref={ref}
hasError={Boolean(errors.slug)}
placeholder="workspace-name"
className="block w-full rounded-md border-none bg-transparent !px-0 py-2 text-13"
className="block w-full rounded-md border-none bg-transparent !px-0 py-2 text-sm"
/>
)}
/>
</div>
{slugError && <p className="text-13 text-red-500">This URL is taken. Try something else.</p>}
{slugError && <p className="text-sm text-red-500">This URL is taken. Try something else.</p>}
{invalidSlug && (
<p className="text-13 text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
<p className="text-sm text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
)}
{errors.slug && <span className="text-11 text-red-500">{errors.slug.message}</span>}
{errors.slug && <span className="text-xs text-red-500">{errors.slug.message}</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">How many people will use this workspace?</h4>
<h4 className="text-sm text-custom-text-300">How many people will use this workspace?</h4>
<div className="w-full">
<Controller
name="organization_size"
@@ -172,11 +172,12 @@ export function WorkspaceCreateForm() {
onChange={onChange}
label={
ORGANIZATION_SIZE.find((c) => c === value) ?? (
<span className="text-placeholder">Select a range</span>
<span className="text-custom-text-400">Select a range</span>
)
}
buttonClassName="!border-[0.5px] !border-subtle !shadow-none"
buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none"
input
optionsClassName="w-full"
>
{ORGANIZATION_SIZE.map((item) => (
<CustomSelect.Option key={item} value={item}>
@@ -187,7 +188,7 @@ export function WorkspaceCreateForm() {
)}
/>
{errors.organization_size && (
<span className="text-13 text-red-500">{errors.organization_size.message}</span>
<span className="text-sm text-red-500">{errors.organization_size.message}</span>
)}
</div>
</div>
@@ -195,14 +196,14 @@ export function WorkspaceCreateForm() {
<div className="flex max-w-4xl items-center py-1 gap-4">
<Button
variant="primary"
size="lg"
size="sm"
onClick={handleSubmit(handleCreateWorkspace)}
disabled={!isValid}
loading={isSubmitting}
>
{isSubmitting ? "Creating workspace" : "Create workspace"}
</Button>
<Link className={getButtonStyling("secondary", "lg")} href="/workspace">
<Link className={getButtonStyling("neutral-primary", "sm")} href="/workspace">
Go back
</Link>
</div>
@@ -1,21 +1,21 @@
import { observer } from "mobx-react";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
// types
import type { Route } from "./+types/page";
// local
import { WorkspaceCreateForm } from "./form";
const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.ComponentProps) {
return (
<PageWrapper
header={{
title: "Create a new workspace on this instance.",
description: "You will need to invite users from Workspace Settings after you create this workspace.",
}}
>
<WorkspaceCreateForm />
</PageWrapper>
<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>
);
});
@@ -8,13 +8,12 @@ import { Button, getButtonStyling } from "@plane/propel/button";
import { setPromiseToast } from "@plane/propel/toast";
import type { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
import { WorkspaceListItem } from "@/components/workspace/list-item";
// hooks
import { useInstance, useWorkspace } from "@/hooks/store";
// types
import type { Route } from "./+types/page";
const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props: Route.ComponentProps) {
@@ -69,95 +68,101 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
};
return (
<PageWrapper
header={{
title: "Workspaces on this instance",
description: "See all workspaces and control who can create them.",
}}
>
<div className="space-y-3">
{formattedConfig ? (
<div className={cn("w-full flex items-center gap-14 rounded-sm")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="text-16 font-medium pb-1">Prevent anyone else from creating a workspace.</div>
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
Toggling this on will let only you create workspaces. You will have to invite users to new workspaces.
</div>
</div>
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<div className="flex items-center gap-4">
<ToggleSwitch
value={Boolean(parseInt(disableWorkspaceCreation))}
onChange={() => {
if (Boolean(parseInt(disableWorkspaceCreation)) === true) {
updateConfig("DISABLE_WORKSPACE_CREATION", "0");
} else {
updateConfig("DISABLE_WORKSPACE_CREATION", "1");
}
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="flex flex-col gap-1">
<div className="text-xl font-medium text-custom-text-100">Workspaces on this instance</div>
<div className="text-sm font-normal text-custom-text-300">
See all workspaces and control who can create them.
</div>
) : (
<Loader>
<Loader.Item height="50px" width="100%" />
</Loader>
)}
{workspaceLoader !== "init-loader" ? (
<>
<div className="pt-6 flex items-center justify-between gap-2">
<div className="flex flex-col items-start gap-x-2">
<div className="flex items-center gap-2 text-16 font-medium">
All workspaces on this instance <span className="text-tertiary"> {workspaceIds.length}</span>
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
<LoaderIcon className="w-4 h-4 animate-spin" />
)}
</div>
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
You can&apos;t yet delete workspaces and you can only go to the workspace if you are an Admin or a
Member.
</div>
</div>
<div className="flex items-center gap-2">
<Link href="/workspace/create" className={getButtonStyling("primary", "base")}>
Create workspace
</Link>
</div>
</div>
<div className="flex flex-col gap-4 py-2">
{workspaceIds.map((workspaceId) => (
<WorkspaceListItem key={workspaceId} workspaceId={workspaceId} />
))}
</div>
{hasNextPage && (
<div className="flex justify-center">
<Button
variant="link"
size="lg"
onClick={() => fetchNextWorkspaces()}
disabled={workspaceLoader === "pagination"}
>
Load more
{workspaceLoader === "pagination" && <LoaderIcon className="w-3 h-3 animate-spin" />}
</Button>
</div>
)}
</>
) : (
<Loader className="space-y-10 py-8">
<Loader.Item height="24px" width="20%" />
<Loader.Item height="92px" width="100%" />
<Loader.Item height="92px" width="100%" />
<Loader.Item height="92px" width="100%" />
</Loader>
)}
</div>
</div>
</PageWrapper>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<div className="space-y-3">
{formattedConfig ? (
<div className={cn("w-full flex items-center gap-14 rounded")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="text-lg font-medium pb-1">Prevent anyone else from creating a workspace.</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
Toggling this on will let only you create workspaces. You will have to invite users to new
workspaces.
</div>
</div>
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<div className="flex items-center gap-4">
<ToggleSwitch
value={Boolean(parseInt(disableWorkspaceCreation))}
onChange={() => {
if (Boolean(parseInt(disableWorkspaceCreation)) === true) {
updateConfig("DISABLE_WORKSPACE_CREATION", "0");
} else {
updateConfig("DISABLE_WORKSPACE_CREATION", "1");
}
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
</div>
) : (
<Loader>
<Loader.Item height="50px" width="100%" />
</Loader>
)}
{workspaceLoader !== "init-loader" ? (
<>
<div className="pt-6 flex items-center justify-between gap-2">
<div className="flex flex-col items-start gap-x-2">
<div className="flex items-center gap-2 text-lg font-medium">
All workspaces on this instance{" "}
<span className="text-custom-text-300"> {workspaceIds.length}</span>
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
<LoaderIcon className="w-4 h-4 animate-spin" />
)}
</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
You can&apos;t yet delete workspaces and you can only go to the workspace if you are an Admin or a
Member.
</div>
</div>
<div className="flex items-center gap-2">
<Link href="/workspace/create" className={getButtonStyling("primary", "sm")}>
Create workspace
</Link>
</div>
</div>
<div className="flex flex-col gap-4 py-2">
{workspaceIds.map((workspaceId) => (
<WorkspaceListItem key={workspaceId} workspaceId={workspaceId} />
))}
</div>
{hasNextPage && (
<div className="flex justify-center">
<Button
variant="link-primary"
onClick={() => fetchNextWorkspaces()}
disabled={workspaceLoader === "pagination"}
>
Load more
{workspaceLoader === "pagination" && <LoaderIcon className="w-3 h-3 animate-spin" />}
</Button>
</div>
)}
</>
) : (
<Loader className="space-y-10 py-8">
<Loader.Item height="24px" width="20%" />
<Loader.Item height="92px" width="100%" />
<Loader.Item height="92px" width="100%" />
<Loader.Item height="92px" width="100%" />
</Loader>
)}
</div>
</div>
</div>
);
});
+4 -4
View File
@@ -14,13 +14,13 @@ export function AuthBanner(props: TAuthBanner) {
if (!bannerData) return <></>;
return (
<div className="relative flex items-center p-2 rounded-md gap-2 border border-accent-strong/50 bg-accent-primary/10">
<div className="relative flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10">
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
<Info size={16} className="text-accent-primary" />
<Info size={16} className="text-custom-primary-100" />
</div>
<div className="w-full text-13 font-medium text-accent-primary">{bannerData?.message}</div>
<div className="w-full text-sm font-medium text-custom-primary-100">{bannerData?.message}</div>
<div
className="relative ml-auto w-6 h-6 rounded-xs flex justify-center items-center transition-all cursor-pointer hover:bg-accent-primary/20 text-accent-primary"
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80"
onClick={() => handleBannerData && handleBannerData(undefined)}
>
<CloseIcon className="w-4 h-4 flex-shrink-0" />
+1 -1
View File
@@ -5,7 +5,7 @@ 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-primary" />
<PlaneLockup height={20} width={95} className="text-custom-text-100" />
</Link>
</div>
);
+8 -5
View File
@@ -26,7 +26,7 @@ export enum EErrorAlertType {
}
const errorCodeMessages: {
[key in EAdminAuthErrorCodes]: { title: string; message: (email?: string) => React.ReactNode };
[key in EAdminAuthErrorCodes]: { title: string; message: (email?: string | undefined) => React.ReactNode };
} = {
// admin
[EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST]: {
@@ -79,11 +79,14 @@ const errorCodeMessages: {
},
[EAdminAuthErrorCodes.ADMIN_USER_DEACTIVATED]: {
title: `User account deactivated`,
message: () => `User account deactivated. Please contact ${SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
message: () => `User account deactivated. Please contact ${!!SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
},
};
export const authErrorHandler = (errorCode: EAdminAuthErrorCodes, email?: string): TAdminAuthErrorInfo | undefined => {
export const authErrorHandler = (
errorCode: EAdminAuthErrorCodes,
email?: string | undefined
): TAdminAuthErrorInfo | undefined => {
const bannerAlertErrorCodes = [
EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST,
EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
@@ -117,14 +120,14 @@ export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps
name: "Unique codes",
description:
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
icon: <Mails className="h-6 w-6 p-0.5 text-tertiary" />,
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "passwords-login",
name: "Passwords",
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary" />,
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
+1 -1
View File
@@ -16,7 +16,7 @@ function RootLayout() {
}, [replace, isUserLoggedIn]);
return (
<div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8 bg-surface-1">
<div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
<Outlet />
</div>
);
+7 -7
View File
@@ -128,11 +128,11 @@ export function InstanceSignInForm() {
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="w-full space-y-1">
<label className="text-13 text-tertiary font-medium" htmlFor="email">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
Email <span className="text-red-500">*</span>
</label>
<Input
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
id="email"
name="email"
type="email"
@@ -146,12 +146,12 @@ export function InstanceSignInForm() {
</div>
<div className="w-full space-y-1">
<label className="text-13 text-tertiary font-medium" htmlFor="password">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
Password <span className="text-red-500">*</span>
</label>
<div className="relative">
<Input
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
id="password"
name="password"
type={showPassword ? "text" : "password"}
@@ -164,7 +164,7 @@ export function InstanceSignInForm() {
{showPassword ? (
<button
type="button"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
@@ -172,7 +172,7 @@ export function InstanceSignInForm() {
) : (
<button
type="button"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
@@ -181,7 +181,7 @@ export function InstanceSignInForm() {
</div>
</div>
<div className="py-2">
<Button type="submit" size="xl" className="w-full" disabled={isButtonDisabled}>
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Sign in"}
</Button>
</div>
+4 -4
View File
@@ -7,22 +7,22 @@ import Image404 from "@/app/assets/images/404.svg?url";
function PageNotFound() {
return (
<div className={`h-screen w-full overflow-hidden bg-surface-1`}>
<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-16 font-semibold">Oops! Something went wrong.</h3>
<p className="text-13 text-secondary">
<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="secondary" size="lg">
<Button variant="neutral-primary" size="md">
Go to general settings
</Button>
</span>
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable import/order */
import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
@@ -1,11 +1,10 @@
import { ThemeProvider } from "next-themes";
import { SWRConfig } from "swr";
import { AppProgressBar } from "@/lib/b-progress";
// local imports
import { ToastWithTheme } from "./toast";
import { StoreProvider } from "./store.provider";
import { InstanceProvider } from "./instance.provider";
import { UserProvider } from "./user.provider";
import { InstanceProvider } from "./(all)/instance.provider";
import { StoreProvider } from "./(all)/store.provider";
import { ToastWithTheme } from "./(all)/toast";
import { UserProvider } from "./(all)/user.provider";
const DEFAULT_SWR_CONFIG = {
refreshWhenHidden: false,
@@ -16,7 +15,7 @@ const DEFAULT_SWR_CONFIG = {
errorRetryCount: 3,
};
export function CoreProviders({ children }: { children: React.ReactNode }) {
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<AppProgressBar />
+3 -19
View File
@@ -1,20 +1,15 @@
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 * as Sentry from "@sentry/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 { AppProviders } from "@/providers";
import type { Route } from "./+types/root";
// fonts
import "@fontsource-variable/inter";
import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url";
import "@fontsource/material-symbols-rounded";
import "@fontsource/ibm-plex-mono";
import { AppProviders } from "./providers";
const APP_TITLE = "Plane | Simple, extensible, open-source project management tool.";
const APP_DESCRIPTION =
@@ -27,13 +22,6 @@ export const links: LinksFunction = () => [
{ rel: "shortcut icon", href: faviconIco },
{ rel: "manifest", href: `/site.webmanifest.json` },
{ rel: "stylesheet", href: globalStyles },
{
rel: "preload",
href: interVariableWoff2,
as: "font",
type: "font/woff2",
crossOrigin: "anonymous",
},
];
export function Layout({ children }: { children: ReactNode }) {
@@ -68,11 +56,7 @@ export const meta: Route.MetaFunction = () => [
];
export default function Root() {
return (
<div className="bg-canvas min-h-screen">
<Outlet />
</div>
);
return <Outlet />;
}
export function HydrateFallback() {
@@ -1,61 +1,72 @@
import { observer } from "mobx-react";
import { useTheme } from "next-themes";
import { KeyRound, Mails } from "lucide-react";
// types
import type {
TCoreInstanceAuthenticationModeKeys,
TGetBaseAuthenticationModeProps,
TInstanceAuthenticationMethodKeys,
TInstanceAuthenticationModes,
} from "@plane/types";
// assets
import { resolveGeneralTheme } from "@plane/utils";
// components
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
import gitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
import googleLogo from "@/app/assets/logos/google-logo.svg?url";
// components
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
import OIDCLogo from "@/app/assets/logos/oidc-logo.svg?url";
import SAMLLogo from "@/app/assets/logos/saml-logo.svg?url";
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
import { GiteaConfiguration } from "@/components/authentication/gitea-config";
import { GithubConfiguration } from "@/components/authentication/github-config";
import { GitlabConfiguration } from "@/components/authentication/gitlab-config";
import { GoogleConfiguration } from "@/components/authentication/google-config";
import { PasswordLoginConfiguration } from "@/components/authentication/password-config-switch";
// plane admin components
import { UpgradeButton } from "@/plane-admin/components/common";
// assets
export type TAuthenticationModeProps = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
// Authentication methods
export const getCoreAuthenticationModesMap: (
props: TGetBaseAuthenticationModeProps
) => Record<TCoreInstanceAuthenticationModeKeys, TInstanceAuthenticationModes> = ({
export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
disabled,
updateConfig,
resolvedTheme,
}) => ({
"unique-codes": {
}) => [
{
key: "unique-codes",
name: "Unique codes",
description:
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
icon: <Mails className="h-6 w-6 p-0.5 text-tertiary" />,
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
"passwords-login": {
{
key: "passwords-login",
name: "Passwords",
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary" />,
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
google: {
{
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: <img src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
github: {
{
key: "github",
name: "GitHub",
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
icon: (
<img
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
@@ -63,18 +74,56 @@ export const getCoreAuthenticationModesMap: (
),
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
gitlab: {
{
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: <img src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
gitea: {
{
key: "gitea",
name: "Gitea",
description: "Allow members to log in or sign up to plane with their Gitea accounts.",
icon: <img src={giteaLogo} height={20} width={20} alt="Gitea Logo" />,
config: <GiteaConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "oidc",
name: "OIDC",
description: "Authenticate your users via the OpenID Connect protocol.",
icon: <img src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
config: <UpgradeButton />,
unavailable: true,
},
{
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" />,
config: <UpgradeButton />,
unavailable: true,
},
];
export const AuthenticationModes = observer(function AuthenticationModes(props: TAuthenticationModeProps) {
const { disabled, updateConfig } = props;
// next-themes
const { resolvedTheme } = useTheme();
return (
<>
{getAuthenticationModes({ disabled, updateConfig, resolvedTheme }).map((method) => (
<AuthenticationMethodCard
key={method.key}
name={method.name}
description={method.description}
icon={method.icon}
config={method.config}
disabled={disabled}
unavailable={method.unavailable}
/>
))}
</>
);
});
@@ -0,0 +1 @@
export * from "./authentication-modes";
+1
View File
@@ -0,0 +1 @@
export * from "./upgrade-button";
@@ -0,0 +1,19 @@
import React from "react";
// icons
import { SquareArrowOutUpRight } from "lucide-react";
// plane internal packages
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>
);
}
@@ -16,8 +16,8 @@ export function AuthenticationMethodCard(props: Props) {
return (
<div
className={cn("w-full flex items-center gap-14 rounded-lg bg-layer-2", {
"px-4 py-3 border border-subtle": withBorder,
className={cn("w-full flex items-center gap-14 rounded", {
"px-4 py-3 border border-custom-border-200": withBorder,
})}
>
<div
@@ -26,21 +26,21 @@ export function AuthenticationMethodCard(props: Props) {
})}
>
<div className="shrink-0">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-layer-1">{icon}</div>
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-custom-background-80">{icon}</div>
</div>
<div className="grow">
<div
className={cn("font-medium leading-5 text-primary", {
"text-13": withBorder,
"text-18": !withBorder,
className={cn("font-medium leading-5 text-custom-text-100", {
"text-sm": withBorder,
"text-xl": !withBorder,
})}
>
{name}
</div>
<div
className={cn("font-regular leading-5 text-tertiary", {
"text-11": withBorder,
"text-13": !withBorder,
className={cn("font-normal leading-5 text-custom-text-300", {
"text-xs": withBorder,
"text-sm": !withBorder,
})}
>
{description}
@@ -1,11 +1,11 @@
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
// plane internal packages
import { getButtonStyling } from "@plane/propel/button";
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store";
@@ -28,7 +28,7 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
<>
{GiteaConfigured ? (
<div className="flex items-center gap-4">
<Link href="/authentication/gitea" className={cn(getButtonStyling("link", "base"), "font-medium")}>
<Link href="/authentication/gitea" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
Edit
</Link>
<ToggleSwitch
@@ -43,8 +43,11 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
/>
</div>
) : (
<Link href="/authentication/gitea" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
<Link
href="/authentication/gitea"
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
>
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
Configure
</Link>
)}
@@ -1,3 +1,4 @@
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
@@ -27,7 +28,7 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
<>
{isGithubConfigured ? (
<div className="flex items-center gap-4">
<Link href="/authentication/github" className={cn(getButtonStyling("link", "base"), "font-medium")}>
<Link href="/authentication/github" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
Edit
</Link>
<ToggleSwitch
@@ -41,8 +42,11 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
/>
</div>
) : (
<Link href="/authentication/github" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
<Link
href="/authentication/github"
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
>
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
Configure
</Link>
)}
@@ -27,7 +27,7 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
<>
{isGitlabConfigured ? (
<div className="flex items-center gap-4">
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link", "base"), "font-medium")}>
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
Edit
</Link>
<ToggleSwitch
@@ -41,8 +41,11 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
/>
</div>
) : (
<Link href="/authentication/gitlab" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
<Link
href="/authentication/gitlab"
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
>
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
Configure
</Link>
)}
@@ -27,7 +27,7 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
<>
{isGoogleConfigured ? (
<div className="flex items-center gap-4">
<Link href="/authentication/google" className={cn(getButtonStyling("link", "base"), "font-medium")}>
<Link href="/authentication/google" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
Edit
</Link>
<ToggleSwitch
@@ -41,8 +41,11 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
/>
</div>
) : (
<Link href="/authentication/google" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
<Link
href="/authentication/google"
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
>
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
Configure
</Link>
)}
+1 -1
View File
@@ -23,7 +23,7 @@ export function Banner(props: TBanner) {
)}
</div>
<div className="ml-1">
<p className={`text-13 font-medium ${type === "error" ? "text-red-600" : "text-green-600"}`}>{message}</p>
<p className={`text-sm font-medium ${type === "error" ? "text-red-600" : "text-green-600"}`}>{message}</p>
</div>
</div>
</div>
@@ -14,12 +14,17 @@ export function BreadcrumbLink(props: Props) {
<li className="flex items-center space-x-2" tabIndex={-1}>
<div className="flex flex-wrap items-center gap-2.5">
{href ? (
<Link className="flex items-center gap-1 text-13 font-medium text-tertiary hover:text-primary" href={href}>
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-16">{icon}</div>}
<Link
className="flex items-center gap-1 text-sm font-medium text-custom-text-300 hover:text-custom-text-100"
href={href}
>
{icon && (
<div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>
)}
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
</Link>
) : (
<div className="flex cursor-default items-center gap-1 text-13 font-medium text-primary">
<div className="flex cursor-default items-center gap-1 text-sm font-medium text-custom-text-100">
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden">{icon}</div>}
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
</div>
@@ -10,9 +10,9 @@ export function CodeBlock({ children, className, darkerShade }: TProps) {
return (
<span
className={cn(
"px-0.5 text-11 text-tertiary bg-surface-2 font-semibold rounded-md border border-subtle",
"px-0.5 text-xs text-custom-text-300 bg-custom-background-90 font-semibold rounded-md border border-custom-border-100",
{
"text-secondary bg-layer-1 border-subtle": darkerShade,
"text-custom-text-200 bg-custom-background-80 border-custom-border-200": darkerShade,
},
className
)}
@@ -26,7 +26,7 @@ export function ConfirmDiscardModal(props: Props) {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
@@ -39,15 +39,15 @@ export function ConfirmDiscardModal(props: Props) {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[30rem]">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-tertiary">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-300">
You have unsaved changes
</Dialog.Title>
<div className="mt-2">
<p className="text-13 text-placeholder">
<p className="text-sm text-custom-text-400">
Changes you made will be lost if you go back. Do you wish to go back?
</p>
</div>
@@ -55,10 +55,10 @@ export function ConfirmDiscardModal(props: Props) {
</div>
</div>
<div className="flex justify-end items-center p-4 sm:px-6 gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Keep editing
</Button>
<Link href={onDiscardHref} className={getButtonStyling("primary", "base")}>
<Link href={onDiscardHref} className={getButtonStyling("primary", "sm")}>
Go back
</Link>
</div>
@@ -35,7 +35,7 @@ export function ControllerInput(props: Props) {
return (
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">{label}</h4>
<h4 className="text-sm text-custom-text-300">{label}</h4>
<div className="relative">
<Controller
control={control}
@@ -61,7 +61,7 @@ export function ControllerInput(props: Props) {
(showPassword ? (
<button
tabIndex={-1}
className="absolute right-3 top-2.5 flex items-center justify-center text-placeholder"
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
@@ -69,14 +69,14 @@ export function ControllerInput(props: Props) {
) : (
<button
tabIndex={-1}
className="absolute right-3 top-2.5 flex items-center justify-center text-placeholder"
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
</button>
))}
</div>
{description && <p className="pt-0.5 text-11 text-tertiary">{description}</p>}
{description && <p className="pt-0.5 text-xs text-custom-text-300">{description}</p>}
</div>
);
}
@@ -1,38 +0,0 @@
import type { Control, FieldPath, FieldValues } from "react-hook-form";
import { Controller } from "react-hook-form";
// plane internal packages
import { ToggleSwitch } from "@plane/ui";
type Props<T extends FieldValues = FieldValues> = {
control: Control<T>;
field: TControllerSwitchFormField<T>;
};
export type TControllerSwitchFormField<T extends FieldValues = FieldValues> = {
name: FieldPath<T>;
label: string;
};
export function ControllerSwitch<T extends FieldValues>(props: Props<T>) {
const {
control,
field: { name, label },
} = props;
return (
<div className="flex items-center justify-between gap-1">
<h4 className="text-sm text-custom-text-300">Refresh user attributes from {label} during sign in</h4>
<div className="relative">
<Controller
control={control}
name={name as FieldPath<T>}
render={({ field: { value, onChange } }) => {
const parsedValue = Number.parseInt(typeof value === "string" ? value : String(value ?? "0"), 10);
const isOn = !Number.isNaN(parsedValue) && parsedValue !== 0;
return <ToggleSwitch value={isOn} onChange={() => onChange(isOn ? "0" : "1")} size="sm" />;
}}
/>
</div>
</div>
);
}
@@ -22,10 +22,9 @@ export function CopyField(props: Props) {
return (
<div className="flex flex-col gap-1">
<h4 className="text-13 text-secondary">{label}</h4>
<h4 className="text-sm text-custom-text-200">{label}</h4>
<Button
variant="secondary"
size="lg"
variant="neutral-primary"
className="flex items-center justify-between py-2"
onClick={() => {
navigator.clipboard.writeText(url);
@@ -36,10 +35,10 @@ export function CopyField(props: Props) {
});
}}
>
<p className="text-13 font-medium">{url}</p>
<p className="text-sm font-medium">{url}</p>
<Copy size={18} color="#B9B9B9" />
</Button>
<div className="text-11 text-tertiary">{description}</div>
<div className="text-xs text-custom-text-300">{description}</div>
</div>
);
}
@@ -19,8 +19,8 @@ export function EmptyState({ title, description, image, primaryButton, secondary
<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-18 font-semibold sm:mt-8">{title}</h6>
{description && <p className="mb-7 px-5 text-tertiary sm:mb-8">{description}</p>}
<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
@@ -28,7 +28,6 @@ export function EmptyState({ title, description, image, primaryButton, secondary
prependIcon={primaryButton.icon}
onClick={primaryButton.onClick}
disabled={disabled}
size="lg"
>
{primaryButton.text}
</Button>
@@ -1,13 +0,0 @@
export const CORE_HEADER_SEGMENT_LABELS: Record<string, string> = {
general: "General",
ai: "Artificial Intelligence",
email: "Email",
authentication: "Authentication",
image: "Image",
google: "Google",
github: "GitHub",
gitlab: "GitLab",
gitea: "Gitea",
workspace: "Workspace",
create: "Create",
};
@@ -1 +0,0 @@
export const EXTENDED_HEADER_SEGMENT_LABELS: Record<string, string> = {};
@@ -1,44 +0,0 @@
import type { ReactNode } from "react";
// plane imports
import { cn } from "@plane/utils";
type TPageWrapperProps = {
children: ReactNode;
header?: {
title: string;
description: string | ReactNode;
actions?: ReactNode;
};
customHeader?: ReactNode;
size?: "lg" | "md";
};
export const PageWrapper = (props: TPageWrapperProps) => {
const { children, header, customHeader, size = "md" } = props;
return (
<div
className={cn("mx-auto w-full h-full space-y-6 py-4", {
"md:px-4 max-w-[1000px] 2xl:max-w-[1200px]": size === "md",
"px-4 lg:px-12": size === "lg",
})}
>
{customHeader ? (
<div className="border-b border-subtle mx-4 py-4 space-y-1 shrink-0">{customHeader}</div>
) : (
header && (
<div className="flex items-center justify-between gap-4 border-b border-subtle mx-4 py-4 space-y-1 shrink-0">
<div className={header.actions ? "flex flex-col gap-1" : "space-y-1"}>
<div className="text-primary text-h5-semibold">{header.title}</div>
<div className="text-secondary text-body-sm-regular">{header.description}</div>
</div>
{header.actions && <div className="shrink-0">{header.actions}</div>}
</div>
)
)}
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 pb-4">
{children}
</div>
</div>
);
};
@@ -22,13 +22,13 @@ export const InstanceFailureView = observer(function InstanceFailureView() {
<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" />
<h3 className="font-medium text-20 text-on-color text-center">Unable to fetch instance details.</h3>
<p className="font-medium text-14 text-center">
<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.
</p>
</div>
<div className="flex justify-center">
<Button size="lg" onClick={handleRetry}>
<Button size="md" onClick={handleRetry}>
Retry
</Button>
</div>
@@ -1,8 +1,8 @@
export function FormHeader({ heading, subHeading }: { heading: string; subHeading: string }) {
return (
<div className="flex flex-col gap-1">
<span className="text-20 font-semibold text-primary leading-7">{heading}</span>
<span className="text-16 font-semibold text-placeholder leading-7">{subHeading}</span>
<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>
);
}
@@ -8,14 +8,16 @@ export function InstanceNotReady() {
<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-24 font-bold pb-3">Welcome aboard Plane!</h1>
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
<img src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="font-medium text-14 text-placeholder">Get started by setting up your instance and workspace</p>
<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="xl" className="w-full">
<Button size="lg" className="w-full">
Get started
</Button>
</Link>
@@ -54,13 +54,13 @@ const defaultFromData: TFormData = {
export function InstanceSetupForm() {
// search params
const searchParams = useSearchParams();
const firstNameParam = searchParams?.get("first_name") || undefined;
const lastNameParam = searchParams?.get("last_name") || undefined;
const companyParam = searchParams?.get("company") || undefined;
const emailParam = searchParams?.get("email") || undefined;
const isTelemetryEnabledParam = (searchParams?.get("is_telemetry_enabled") === "True" ? true : false) || true;
const errorCode = searchParams?.get("error_code") || undefined;
const errorMessage = searchParams?.get("error_message") || undefined;
const firstNameParam = searchParams.get("first_name") || undefined;
const lastNameParam = searchParams.get("last_name") || undefined;
const companyParam = searchParams.get("company") || undefined;
const emailParam = searchParams.get("email") || undefined;
const isTelemetryEnabledParam = (searchParams.get("is_telemetry_enabled") === "True" ? true : false) || true;
const errorCode = searchParams.get("error_code") || undefined;
const errorMessage = searchParams.get("error_message") || undefined;
// state
const [showPassword, setShowPassword] = useState({
password: false,
@@ -156,11 +156,11 @@ export function InstanceSetupForm() {
<div className="flex flex-col sm:flex-row items-center gap-4">
<div className="w-full space-y-1">
<label className="text-13 text-tertiary font-medium" htmlFor="first_name">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="first_name">
First name <span className="text-red-500">*</span>
</label>
<Input
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
id="first_name"
name="first_name"
type="text"
@@ -173,11 +173,11 @@ export function InstanceSetupForm() {
/>
</div>
<div className="w-full space-y-1">
<label className="text-13 text-tertiary font-medium" htmlFor="last_name">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="last_name">
Last name <span className="text-red-500">*</span>
</label>
<Input
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
id="last_name"
name="last_name"
type="text"
@@ -191,11 +191,11 @@ export function InstanceSetupForm() {
</div>
<div className="w-full space-y-1">
<label className="text-13 text-tertiary font-medium" htmlFor="email">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
Email <span className="text-red-500">*</span>
</label>
<Input
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
id="email"
name="email"
type="email"
@@ -207,16 +207,16 @@ export function InstanceSetupForm() {
autoComplete="on"
/>
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
<p className="px-1 text-11 text-red-500">{errorData.message}</p>
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
)}
</div>
<div className="w-full space-y-1">
<label className="text-13 text-tertiary font-medium" htmlFor="company_name">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="company_name">
Company name <span className="text-red-500">*</span>
</label>
<Input
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
id="company_name"
name="company_name"
type="text"
@@ -228,17 +228,17 @@ export function InstanceSetupForm() {
</div>
<div className="w-full space-y-1">
<label className="text-13 text-tertiary font-medium" htmlFor="password">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
Set a password <span className="text-red-500">*</span>
</label>
<div className="relative">
<Input
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
id="password"
name="password"
type={showPassword.password ? "text" : "password"}
inputSize="md"
placeholder="New password"
placeholder="New password..."
value={formData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
@@ -250,7 +250,7 @@ export function InstanceSetupForm() {
<button
type="button"
tabIndex={-1}
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => handleShowPassword("password")}
>
<EyeOff className="h-4 w-4" />
@@ -259,7 +259,7 @@ export function InstanceSetupForm() {
<button
type="button"
tabIndex={-1}
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => handleShowPassword("password")}
>
<Eye className="h-4 w-4" />
@@ -267,13 +267,13 @@ export function InstanceSetupForm() {
)}
</div>
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
<p className="px-1 text-11 text-red-500">{errorData.message}</p>
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
)}
<PasswordStrengthIndicator password={formData.password} isFocused={isPasswordInputFocused} />
</div>
<div className="w-full space-y-1">
<label className="text-13 text-tertiary font-medium" htmlFor="confirm_password">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="confirm_password">
Confirm password <span className="text-red-500">*</span>
</label>
<div className="relative">
@@ -285,7 +285,7 @@ export function InstanceSetupForm() {
value={formData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
className="w-full border border-subtle !bg-surface-1 pr-12 placeholder:text-placeholder"
className="w-full border border-custom-border-100 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
/>
@@ -293,7 +293,7 @@ export function InstanceSetupForm() {
<button
type="button"
tabIndex={-1}
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => handleShowPassword("retypePassword")}
>
<EyeOff className="h-4 w-4" />
@@ -302,7 +302,7 @@ export function InstanceSetupForm() {
<button
type="button"
tabIndex={-1}
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => handleShowPassword("retypePassword")}
>
<Eye className="h-4 w-4" />
@@ -311,7 +311,7 @@ export function InstanceSetupForm() {
</div>
{!!formData.confirm_password &&
formData.password !== formData.confirm_password &&
renderPasswordMatchError && <span className="text-13 text-red-500">Passwords don{"'"}t match</span>}
renderPasswordMatchError && <span className="text-sm text-red-500">Passwords don{"'"}t match</span>}
</div>
<div className="relative flex gap-2">
@@ -324,14 +324,14 @@ export function InstanceSetupForm() {
checked={formData.is_telemetry_enabled}
/>
</div>
<label className="text-13 text-tertiary font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
<label className="text-sm text-custom-text-300 font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
Allow Plane to anonymously collect usage events.{" "}
<a
tabIndex={-1}
href="https://developers.plane.so/self-hosting/telemetry"
target="_blank"
rel="noopener noreferrer"
className="text-13 font-medium text-blue-500 hover:text-blue-600 flex-shrink-0"
className="text-sm font-medium text-blue-500 hover:text-blue-600 flex-shrink-0"
>
See More
</a>
@@ -339,7 +339,7 @@ export function InstanceSetupForm() {
</div>
<div className="py-2">
<Button type="submit" size="xl" className="w-full" disabled={isButtonDisabled}>
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</div>
@@ -18,19 +18,19 @@ export const NewUserPopup = observer(function NewUserPopup() {
if (!isNewUserPopup) return <></>;
return (
<div className="absolute bottom-8 right-8 p-6 w-96 border border-subtle shadow-md rounded-lg bg-surface-1">
<div className="absolute bottom-8 right-8 p-6 w-96 border border-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
<div className="flex gap-4">
<div className="grow">
<div className="text-14 font-semibold">Create workspace</div>
<div className="py-2 text-13 font-medium text-tertiary">
<div className="text-base font-semibold">Create workspace</div>
<div className="py-2 text-sm font-medium text-custom-text-300">
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
workspace.
</div>
<div className="flex items-center gap-4 pt-2">
<Link href="/workspace/create" className={getButtonStyling("primary", "lg")}>
<Link href="/workspace/create" className={getButtonStyling("primary", "sm")}>
Create workspace
</Link>
<Button variant="secondary" size="lg" onClick={toggleNewUserPopup}>
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
Close
</Button>
</div>
@@ -23,19 +23,18 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
key={workspaceId}
href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`}
target="_blank"
className="group flex items-center justify-between p-3 gap-2.5 truncate border border-subtle hover:border-subtle-1 bg-layer-1 hover:bg-layer-1-hover hover:shadow-raised-100 rounded-lg"
rel="noreferrer"
className="group flex items-center justify-between p-4 gap-2.5 truncate border border-custom-border-200/70 hover:border-custom-border-200 hover:bg-custom-background-90 rounded-md"
>
<div className="flex items-start gap-4">
<span
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 mt-1 text-11 uppercase ${
!workspace?.logo_url && "rounded-lg bg-accent-primary text-on-color"
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 mt-1 text-xs uppercase ${
!workspace?.logo_url && "rounded bg-custom-primary-500 text-white"
}`}
>
{workspace?.logo_url && workspace.logo_url !== "" ? (
<img
src={getFileURL(workspace.logo_url)}
className="absolute left-0 top-0 h-full w-full rounded-sm object-cover"
className="absolute left-0 top-0 h-full w-full rounded object-cover"
alt="Workspace Logo"
/>
) : (
@@ -44,30 +43,30 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
</span>
<div className="flex flex-col items-start gap-1">
<div className="flex flex-wrap w-full items-center gap-2.5">
<h3 className={`text-14 font-medium capitalize`}>{workspace.name}</h3>/
<h3 className={`text-base font-medium capitalize`}>{workspace.name}</h3>/
<Tooltip tooltipContent="The unique URL of your workspace">
<h4 className="text-13 text-tertiary">[{workspace.slug}]</h4>
<h4 className="text-sm text-custom-text-300">[{workspace.slug}]</h4>
</Tooltip>
</div>
{workspace.owner.email && (
<div className="flex items-center gap-1 text-11">
<h3 className="text-secondary font-medium">Owned by:</h3>
<h4 className="text-tertiary">{workspace.owner.email}</h4>
<div className="flex items-center gap-1 text-xs">
<h3 className="text-custom-text-200 font-medium">Owned by:</h3>
<h4 className="text-custom-text-300">{workspace.owner.email}</h4>
</div>
)}
<div className="flex items-center gap-2.5 text-11">
<div className="flex items-center gap-2.5 text-xs">
{workspace.total_projects !== null && (
<span className="flex items-center gap-1">
<h3 className="text-secondary font-medium">Total projects:</h3>
<h4 className="text-tertiary">{workspace.total_projects}</h4>
<h3 className="text-custom-text-200 font-medium">Total projects:</h3>
<h4 className="text-custom-text-300">{workspace.total_projects}</h4>
</span>
)}
{workspace.total_members !== null && (
<>
<span className="flex items-center gap-1">
<h3 className="text-secondary font-medium">Total members:</h3>
<h4 className="text-tertiary">{workspace.total_members}</h4>
<h3 className="text-custom-text-200 font-medium">Total members:</h3>
<h4 className="text-custom-text-300">{workspace.total_members}</h4>
</span>
</>
)}
@@ -75,7 +74,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
</div>
</div>
<div className="flex-shrink-0">
<ExternalLink size={16} className="text-placeholder group-hover:text-secondary" />
<ExternalLink size={14} className="text-custom-text-400 group-hover:text-custom-text-200" />
</div>
</a>
);
-19
View File
@@ -1,19 +0,0 @@
import type { TInstanceAuthenticationModes } from "@plane/types";
import { getCoreAuthenticationModesMap } from "./core";
import type { TGetAuthenticationModeProps } from "./types";
export const useAuthenticationModes = (props: TGetAuthenticationModeProps): TInstanceAuthenticationModes[] => {
// derived values
const authenticationModes = getCoreAuthenticationModesMap(props);
const availableAuthenticationModes: TInstanceAuthenticationModes[] = [
authenticationModes["unique-codes"],
authenticationModes["passwords-login"],
authenticationModes["google"],
authenticationModes["github"],
authenticationModes["gitlab"],
authenticationModes["gitea"],
];
return availableAuthenticationModes;
};
-7
View File
@@ -1,7 +0,0 @@
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
export type TGetAuthenticationModeProps = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
resolvedTheme: string | undefined;
};
+1 -1
View File
@@ -1,6 +1,6 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/providers/store.provider";
import { StoreContext } from "@/app/(all)/store.provider";
import type { IInstanceStore } from "@/store/instance.store";
export const useInstance = (): IInstanceStore => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/providers/store.provider";
import { StoreContext } from "@/app/(all)/store.provider";
import type { IThemeStore } from "@/store/theme.store";
export const useTheme = (): IThemeStore => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/providers/store.provider";
import { StoreContext } from "@/app/(all)/store.provider";
import type { IUserStore } from "@/store/user.store";
export const useUser = (): IUserStore => {
@@ -1,6 +1,6 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/providers/store.provider";
import { StoreContext } from "@/app/(all)/store.provider";
import type { IWorkspaceStore } from "@/store/workspace.store";
export const useWorkspace = (): IWorkspaceStore => {
@@ -1,46 +0,0 @@
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// plane imports
import { WorkspaceIcon } from "@plane/propel/icons";
// types
import type { TSidebarMenuItem } from "./types";
export type TCoreSidebarMenuKey = "general" | "email" | "workspace" | "authentication" | "ai" | "image";
export const coreSidebarMenuLinks: Record<TCoreSidebarMenuKey, TSidebarMenuItem> = {
general: {
Icon: Cog,
name: "General",
description: "Identify your instances and get key details.",
href: `/general/`,
},
email: {
Icon: Mail,
name: "Email",
description: "Configure your SMTP controls.",
href: `/email/`,
},
workspace: {
Icon: WorkspaceIcon,
name: "Workspaces",
description: "Manage all workspaces on this instance.",
href: `/workspace/`,
},
authentication: {
Icon: Lock,
name: "Authentication",
description: "Configure authentication modes.",
href: `/authentication/`,
},
ai: {
Icon: BrainCog,
name: "Artificial intelligence",
description: "Configure your OpenAI creds.",
href: `/ai/`,
},
image: {
Icon: Image,
name: "Images in Plane",
description: "Allow third-party image libraries.",
href: `/image/`,
},
};
@@ -1,14 +0,0 @@
// local imports
import { coreSidebarMenuLinks } from "./core";
import type { TSidebarMenuItem } from "./types";
export function useSidebarMenu(): TSidebarMenuItem[] {
return [
coreSidebarMenuLinks.general,
coreSidebarMenuLinks.email,
coreSidebarMenuLinks.authentication,
coreSidebarMenuLinks.workspace,
coreSidebarMenuLinks.ai,
coreSidebarMenuLinks.image,
];
}
@@ -1,8 +0,0 @@
import type { LucideIcon } from "lucide-react";
export type TSidebarMenuItem = {
Icon: LucideIcon | React.ComponentType<{ className?: string }>;
name: string;
description: string;
href: string;
};
-3
View File
@@ -1,3 +0,0 @@
export function ExtendedProviders({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
-10
View File
@@ -1,10 +0,0 @@
import { CoreProviders } from "./core";
import { ExtendedProviders } from "./extended";
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<CoreProviders>
<ExtendedProviders>{children}</ExtendedProviders>
</CoreProviders>
);
}
@@ -0,0 +1 @@
export * from "ce/components/authentication/authentication-modes";
@@ -0,0 +1 @@
export * from "./authentication-modes";
+1
View File
@@ -0,0 +1 @@
export * from "ce/components/common";
+9 -10
View File
@@ -1,7 +1,7 @@
{
"name": "admin",
"description": "Admin UI for Plane",
"version": "1.2.0",
"version": "1.1.0",
"license": "AGPL-3.0",
"private": true,
"type": "module",
@@ -11,17 +11,14 @@
"preview": "react-router build && serve -s build/client -l 3001",
"start": "serve -s build/client -l 3001",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist && rm -rf build",
"check:lint": "eslint . --cache --cache-location node_modules/.cache/eslint/ --max-warnings=485",
"check:lint": "eslint . --max-warnings 19",
"check:types": "react-router typegen && tsc --noEmit",
"check:format": "prettier . --cache --check",
"fix:lint": "eslint . --cache --cache-location node_modules/.cache/eslint/ --fix --max-warnings=485",
"fix:format": "prettier . --cache --write"
"check:format": "prettier --check \"**/*.{ts,tsx,md,json,css,scss}\"",
"fix:lint": "eslint . --fix",
"fix:format": "prettier --write \"**/*.{ts,tsx,md,json,css,scss}\""
},
"dependencies": {
"@bprogress/core": "catalog:",
"@fontsource-variable/inter": "5.2.8",
"@fontsource/ibm-plex-mono": "5.2.7",
"@fontsource/material-symbols-rounded": "5.2.30",
"@headlessui/react": "^1.7.19",
"@plane/constants": "workspace:*",
"@plane/hooks": "workspace:*",
@@ -40,7 +37,7 @@
"lucide-react": "catalog:",
"mobx": "catalog:",
"mobx-react": "catalog:",
"next-themes": "0.4.6",
"next-themes": "^0.2.1",
"react": "catalog:",
"react-dom": "catalog:",
"react-hook-form": "7.51.5",
@@ -51,14 +48,16 @@
"uuid": "catalog:"
},
"devDependencies": {
"@dotenvx/dotenvx": "catalog:",
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@prettier/plugin-oxc": "0.0.4",
"@react-router/dev": "catalog:",
"@types/lodash-es": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"dotenv": "^16.4.5",
"typescript": "catalog:",
"vite": "catalog:",
"vite-tsconfig-paths": "^5.1.4"

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