Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 202336dd9c |
@@ -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
@@ -105,8 +105,9 @@ CLAUDE.md
|
||||
|
||||
build/
|
||||
.react-router/
|
||||
AGENTS.md
|
||||
|
||||
build/
|
||||
.react-router/
|
||||
AGENTS.md
|
||||
temp/
|
||||
scripts/
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pnpm lint-staged
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
.next/
|
||||
.react-router/
|
||||
.turbo/
|
||||
.vite/
|
||||
build/
|
||||
dist/
|
||||
node_modules/
|
||||
out/
|
||||
pnpm-lock.yaml
|
||||
storybook-static/
|
||||
+2
-11
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
eslint.config.mjs @lifeiscontent
|
||||
+8
-10
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
[](https://reactrouter.com/)
|
||||
[](https://nextjs.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://nodejs.org/en)
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
.next/*
|
||||
.react-router/*
|
||||
.vite/*
|
||||
out/*
|
||||
public/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -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/
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"plugins": ["@prettier/plugin-oxc"]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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.
|
||||
<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.
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+37
-16
@@ -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>
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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'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'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'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'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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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} />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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
@@ -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() {
|
||||
|
||||
+69
-20
@@ -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";
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,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,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,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;
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export function ExtendedProviders({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "ce/components/common";
|
||||
+9
-10
@@ -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
Reference in New Issue
Block a user