Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ebff5e5bd | |||
| 88f4d8253d | |||
| 9b90fbf5aa | |||
| d9ab60104e | |||
| f0b98ceb8f | |||
| 4cac953cd1 | |||
| 508e332a23 | |||
| b906d42e1d | |||
| 8b0a8160a1 | |||
| b7621c62eb | |||
| 617c7ab734 | |||
| b675c5c7a7 | |||
| 2f80d59ba9 | |||
| 8bd23ac44b | |||
| dbda7504b2 | |||
| 61db42bcb7 | |||
| 1f06b67c66 | |||
| 1338e9d2a4 | |||
| 7c6a6a3432 | |||
| d8930c5188 | |||
| 88f5a063d6 | |||
| df710e00dc | |||
| e8047b6f1c | |||
| fa63964566 | |||
| be1113b170 | |||
| e7974afd8b | |||
| fc1c7fe235 | |||
| 22339b9786 | |||
| d86418aad8 | |||
| 0263f07977 | |||
| 2ac5efe2f0 | |||
| 1b427392c4 | |||
| 192ed9ba8d | |||
| 119cf39b4d | |||
| 234b088c4a | |||
| bc37edbe9a | |||
| 7451d5e125 | |||
| 58e48a995b | |||
| 248acdc4c1 | |||
| 7b41ffa08e | |||
| 906c1b51bb | |||
| 8e518966c2 | |||
| f8f764622a | |||
| f5fe8d0a7e | |||
| e0c97c5471 | |||
| 0370a1bfdd | |||
| 5e621cf620 | |||
| f1761c65b5 | |||
| 97e21ba21c | |||
| 647813a6ab | |||
| 76ebf395e6 | |||
| 67dfe91890 | |||
| b53016b449 | |||
| e20f686398 | |||
| ff544c98b7 | |||
| be722f708d | |||
| 43b3a7730e | |||
| 20510bb2dd | |||
| 7124e8e7ce | |||
| df1a512a80 | |||
| 639a2aab41 | |||
| 491b5bc6cc | |||
| 368af223db | |||
| 8bb7ebb725 | |||
| 079a624006 | |||
| 0bfb74d4c0 | |||
| 362d29c7b0 | |||
| a55253d242 | |||
| 69b64680d1 | |||
| 2f45bfb7f6 | |||
| 4b59998e52 | |||
| f70384bff7 | |||
| 7caa1bb482 | |||
| af939fca41 | |||
| a3e651854a | |||
| 2240ac0e74 | |||
| 11e7bd115b | |||
| 0ab94ed6d6 | |||
| a9e9cb2983 | |||
| f0bc2bd3bd | |||
| 7659997b53 | |||
| 316856a555 | |||
| f41e121e58 | |||
| 85daa1572c | |||
| b359849606 | |||
| 5ea74e1112 | |||
| d9ea119588 | |||
| d34c7f9a0e | |||
| f81e120328 | |||
| 85d90030cf | |||
| 82c970ac4b | |||
| 392c8cf2e1 | |||
| c3906048ea | |||
| 1090b3e938 | |||
| fe867135c4 | |||
| 65166d19be | |||
| 3bc85c8430 | |||
| cf84ade52c | |||
| 9699a01fb8 | |||
| bc96d34cb7 | |||
| 739b1bfb75 | |||
| 22da26742c | |||
| 63f3a57022 | |||
| 36d42856e9 | |||
| 105ac5ece5 | |||
| b8a41ad5a0 | |||
| 7c74d0a403 | |||
| 5f7ffcb37a | |||
| 584a1aa725 | |||
| 1e1520b85f | |||
| 0bc45e3047 | |||
| cacd1b489e | |||
| e650b19933 | |||
| ec478a8979 | |||
| e07d91e857 | |||
| 9ccb3e321a | |||
| f2c046dff9 | |||
| 2a378b3bc1 | |||
| 8b0a797906 | |||
| cea6f7530b | |||
| a7e2e596bf | |||
| 980428b204 | |||
| f428c3bdaf | |||
| a05cd88a53 | |||
| 60220801ac |
@@ -1,7 +1,7 @@
|
||||
name: Bug report
|
||||
description: Create a bug report to help us improve Plane
|
||||
title: "[bug]: "
|
||||
labels: [🐛bug]
|
||||
labels: [🐛bug, plane]
|
||||
assignees: [vihar, pushya22]
|
||||
body:
|
||||
- type: markdown
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Feature request
|
||||
description: Suggest a feature to improve Plane
|
||||
title: "[feature]: "
|
||||
labels: [✨feature]
|
||||
labels: [✨feature, plane]
|
||||
assignees: [vihar, pushya22]
|
||||
body:
|
||||
- type: markdown
|
||||
|
||||
@@ -27,11 +27,13 @@ jobs:
|
||||
github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.requested_reviewers != null
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
python-version: "3.12.x"
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'apps/api/requirements.txt'
|
||||
- name: Install Pylint
|
||||
run: python -m pip install ruff
|
||||
- name: Install API Dependencies
|
||||
|
||||
@@ -17,10 +17,11 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-lint:
|
||||
name: Build and lint web apps
|
||||
# Format check has no build dependencies - run immediately in parallel
|
||||
check-format:
|
||||
name: check:format
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
timeout-minutes: 10
|
||||
if: |
|
||||
github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.requested_reviewers != null
|
||||
@@ -29,28 +30,140 @@ jobs:
|
||||
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 50
|
||||
filter: blob:none
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
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: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint Affected
|
||||
run: pnpm turbo run check:lint --affected
|
||||
|
||||
- name: Check Affected format
|
||||
- name: Check formatting
|
||||
run: pnpm turbo run check:format --affected
|
||||
|
||||
- name: Check Affected types
|
||||
run: pnpm turbo run check:types --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: Build 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
|
||||
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
|
||||
|
||||
+1
-2
@@ -105,9 +105,8 @@ CLAUDE.md
|
||||
|
||||
build/
|
||||
.react-router/
|
||||
AGENTS.md
|
||||
|
||||
build/
|
||||
.react-router/
|
||||
AGENTS.md
|
||||
temp/
|
||||
scripts/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pnpm lint-staged
|
||||
@@ -1,34 +1,54 @@
|
||||
# Enforce pnpm workspace behavior and allow Turbo's lifecycle hooks if scripts are disabled
|
||||
# This repo uses pnpm with workspaces.
|
||||
# ------------------------------
|
||||
# Core Workspace Behavior
|
||||
# ------------------------------
|
||||
|
||||
# Prefer linking local workspace packages when available
|
||||
prefer-workspace-packages=true
|
||||
link-workspace-packages=true
|
||||
shared-workspace-lockfile=true
|
||||
# Always prefer using local workspace packages when available
|
||||
prefer-workspace-packages = true
|
||||
|
||||
# Make peer installs smoother across the monorepo
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
# Symlink workspace packages instead of duplicating them
|
||||
link-workspace-packages = 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)
|
||||
# Use a single lockfile across the whole monorepo
|
||||
shared-workspace-lockfile = true
|
||||
|
||||
public-hoist-pattern[]=*eslint*
|
||||
public-hoist-pattern[]=prettier
|
||||
public-hoist-pattern[]=typescript
|
||||
# Ensure packages added from workspace save using workspace: protocol
|
||||
save-workspace-protocol = true
|
||||
|
||||
# Reproducible installs across CI and dev
|
||||
prefer-frozen-lockfile=true
|
||||
|
||||
# Prefer resolving to highest versions in monorepo to reduce duplication
|
||||
resolution-mode=highest
|
||||
# ------------------------------
|
||||
# Dependency Resolution
|
||||
# ------------------------------
|
||||
|
||||
# Speed up native module builds by caching side effects
|
||||
side-effects-cache=true
|
||||
# Choose the highest compatible version across the workspace
|
||||
# → reduces fragmentation & node_modules bloat
|
||||
resolution-mode = highest
|
||||
|
||||
# Speed up local dev by reusing local store when possible
|
||||
prefer-offline=true
|
||||
# Automatically install peer dependencies instead of forcing every package to declare them
|
||||
auto-install-peers = true
|
||||
|
||||
# Ensure workspace protocol is used when adding internal deps
|
||||
save-workspace-protocol=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
|
||||
@@ -0,0 +1,10 @@
|
||||
.next/
|
||||
.react-router/
|
||||
.turbo/
|
||||
.vite/
|
||||
build/
|
||||
dist/
|
||||
node_modules/
|
||||
out/
|
||||
pnpm-lock.yaml
|
||||
storybook-static/
|
||||
+11
-2
@@ -1,6 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["packages/codemods/**/*"],
|
||||
"options": {
|
||||
"printWidth": 80
|
||||
}
|
||||
}
|
||||
],
|
||||
"plugins": ["@prettier/plugin-oxc"],
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"plugins": ["@prettier/plugin-oxc"]
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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
|
||||
@@ -0,0 +1 @@
|
||||
eslint.config.mjs @lifeiscontent
|
||||
+10
-8
@@ -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 use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
|
||||
- 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`.
|
||||
|
||||
## Ways to contribute
|
||||
|
||||
@@ -187,18 +187,19 @@ 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**
|
||||
@@ -210,6 +211,7 @@ Adding a new language involves several steps to ensure it integrates seamlessly
|
||||
|
||||
3. **Update import logic**
|
||||
Modify the language import logic to include your new language:
|
||||
|
||||
```ts
|
||||
private importLanguageFile(language: TLanguage): Promise<any> {
|
||||
switch (language) {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
.next/*
|
||||
.react-router/*
|
||||
.vite/*
|
||||
out/*
|
||||
public/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
@@ -1,18 +0,0 @@
|
||||
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,8 +1,10 @@
|
||||
.next
|
||||
.react-router
|
||||
.vite
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dist/
|
||||
.next/
|
||||
.react-router/
|
||||
.turbo/
|
||||
.vite/
|
||||
build/
|
||||
dist/
|
||||
node_modules/
|
||||
out/
|
||||
pnpm-lock.yaml
|
||||
storybook-static/
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"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.5.8
|
||||
RUN pnpm add -g turbo@2.6.3
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -62,10 +62,12 @@ COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
|
||||
# Fetch dependencies to cache store, then install offline with dev deps
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
|
||||
# Copy full directory structure before fetch to ensure all package.json files are available
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
# Fetch dependencies to cache store, then install offline with dev deps
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
|
||||
|
||||
# Build only the admin package
|
||||
@@ -73,7 +75,7 @@ RUN pnpm turbo run build --filter=admin
|
||||
|
||||
# =========================================================================== #
|
||||
|
||||
FROM nginx:1.27-alpine AS production
|
||||
FROM nginx:1.29-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-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary 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-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary 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-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 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>
|
||||
<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,12 +114,12 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
<div className="flex flex-col gap-2 items-start">
|
||||
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
|
||||
<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">
|
||||
<div className="relative inline-flex items-center gap-2 rounded-sm border border-accent-strong/20 bg-accent-primary/10 px-4 py-2 text-11 text-accent-secondary">
|
||||
<Lightbulb height="14" width="14" />
|
||||
<div>
|
||||
If you have a preferred AI models vendor, please get in{" "}
|
||||
|
||||
@@ -16,9 +16,9 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">AI features for all your workspaces</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-18 font-medium text-primary">AI features for all your workspaces</div>
|
||||
<div className="text-13 font-regular text-tertiary">
|
||||
Configure your AI API credentials so Plane AI features are turned on for all your workspaces.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,9 @@ 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";
|
||||
@@ -69,7 +68,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://gitea.com/user/settings/applications"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Gitea OAuth application settings.
|
||||
@@ -91,7 +90,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://gitea.com/user/settings/applications"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Gitea OAuth application settings.
|
||||
@@ -117,7 +116,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href={`${control._formValues.GITEA_HOST || "https://gitea.com"}/user/settings/applications`}
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
@@ -163,7 +162,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-xl font-medium">Gitea-provided details for Plane</div>
|
||||
<div className="pt-2.5 text-18 font-medium">Gitea-provided details for Plane</div>
|
||||
{GITEA_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@@ -179,22 +178,24 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
))}
|
||||
<div className="flex flex-col gap-1 pt-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
|
||||
<div className="pt-2 text-xl font-medium">Plane-provided details for Gitea</div>
|
||||
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-1/60 rounded-lg">
|
||||
<div className="pt-2 text-18 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} />
|
||||
))}
|
||||
|
||||
@@ -58,7 +58,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="border-b border-subtle 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."
|
||||
|
||||
@@ -60,7 +60,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub OAuth application settings.
|
||||
@@ -82,7 +82,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub OAuth application settings.
|
||||
@@ -116,7 +116,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
@@ -139,7 +139,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
@@ -185,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-xl font-medium">GitHub-provided details for Plane</div>
|
||||
<div className="pt-2.5 text-18 font-medium">GitHub-provided details for Plane</div>
|
||||
{GITHUB_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@@ -201,25 +201,27 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
))}
|
||||
<div className="flex flex-col gap-1 pt-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} 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-xl font-medium">Plane-provided details for GitHub</div>
|
||||
<div className="pt-2 text-18 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-custom-background-80 rounded-lg">
|
||||
<div className="flex flex-col gap-y-4 px-6 py-4 bg-layer-1 rounded-lg">
|
||||
{GITHUB_COMMON_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
@@ -227,11 +229,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-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
|
||||
<div className="px-6 py-3 bg-layer-1/60 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary">
|
||||
<Monitor className="w-3 h-3" />
|
||||
Web
|
||||
</div>
|
||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
|
||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-layer-1">
|
||||
{GITHUB_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
|
||||
@@ -67,7 +67,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="border-b border-subtle 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."
|
||||
|
||||
@@ -71,7 +71,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitLab OAuth application settings
|
||||
@@ -94,7 +94,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitLab OAuth application settings
|
||||
@@ -120,7 +120,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitLab OAuth application
|
||||
@@ -167,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-xl font-medium">GitLab-provided details for Plane</div>
|
||||
<div className="pt-2.5 text-18 font-medium">GitLab-provided details for Plane</div>
|
||||
{GITLAB_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@@ -183,22 +183,24 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
))}
|
||||
<div className="flex flex-col gap-1 pt-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
|
||||
<div className="pt-2 text-xl font-medium">Plane-provided details for GitLab</div>
|
||||
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-1/60 rounded-lg">
|
||||
<div className="pt-2 text-18 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} />
|
||||
))}
|
||||
|
||||
@@ -58,7 +58,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="border-b border-subtle 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."
|
||||
|
||||
@@ -58,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-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
@@ -80,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-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
@@ -105,7 +105,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
<a
|
||||
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
@@ -127,7 +127,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
<a
|
||||
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
@@ -172,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-xl font-medium">Google-provided details for Plane</div>
|
||||
<div className="pt-2.5 text-18 font-medium">Google-provided details for Plane</div>
|
||||
{GOOGLE_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@@ -188,25 +188,27 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
))}
|
||||
<div className="flex flex-col gap-1 pt-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} 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-xl font-medium">Plane-provided details for Google</div>
|
||||
<div className="pt-2 text-18 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-custom-background-80 rounded-lg">
|
||||
<div className="flex flex-col gap-y-4 px-6 py-4 bg-layer-1 rounded-lg">
|
||||
{GOOGLE_COMMON_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
@@ -214,11 +216,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-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
|
||||
<div className="px-6 py-3 bg-layer-1/60 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary">
|
||||
<Monitor className="w-3 h-3" />
|
||||
Web
|
||||
</div>
|
||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
|
||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-layer-1">
|
||||
{GOOGLE_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
|
||||
@@ -58,7 +58,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="border-b border-subtle 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
|
||||
|
||||
@@ -57,20 +57,20 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Manage authentication modes for your instance</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-18 font-medium text-primary">Manage authentication modes for your instance</div>
|
||||
<div className="text-13 font-regular text-tertiary">
|
||||
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={cn("w-full flex items-center gap-14 rounded-sm")}>
|
||||
<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")}>
|
||||
<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>
|
||||
</div>
|
||||
@@ -92,7 +92,7 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-lg font-medium pt-6">Available authentication modes</div>
|
||||
<div className="text-16 font-medium pt-6">Available authentication modes</div>
|
||||
<AuthenticationModes disabled={isSubmitting} updateConfig={updateConfig} />
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -157,13 +157,12 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||
/>
|
||||
))}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Email security</h4>
|
||||
<h4 className="text-13 text-tertiary">Email security</h4>
|
||||
<CustomSelect
|
||||
value={emailSecurityKey}
|
||||
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
|
||||
onChange={handleEmailSecurityChange}
|
||||
buttonClassName="rounded-md border-custom-border-200"
|
||||
optionsClassName="w-full"
|
||||
buttonClassName="rounded-md border-subtle"
|
||||
input
|
||||
>
|
||||
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
|
||||
@@ -174,12 +173,12 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||
</CustomSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-custom-border-100">
|
||||
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-subtle">
|
||||
<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-sm font-medium text-custom-text-100">Authentication</div>
|
||||
<div className="text-xs font-normal text-custom-text-300">
|
||||
<div className="text-13 font-medium text-primary">Authentication</div>
|
||||
<div className="text-11 font-regular text-tertiary">
|
||||
This is optional, but we recommend setting up a username and a password for your SMTP server.
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,6 +204,7 @@ 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}
|
||||
@@ -212,7 +212,8 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
onClick={() => setIsSendTestEmailModalOpen(true)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid}
|
||||
|
||||
@@ -51,14 +51,14 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
|
||||
return (
|
||||
<>
|
||||
<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 items-center justify-between gap-4 border-b border-subtle 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">
|
||||
<div className="text-18 font-medium text-primary">Secure emails from your own instance</div>
|
||||
<div className="text-13 font-regular text-tertiary">
|
||||
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">
|
||||
<div className="text-13 font-regular text-tertiary">
|
||||
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>
|
||||
<span className="text-danger">Misconfigs can lead to email bounces and errors.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function SendTestEmailModal(props: Props) {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
<div className="fixed inset-0 bg-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-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">
|
||||
<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">
|
||||
{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-lg"
|
||||
className="w-full resize-none text-16"
|
||||
tabIndex={1}
|
||||
/>
|
||||
)}
|
||||
{sendEmailStep === ESendEmailSteps.SUCCESS && (
|
||||
<div className="flex flex-col gap-y-4 text-sm">
|
||||
<div className="flex flex-col gap-y-4 text-13">
|
||||
<p>
|
||||
We have sent the test email to {receiverEmail}. Please check your spam folder if you cannot find
|
||||
it.
|
||||
@@ -114,13 +114,13 @@ 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-sm">{error}</div>}
|
||||
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-13">{error}</div>}
|
||||
<div className="flex items-center gap-2 justify-end mt-5">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={2}>
|
||||
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={2}>
|
||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Cancel" : "Close"}
|
||||
</Button>
|
||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
|
||||
<Button variant="primary" size="sm" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
|
||||
<Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
|
||||
{isLoading ? "Sending email..." : "Send email"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -63,8 +63,8 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-medium">Instance details</div>
|
||||
<div className="space-y-4">
|
||||
<div className="text-16 font-medium text-primary">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,54 +78,52 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Email</h4>
|
||||
<h4 className="text-13 text-tertiary">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-custom-text-400"
|
||||
className="w-full cursor-not-allowed !text-placeholder"
|
||||
autoComplete="on"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Instance ID</h4>
|
||||
<h4 className="text-13 text-tertiary">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-custom-text-400"
|
||||
className="w-full cursor-not-allowed rounded-md font-medium !text-placeholder"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-medium">Chat + telemetry</div>
|
||||
<div className="space-y-4">
|
||||
<div className="text-16 font-medium text-primary">Chat + telemetry</div>
|
||||
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
|
||||
<div className="flex items-center gap-14 px-4 py-3 border border-custom-border-200 rounded">
|
||||
<div className="flex items-center gap-14 px-4 py-3 border border-subtle rounded-sm">
|
||||
<div className="grow flex items-center gap-4">
|
||||
<div className="shrink-0">
|
||||
<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 className="flex items-center justify-center w-10 h-10 bg-layer-1 rounded-full">
|
||||
<Telescope className="w-6 h-6 text-tertiary/80 p-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<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">
|
||||
<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">
|
||||
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-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
our Telemetry Policy.
|
||||
@@ -146,7 +144,7 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -49,17 +49,17 @@ export const IntercomConfig = observer(function IntercomConfig(props: TIntercomC
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-14 px-4 py-3 border border-custom-border-200 rounded">
|
||||
<div className="flex items-center gap-14 px-4 py-3 border border-subtle rounded-sm">
|
||||
<div className="grow flex items-center gap-4">
|
||||
<div className="shrink-0">
|
||||
<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 className="flex items-center justify-center w-10 h-10 bg-layer-1 rounded-full">
|
||||
<MessageSquare className="w-6 h-6 text-tertiary/80 p-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grow">
|
||||
<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">
|
||||
<div className="text-13 font-medium text-primary leading-5">Chat with us</div>
|
||||
<div className="text-11 font-regular text-tertiary leading-5">
|
||||
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
|
||||
automatically.
|
||||
</div>
|
||||
|
||||
@@ -11,9 +11,9 @@ function GeneralPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">General settings</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-18 font-medium text-primary">General settings</div>
|
||||
<div className="text-13 font-regular text-tertiary">
|
||||
Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your
|
||||
instance.
|
||||
</div>
|
||||
|
||||
@@ -12,10 +12,10 @@ export const HamburgerToggle = observer(function HamburgerToggle() {
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
return (
|
||||
<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"
|
||||
className="w-7 h-7 rounded-sm flex justify-center items-center bg-layer-1 transition-all hover:bg-layer-1-hover cursor-pointer group md:hidden"
|
||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||
>
|
||||
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />
|
||||
<Menu size={14} className="text-secondary group-hover:text-primary transition-all" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -71,7 +71,7 @@ export const AdminHeader = observer(function AdminHeader() {
|
||||
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-custom-sidebar-border-200 bg-custom-sidebar-background-100 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-subtle bg-surface-1 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<HamburgerToggle />
|
||||
{breadcrumbItems.length >= 0 && (
|
||||
@@ -82,7 +82,7 @@ export const AdminHeader = observer(function AdminHeader() {
|
||||
<BreadcrumbLink
|
||||
href="/general/"
|
||||
label="Settings"
|
||||
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
||||
icon={<Settings className="h-4 w-4 text-tertiary" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -56,7 +56,7 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
||||
<a
|
||||
href="https://unsplash.com/documentation#creating-a-developer-account"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more.
|
||||
@@ -70,7 +70,7 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -16,9 +16,9 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Third-party image libraries</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-18 font-medium text-primary">Third-party image libraries</div>
|
||||
<div className="text-13 font-regular text-tertiary">
|
||||
Let your users search and choose images from third-party libraries
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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-custom-background-100">
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
|
||||
<AdminHeader />
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<div className="h-full w-full overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md">
|
||||
<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-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",
|
||||
"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",
|
||||
{
|
||||
"left-4": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 pb-2">
|
||||
<span className="px-2 text-custom-sidebar-text-200 truncate">{currentUser?.email}</span>
|
||||
<span className="px-2 text-secondary truncate">{currentUser?.email}</span>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
|
||||
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 px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
|
||||
>
|
||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||
Sign out
|
||||
@@ -75,10 +75,10 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||
}, [csrfToken]);
|
||||
|
||||
return (
|
||||
<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="flex max-h-header items-center gap-x-5 gap-y-2 border-b border-subtle px-4 py-3.5">
|
||||
<div className="h-full w-full truncate">
|
||||
<div
|
||||
className={`flex flex-grow items-center gap-x-2 truncate rounded py-1 ${
|
||||
className={`flex flex-grow items-center gap-x-2 truncate rounded-sm py-1 ${
|
||||
isSidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
@@ -88,8 +88,8 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||
"cursor-default": !isSidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<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 className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-sm bg-layer-1">
|
||||
<UserCog2 className="h-5 w-5 text-secondary" />
|
||||
</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-base font-medium text-custom-text-200">Instance admin</h4>
|
||||
<h4 className="grow truncate text-14 font-medium text-secondary">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-base"
|
||||
className="!text-14"
|
||||
/>
|
||||
</Menu.Button>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
// assets
|
||||
// eslint-disable-next-line import/order
|
||||
|
||||
import packageJson from "package.json";
|
||||
|
||||
const helpOptions = [
|
||||
@@ -45,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-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
|
||||
"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-col h-auto py-1.5": isSidebarCollapsed,
|
||||
}
|
||||
@@ -55,7 +55,7 @@ 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.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`}
|
||||
className={`relative px-2 py-1.5 flex items-center gap-2 font-medium rounded-sm border border-accent-strong/20 bg-accent-primary/10 text-11 text-accent-secondary whitespace-nowrap`}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
{!isSidebarCollapsed && "Redirect to Plane"}
|
||||
@@ -64,7 +64,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||
<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-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
className={`ml-auto grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
|
||||
isSidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||
@@ -75,7 +75,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||
<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-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
className={`grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
|
||||
isSidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||
@@ -98,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-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
|
||||
} divide-y divide-subtle-1 whitespace-nowrap rounded-sm bg-surface-1 p-1 shadow-raised-100`}
|
||||
ref={helpOptionsRef}
|
||||
>
|
||||
<div className="space-y-1 pb-2">
|
||||
@@ -106,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 px-2 py-1 text-xs hover:bg-custom-background-80">
|
||||
<div className="flex items-center gap-x-2 rounded-sm px-2 py-1 text-11 hover:bg-layer-1-hover">
|
||||
<div className="grid flex-shrink-0 place-items-center">
|
||||
<Icon className="h-3.5 w-3.5 text-custom-text-200" width={14} height={14} />
|
||||
<Icon className="h-3.5 w-3.5 text-secondary" width={14} height={14} />
|
||||
</div>
|
||||
<span className="text-xs">{name}</span>
|
||||
<span className="text-11">{name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
@@ -119,17 +119,17 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
className="flex w-full items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80"
|
||||
className="flex w-full items-center gap-x-2 rounded-sm px-2 py-1 text-11 hover:bg-layer-1"
|
||||
>
|
||||
<div className="grid flex-shrink-0 place-items-center">
|
||||
<Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<Icon className="h-3.5 w-3.5 text-secondary" />
|
||||
</div>
|
||||
<span className="text-xs">{name}</span>
|
||||
<span className="text-11">{name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="px-2 pb-1 pt-2 text-[10px]">Version: v{packageJson.version}</div>
|
||||
<div className="px-2 pb-1 pt-2 text-10">Version: v{packageJson.version}</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
@@ -72,8 +72,8 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
|
||||
className={cn(
|
||||
`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",
|
||||
? "bg-accent-primary/10 text-accent-primary"
|
||||
: "text-secondary hover:bg-layer-1-hover focus:bg-layer-1-hover",
|
||||
isSidebarCollapsed ? "justify-center" : "w-[260px]"
|
||||
)}
|
||||
>
|
||||
@@ -82,16 +82,16 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
|
||||
<div className="w-full ">
|
||||
<div
|
||||
className={cn(
|
||||
`text-sm font-medium transition-colors`,
|
||||
isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-200"
|
||||
`text-13 font-medium transition-colors`,
|
||||
isActive ? "text-accent-primary" : "text-secondary"
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
`text-[10px] transition-colors`,
|
||||
isActive ? "text-custom-primary-90" : "text-custom-sidebar-text-400"
|
||||
`text-10 transition-colors`,
|
||||
isActive ? "text-accent-secondary" : "text-placeholder"
|
||||
)}
|
||||
>
|
||||
{item.description}
|
||||
|
||||
@@ -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-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300
|
||||
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
|
||||
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-sm text-custom-text-300">Name your workspace</h4>
|
||||
<h4 className="text-13 text-tertiary">Name your workspace</h4>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -118,13 +118,13 @@ export function WorkspaceCreateForm() {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
||||
<span className="text-11 text-red-500">{errors?.name?.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<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>
|
||||
<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>
|
||||
<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-sm"
|
||||
className="block w-full rounded-md border-none bg-transparent !px-0 py-2 text-13"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{slugError && <p className="text-sm text-red-500">This URL is taken. Try something else.</p>}
|
||||
{slugError && <p className="text-13 text-red-500">This URL is taken. Try something else.</p>}
|
||||
{invalidSlug && (
|
||||
<p className="text-sm text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
|
||||
<p className="text-13 text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
|
||||
)}
|
||||
{errors.slug && <span className="text-xs text-red-500">{errors.slug.message}</span>}
|
||||
{errors.slug && <span className="text-11 text-red-500">{errors.slug.message}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">How many people will use this workspace?</h4>
|
||||
<h4 className="text-13 text-tertiary">How many people will use this workspace?</h4>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
name="organization_size"
|
||||
@@ -172,12 +172,11 @@ export function WorkspaceCreateForm() {
|
||||
onChange={onChange}
|
||||
label={
|
||||
ORGANIZATION_SIZE.find((c) => c === value) ?? (
|
||||
<span className="text-custom-text-400">Select a range</span>
|
||||
<span className="text-placeholder">Select a range</span>
|
||||
)
|
||||
}
|
||||
buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none"
|
||||
buttonClassName="!border-[0.5px] !border-subtle !shadow-none"
|
||||
input
|
||||
optionsClassName="w-full"
|
||||
>
|
||||
{ORGANIZATION_SIZE.map((item) => (
|
||||
<CustomSelect.Option key={item} value={item}>
|
||||
@@ -188,7 +187,7 @@ export function WorkspaceCreateForm() {
|
||||
)}
|
||||
/>
|
||||
{errors.organization_size && (
|
||||
<span className="text-sm text-red-500">{errors.organization_size.message}</span>
|
||||
<span className="text-13 text-red-500">{errors.organization_size.message}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,14 +195,14 @@ export function WorkspaceCreateForm() {
|
||||
<div className="flex max-w-4xl items-center py-1 gap-4">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
size="lg"
|
||||
onClick={handleSubmit(handleCreateWorkspace)}
|
||||
disabled={!isValid}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Creating workspace" : "Create workspace"}
|
||||
</Button>
|
||||
<Link className={getButtonStyling("neutral-primary", "sm")} href="/workspace">
|
||||
<Link className={getButtonStyling("secondary", "lg")} href="/workspace">
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,9 @@ import { WorkspaceCreateForm } from "./form";
|
||||
const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.ComponentProps) {
|
||||
return (
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Create a new workspace on this instance.</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-18 font-medium text-primary">Create a new workspace on this instance.</div>
|
||||
<div className="text-13 font-regular text-tertiary">
|
||||
You will need to invite users from Workspace Settings after you create this workspace.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,22 +69,20 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
|
||||
|
||||
return (
|
||||
<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 items-center justify-between gap-4 border-b border-subtle 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>
|
||||
<div className="text-18 font-medium text-primary">Workspaces on this instance</div>
|
||||
<div className="text-13 font-regular text-tertiary">See all workspaces and control who can create them.</div>
|
||||
</div>
|
||||
</div>
|
||||
<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={cn("w-full flex items-center gap-14 rounded-sm")}>
|
||||
<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")}>
|
||||
<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>
|
||||
@@ -116,20 +114,19 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
|
||||
<>
|
||||
<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>
|
||||
<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-normal leading-5 text-custom-text-300 text-xs")}>
|
||||
<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", "sm")}>
|
||||
<Link href="/workspace/create" className={getButtonStyling("primary", "lg")}>
|
||||
Create workspace
|
||||
</Link>
|
||||
</div>
|
||||
@@ -142,7 +139,8 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
|
||||
{hasNextPage && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="link-primary"
|
||||
variant="link"
|
||||
size="lg"
|
||||
onClick={() => fetchNextWorkspaces()}
|
||||
disabled={workspaceLoader === "pagination"}
|
||||
>
|
||||
|
||||
@@ -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-custom-primary-100/50 bg-custom-primary-100/10">
|
||||
<div className="relative flex items-center p-2 rounded-md gap-2 border border-accent-strong/50 bg-accent-primary/10">
|
||||
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
|
||||
<Info size={16} className="text-custom-primary-100" />
|
||||
<Info size={16} className="text-accent-primary" />
|
||||
</div>
|
||||
<div className="w-full text-sm font-medium text-custom-primary-100">{bannerData?.message}</div>
|
||||
<div className="w-full text-13 font-medium text-accent-primary">{bannerData?.message}</div>
|
||||
<div
|
||||
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"
|
||||
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/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-custom-text-100" />
|
||||
<PlaneLockup height={20} width={95} className="text-primary" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ export enum EErrorAlertType {
|
||||
}
|
||||
|
||||
const errorCodeMessages: {
|
||||
[key in EAdminAuthErrorCodes]: { title: string; message: (email?: string | undefined) => React.ReactNode };
|
||||
[key in EAdminAuthErrorCodes]: { title: string; message: (email?: string) => React.ReactNode };
|
||||
} = {
|
||||
// admin
|
||||
[EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST]: {
|
||||
@@ -79,14 +79,11 @@ 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 | undefined
|
||||
): TAdminAuthErrorInfo | undefined => {
|
||||
export const authErrorHandler = (errorCode: EAdminAuthErrorCodes, email?: string): TAdminAuthErrorInfo | undefined => {
|
||||
const bannerAlertErrorCodes = [
|
||||
EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST,
|
||||
EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
|
||||
@@ -120,14 +117,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-custom-text-300/80" />,
|
||||
icon: <Mails className="h-6 w-6 p-0.5 text-tertiary/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-custom-text-300/80" />,
|
||||
icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary/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">
|
||||
<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">
|
||||
<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-sm text-custom-text-300 font-medium" htmlFor="email">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="email">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
@@ -146,12 +146,12 @@ export function InstanceSignInForm() {
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="password">
|
||||
Password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
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-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
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-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
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="lg" className="w-full" disabled={isButtonDisabled}>
|
||||
<Button type="submit" size="xl" 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-custom-background-100`}>
|
||||
<div className={`h-screen w-full overflow-hidden bg-surface-1`}>
|
||||
<div className="grid h-full place-items-center p-4">
|
||||
<div className="space-y-8 text-center">
|
||||
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
|
||||
<img src={Image404} alt="404 - Page not found" className="h-full w-full object-contain" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
<h3 className="text-16 font-semibold">Oops! Something went wrong.</h3>
|
||||
<p className="text-13 text-secondary">
|
||||
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
|
||||
temporarily unavailable.
|
||||
</p>
|
||||
</div>
|
||||
<Link to="/general/">
|
||||
<span className="flex justify-center py-4">
|
||||
<Button variant="neutral-primary" size="md">
|
||||
<Button variant="secondary" size="lg">
|
||||
Go to general settings
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/order */
|
||||
import * as Sentry from "@sentry/react-router";
|
||||
import { startTransition, StrictMode } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
|
||||
@@ -56,7 +56,11 @@ export const meta: Route.MetaFunction = () => [
|
||||
];
|
||||
|
||||
export default function Root() {
|
||||
return <Outlet />;
|
||||
return (
|
||||
<div className="bg-canvas min-h-screen">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HydrateFallback() {
|
||||
|
||||
@@ -43,14 +43,14 @@ export const getAuthenticationModes: (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-custom-text-300/80" />,
|
||||
icon: <Mails className="h-6 w-6 p-0.5 text-tertiary/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-custom-text-300/80" />,
|
||||
icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary/80" />,
|
||||
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
@@ -112,7 +112,7 @@ export const AuthenticationModes = observer(function AuthenticationModes(props:
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
{getAuthenticationModes({ disabled, updateConfig, resolvedTheme }).map((method) => (
|
||||
<AuthenticationMethodCard
|
||||
key={method.key}
|
||||
@@ -124,6 +124,6 @@ export const AuthenticationModes = observer(function AuthenticationModes(props:
|
||||
unavailable={method.unavailable}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,7 +10,8 @@ export function UpgradeButton() {
|
||||
<a
|
||||
href="https://plane.so/pricing?mode=self-hosted"
|
||||
target="_blank"
|
||||
className={cn(getButtonStyling("primary", "sm"))}
|
||||
className={cn(getButtonStyling("primary", "base"))}
|
||||
rel="noreferrer"
|
||||
>
|
||||
Upgrade
|
||||
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
|
||||
|
||||
@@ -16,8 +16,8 @@ export function AuthenticationMethodCard(props: Props) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("w-full flex items-center gap-14 rounded", {
|
||||
"px-4 py-3 border border-custom-border-200": withBorder,
|
||||
className={cn("w-full flex items-center gap-14 rounded-sm", {
|
||||
"px-4 py-3 border border-subtle": 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-custom-background-80">{icon}</div>
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-layer-1">{icon}</div>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div
|
||||
className={cn("font-medium leading-5 text-custom-text-100", {
|
||||
"text-sm": withBorder,
|
||||
"text-xl": !withBorder,
|
||||
className={cn("font-medium leading-5 text-primary", {
|
||||
"text-13": withBorder,
|
||||
"text-18": !withBorder,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div
|
||||
className={cn("font-normal leading-5 text-custom-text-300", {
|
||||
"text-xs": withBorder,
|
||||
"text-sm": !withBorder,
|
||||
className={cn("font-regular leading-5 text-tertiary", {
|
||||
"text-11": withBorder,
|
||||
"text-13": !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, getButtonStyling } from "@plane/ui";
|
||||
import { ToggleSwitch } 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-primary", "md"), "font-medium")}>
|
||||
<Link href="/authentication/gitea" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
@@ -43,11 +43,8 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/authentication/gitea"
|
||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
||||
>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
||||
<Link href="/authentication/gitea" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-tertiary/80" />
|
||||
Configure
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -28,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-primary", "md"), "font-medium")}>
|
||||
<Link href="/authentication/github" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
@@ -42,11 +42,8 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<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" />
|
||||
<Link href="/authentication/github" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-tertiary/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-primary", "md"), "font-medium")}>
|
||||
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
@@ -41,11 +41,8 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<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" />
|
||||
<Link href="/authentication/gitlab" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-tertiary/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-primary", "md"), "font-medium")}>
|
||||
<Link href="/authentication/google" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
@@ -41,11 +41,8 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<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" />
|
||||
<Link href="/authentication/google" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-tertiary/80" />
|
||||
Configure
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function Banner(props: TBanner) {
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-1">
|
||||
<p className={`text-sm font-medium ${type === "error" ? "text-red-600" : "text-green-600"}`}>{message}</p>
|
||||
<p className={`text-13 font-medium ${type === "error" ? "text-red-600" : "text-green-600"}`}>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,17 +14,12 @@ 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-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>
|
||||
)}
|
||||
<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>}
|
||||
<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-sm font-medium text-custom-text-100">
|
||||
<div className="flex cursor-default items-center gap-1 text-13 font-medium text-primary">
|
||||
{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-xs text-custom-text-300 bg-custom-background-90 font-semibold rounded-md border border-custom-border-100",
|
||||
"px-0.5 text-11 text-tertiary bg-surface-2 font-semibold rounded-md border border-subtle",
|
||||
{
|
||||
"text-custom-text-200 bg-custom-background-80 border-custom-border-200": darkerShade,
|
||||
"text-secondary bg-layer-1 border-subtle": darkerShade,
|
||||
},
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function ConfirmDiscardModal(props: Props) {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
<div className="fixed inset-0 bg-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-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[30rem]">
|
||||
<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]">
|
||||
<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-lg font-medium leading-6 text-custom-text-300">
|
||||
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-tertiary">
|
||||
You have unsaved changes
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-400">
|
||||
<p className="text-13 text-placeholder">
|
||||
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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
<Button variant="secondary" size="lg" onClick={handleClose}>
|
||||
Keep editing
|
||||
</Button>
|
||||
<Link href={onDiscardHref} className={getButtonStyling("primary", "sm")}>
|
||||
<Link href={onDiscardHref} className={getButtonStyling("primary", "base")}>
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function ControllerInput(props: Props) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">{label}</h4>
|
||||
<h4 className="text-13 text-tertiary">{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-custom-text-400"
|
||||
className="absolute right-3 top-2.5 flex items-center justify-center text-placeholder"
|
||||
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-custom-text-400"
|
||||
className="absolute right-3 top-2.5 flex items-center justify-center text-placeholder"
|
||||
onClick={() => setShowPassword(true)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{description && <p className="pt-0.5 text-xs text-custom-text-300">{description}</p>}
|
||||
{description && <p className="pt-0.5 text-11 text-tertiary">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,10 @@ export function CopyField(props: Props) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-200">{label}</h4>
|
||||
<h4 className="text-13 text-secondary">{label}</h4>
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="flex items-center justify-between py-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(url);
|
||||
@@ -35,10 +36,10 @@ export function CopyField(props: Props) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<p className="text-sm font-medium">{url}</p>
|
||||
<p className="text-13 font-medium">{url}</p>
|
||||
<Copy size={18} color="#B9B9B9" />
|
||||
</Button>
|
||||
<div className="text-xs text-custom-text-300">{description}</div>
|
||||
<div className="text-11 text-tertiary">{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-xl font-semibold sm:mt-8">{title}</h6>
|
||||
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
|
||||
<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>}
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
@@ -28,6 +28,7 @@ export function EmptyState({ title, description, image, primaryButton, secondary
|
||||
prependIcon={primaryButton.icon}
|
||||
onClick={primaryButton.onClick}
|
||||
disabled={disabled}
|
||||
size="lg"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</Button>
|
||||
|
||||
@@ -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-2xl text-white text-center">Unable to fetch instance details.</h3>
|
||||
<p className="font-medium text-base text-center">
|
||||
<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">
|
||||
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="md" onClick={handleRetry}>
|
||||
<Button size="lg" 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-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>
|
||||
<span className="text-20 font-semibold text-primary leading-7">{heading}</span>
|
||||
<span className="text-16 font-semibold text-placeholder leading-7">{subHeading}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,16 +8,14 @@ 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-3xl font-bold pb-3">Welcome aboard Plane!</h1>
|
||||
<h1 className="text-24 font-bold pb-3">Welcome aboard Plane!</h1>
|
||||
<img src={PlaneTakeOffImage} alt="Plane Logo" />
|
||||
<p className="font-medium text-base text-custom-text-400">
|
||||
Get started by setting up your instance and workspace
|
||||
</p>
|
||||
<p className="font-medium text-14 text-placeholder">Get started by setting up your instance and workspace</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Link href={"/setup/?auth_enabled=0"}>
|
||||
<Button size="lg" className="w-full">
|
||||
<Button size="xl" className="w-full">
|
||||
Get started
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -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-sm text-custom-text-300 font-medium" htmlFor="first_name">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="first_name">
|
||||
First name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
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-sm text-custom-text-300 font-medium" htmlFor="last_name">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="last_name">
|
||||
Last name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
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-sm text-custom-text-300 font-medium" htmlFor="email">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="email">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
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-xs text-red-500">{errorData.message}</p>
|
||||
<p className="px-1 text-11 text-red-500">{errorData.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="company_name">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="company_name">
|
||||
Company name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
id="company_name"
|
||||
name="company_name"
|
||||
type="text"
|
||||
@@ -228,12 +228,12 @@ export function InstanceSetupForm() {
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="password">
|
||||
Set a password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword.password ? "text" : "password"}
|
||||
@@ -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-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
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-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
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-xs text-red-500">{errorData.message}</p>
|
||||
<p className="px-1 text-11 text-red-500">{errorData.message}</p>
|
||||
)}
|
||||
<PasswordStrengthIndicator password={formData.password} isFocused={isPasswordInputFocused} />
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="confirm_password">
|
||||
<label className="text-13 text-tertiary 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-custom-border-100 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 pr-12 placeholder:text-placeholder"
|
||||
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-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
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-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
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-sm text-red-500">Passwords don{"'"}t match</span>}
|
||||
renderPasswordMatchError && <span className="text-13 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-sm text-custom-text-300 font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
|
||||
<label className="text-13 text-tertiary 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-sm font-medium text-blue-500 hover:text-blue-600 flex-shrink-0"
|
||||
className="text-13 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="lg" className="w-full" disabled={isButtonDisabled}>
|
||||
<Button type="submit" size="xl" 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-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
|
||||
<div className="absolute bottom-8 right-8 p-6 w-96 border border-subtle shadow-md rounded-lg bg-surface-1">
|
||||
<div className="flex gap-4">
|
||||
<div className="grow">
|
||||
<div className="text-base font-semibold">Create workspace</div>
|
||||
<div className="py-2 text-sm font-medium text-custom-text-300">
|
||||
<div className="text-14 font-semibold">Create workspace</div>
|
||||
<div className="py-2 text-13 font-medium text-tertiary">
|
||||
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", "sm")}>
|
||||
<Link href="/workspace/create" className={getButtonStyling("primary", "lg")}>
|
||||
Create workspace
|
||||
</Link>
|
||||
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
|
||||
<Button variant="secondary" size="lg" onClick={toggleNewUserPopup}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -23,18 +23,19 @@ 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-4 gap-2.5 truncate border border-custom-border-200/70 hover:border-custom-border-200 hover:bg-custom-background-90 rounded-md"
|
||||
className="group flex items-center justify-between p-4 gap-2.5 truncate border border-subtle/70 hover:border-subtle bg-layer-1 hover:bg-layer-1-hover rounded-md"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<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-xs uppercase ${
|
||||
!workspace?.logo_url && "rounded bg-custom-primary-500 text-white"
|
||||
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-sm bg-accent-primary text-on-color"
|
||||
}`}
|
||||
>
|
||||
{workspace?.logo_url && workspace.logo_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(workspace.logo_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded object-cover"
|
||||
className="absolute left-0 top-0 h-full w-full rounded-sm object-cover"
|
||||
alt="Workspace Logo"
|
||||
/>
|
||||
) : (
|
||||
@@ -43,30 +44,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-base font-medium capitalize`}>{workspace.name}</h3>/
|
||||
<h3 className={`text-14 font-medium capitalize`}>{workspace.name}</h3>/
|
||||
<Tooltip tooltipContent="The unique URL of your workspace">
|
||||
<h4 className="text-sm text-custom-text-300">[{workspace.slug}]</h4>
|
||||
<h4 className="text-13 text-tertiary">[{workspace.slug}]</h4>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{workspace.owner.email && (
|
||||
<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 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>
|
||||
)}
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<div className="flex items-center gap-2.5 text-11">
|
||||
{workspace.total_projects !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<h3 className="text-custom-text-200 font-medium">Total projects:</h3>
|
||||
<h4 className="text-custom-text-300">{workspace.total_projects}</h4>
|
||||
<h3 className="text-secondary font-medium">Total projects:</h3>
|
||||
<h4 className="text-tertiary">{workspace.total_projects}</h4>
|
||||
</span>
|
||||
)}
|
||||
{workspace.total_members !== null && (
|
||||
<>
|
||||
•
|
||||
<span className="flex items-center gap-1">
|
||||
<h3 className="text-custom-text-200 font-medium">Total members:</h3>
|
||||
<h4 className="text-custom-text-300">{workspace.total_members}</h4>
|
||||
<h3 className="text-secondary font-medium">Total members:</h3>
|
||||
<h4 className="text-tertiary">{workspace.total_members}</h4>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@@ -74,7 +75,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<ExternalLink size={14} className="text-custom-text-400 group-hover:text-custom-text-200" />
|
||||
<ExternalLink size={14} className="text-placeholder group-hover:text-secondary" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Admin UI for Plane",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -11,11 +11,11 @@
|
||||
"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 . --max-warnings 19",
|
||||
"check:lint": "eslint . --cache --cache-location node_modules/.cache/eslint/ --max-warnings=485",
|
||||
"check:types": "react-router typegen && tsc --noEmit",
|
||||
"check:format": "prettier --check \"**/*.{ts,tsx,md,json,css,scss}\"",
|
||||
"fix:lint": "eslint . --fix",
|
||||
"fix:format": "prettier --write \"**/*.{ts,tsx,md,json,css,scss}\""
|
||||
"check:format": "prettier . --cache --check",
|
||||
"fix:lint": "eslint . --cache --cache-location node_modules/.cache/eslint/ --fix --max-warnings=485",
|
||||
"fix:format": "prettier . --cache --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bprogress/core": "catalog:",
|
||||
@@ -37,7 +37,7 @@
|
||||
"lucide-react": "catalog:",
|
||||
"mobx": "catalog:",
|
||||
"mobx-react": "catalog:",
|
||||
"next-themes": "^0.2.1",
|
||||
"next-themes": "0.4.6",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-hook-form": "7.51.5",
|
||||
@@ -48,16 +48,14 @@
|
||||
"uuid": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@dotenvx/dotenvx": "catalog:",
|
||||
"@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"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
module.exports = require("@plane/tailwind-config/postcss.config.js");
|
||||
@@ -0,0 +1,3 @@
|
||||
import postcssConfig from "@plane/tailwind-config/postcss.config.js";
|
||||
|
||||
export default postcssConfig;
|
||||
+12
-393
@@ -1,403 +1,22 @@
|
||||
@import "@plane/propel/styles/fonts.css";
|
||||
@import "@plane/tailwind-config/index.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.text-1\.5xl {
|
||||
font-size: 1.375rem;
|
||||
line-height: 1.875rem;
|
||||
}
|
||||
|
||||
.text-2\.5xl {
|
||||
font-size: 1.75rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
.shadow-custom {
|
||||
box-shadow: 2px 2px 8px 2px rgba(234, 231, 250, 0.3); /* Convert #EAE7FA4D to rgba */
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light !important;
|
||||
|
||||
--color-primary-10: 229, 243, 250;
|
||||
--color-primary-20: 216, 237, 248;
|
||||
--color-primary-30: 199, 229, 244;
|
||||
--color-primary-40: 169, 214, 239;
|
||||
--color-primary-50: 144, 202, 234;
|
||||
--color-primary-60: 109, 186, 227;
|
||||
--color-primary-70: 75, 170, 221;
|
||||
--color-primary-80: 41, 154, 214;
|
||||
--color-primary-90: 34, 129, 180;
|
||||
--color-primary-100: 0, 99, 153;
|
||||
--color-primary-200: 0, 92, 143;
|
||||
--color-primary-300: 0, 86, 133;
|
||||
--color-primary-400: 0, 77, 117;
|
||||
--color-primary-500: 0, 66, 102;
|
||||
--color-primary-600: 0, 53, 82;
|
||||
--color-primary-700: 0, 43, 66;
|
||||
--color-primary-800: 0, 33, 51;
|
||||
--color-primary-900: 0, 23, 36;
|
||||
|
||||
--color-background-100: 255, 255, 255; /* primary bg */
|
||||
--color-background-90: 247, 247, 247; /* secondary bg */
|
||||
--color-background-80: 232, 232, 232; /* tertiary bg */
|
||||
|
||||
--color-text-100: 23, 23, 23; /* primary text */
|
||||
--color-text-200: 58, 58, 58; /* secondary text */
|
||||
--color-text-300: 82, 82, 82; /* tertiary text */
|
||||
--color-text-400: 163, 163, 163; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 163, 163, 163; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
|
||||
--color-shadow-2xs:
|
||||
0px 0px 1px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.06),
|
||||
0px 1px 2px 0px rgba(23, 23, 23, 0.14);
|
||||
--color-shadow-xs:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 8px -1px rgba(16, 24, 40, 0.1);
|
||||
--color-shadow-sm:
|
||||
0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02), 0px 1px 12px 0px rgba(0, 0, 0, 0.12);
|
||||
--color-shadow-rg:
|
||||
0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08),
|
||||
0px 1px 12px 0px rgba(16, 24, 40, 0.04);
|
||||
--color-shadow-md:
|
||||
0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 16px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-lg:
|
||||
0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 1px 24px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-xl:
|
||||
0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16),
|
||||
0px 0px 52px 0px rgba(16, 24, 40, 0.16);
|
||||
--color-shadow-2xl:
|
||||
0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 12px 24px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 32px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-3xl:
|
||||
0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 1px 48px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-4xl: 0px 8px 40px 0px rgba(0, 0, 61, 0.05), 0px 12px 32px -16px rgba(0, 0, 0, 0.05);
|
||||
|
||||
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
|
||||
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
|
||||
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
|
||||
|
||||
--color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
|
||||
--color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
|
||||
--color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
|
||||
--color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
|
||||
|
||||
--color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
|
||||
--color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
|
||||
--color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
|
||||
--color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
|
||||
|
||||
--color-sidebar-shadow-2xs: var(--color-shadow-2xs);
|
||||
--color-sidebar-shadow-xs: var(--color-shadow-xs);
|
||||
--color-sidebar-shadow-sm: var(--color-shadow-sm);
|
||||
--color-sidebar-shadow-rg: var(--color-shadow-rg);
|
||||
--color-sidebar-shadow-md: var(--color-shadow-md);
|
||||
--color-sidebar-shadow-lg: var(--color-shadow-lg);
|
||||
--color-sidebar-shadow-xl: var(--color-shadow-xl);
|
||||
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
|
||||
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
|
||||
--color-sidebar-shadow-4xl: var(--color-shadow-4xl);
|
||||
|
||||
/* toast theme */
|
||||
--color-toast-success-text: 178, 221, 181;
|
||||
--color-toast-error-text: 206, 44, 49;
|
||||
--color-toast-warning-text: 255, 186, 24;
|
||||
--color-toast-info-text: 141, 164, 239;
|
||||
--color-toast-loading-text: 255, 255, 255;
|
||||
--color-toast-secondary-text: 185, 187, 198;
|
||||
--color-toast-tertiary-text: 139, 141, 152;
|
||||
|
||||
--color-toast-success-background: 46, 46, 46;
|
||||
--color-toast-error-background: 46, 46, 46;
|
||||
--color-toast-warning-background: 46, 46, 46;
|
||||
--color-toast-info-background: 46, 46, 46;
|
||||
--color-toast-loading-background: 46, 46, 46;
|
||||
|
||||
--color-toast-success-border: 42, 126, 59;
|
||||
--color-toast-error-border: 100, 23, 35;
|
||||
--color-toast-warning-border: 79, 52, 34;
|
||||
--color-toast-info-border: 58, 91, 199;
|
||||
--color-toast-loading-border: 96, 100, 108;
|
||||
}
|
||||
|
||||
[data-theme="light"],
|
||||
[data-theme="light-contrast"] {
|
||||
color-scheme: light !important;
|
||||
|
||||
--color-background-100: 255, 255, 255; /* primary bg */
|
||||
--color-background-90: 247, 247, 247; /* secondary bg */
|
||||
--color-background-80: 232, 232, 232; /* tertiary bg */
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--color-text-100: 23, 23, 23; /* primary text */
|
||||
--color-text-200: 58, 58, 58; /* secondary text */
|
||||
--color-text-300: 82, 82, 82; /* tertiary text */
|
||||
--color-text-400: 163, 163, 163; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 163, 163, 163; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
|
||||
/* toast theme */
|
||||
--color-toast-success-text: 62, 155, 79;
|
||||
--color-toast-error-text: 220, 62, 66;
|
||||
--color-toast-warning-text: 255, 186, 24;
|
||||
--color-toast-info-text: 51, 88, 212;
|
||||
--color-toast-loading-text: 28, 32, 36;
|
||||
--color-toast-secondary-text: 128, 131, 141;
|
||||
--color-toast-tertiary-text: 96, 100, 108;
|
||||
|
||||
--color-toast-success-background: 253, 253, 254;
|
||||
--color-toast-error-background: 255, 252, 252;
|
||||
--color-toast-warning-background: 254, 253, 251;
|
||||
--color-toast-info-background: 253, 253, 254;
|
||||
--color-toast-loading-background: 253, 253, 254;
|
||||
|
||||
--color-toast-success-border: 218, 241, 219;
|
||||
--color-toast-error-border: 255, 219, 220;
|
||||
--color-toast-warning-border: 255, 247, 194;
|
||||
--color-toast-info-border: 210, 222, 255;
|
||||
--color-toast-loading-border: 224, 225, 230;
|
||||
}
|
||||
|
||||
[data-theme="light-contrast"] {
|
||||
--color-text-100: 11, 11, 11; /* primary text */
|
||||
--color-text-200: 38, 38, 38; /* secondary text */
|
||||
--color-text-300: 58, 58, 58; /* tertiary text */
|
||||
--color-text-400: 115, 115, 115; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 115, 115, 115; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 34, 34, 34; /* subtle border= 1 */
|
||||
--color-border-200: 38, 38, 38; /* subtle border- 2 */
|
||||
--color-border-300: 46, 46, 46; /* strong border- 1 */
|
||||
--color-border-400: 58, 58, 58; /* strong border- 2 */
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
[data-theme="dark-contrast"] {
|
||||
color-scheme: dark !important;
|
||||
|
||||
--color-primary-10: 8, 31, 43;
|
||||
--color-primary-20: 10, 37, 51;
|
||||
--color-primary-30: 13, 49, 69;
|
||||
--color-primary-40: 16, 58, 81;
|
||||
--color-primary-50: 18, 68, 94;
|
||||
--color-primary-60: 23, 86, 120;
|
||||
--color-primary-70: 28, 104, 146;
|
||||
--color-primary-80: 31, 116, 163;
|
||||
--color-primary-90: 34, 129, 180;
|
||||
--color-primary-100: 40, 146, 204;
|
||||
--color-primary-200: 41, 154, 214;
|
||||
--color-primary-300: 75, 170, 221;
|
||||
--color-primary-400: 109, 186, 227;
|
||||
--color-primary-500: 144, 202, 234;
|
||||
--color-primary-600: 169, 214, 239;
|
||||
--color-primary-700: 199, 229, 244;
|
||||
--color-primary-800: 216, 237, 248;
|
||||
--color-primary-900: 229, 243, 250;
|
||||
|
||||
--color-background-100: 25, 25, 25; /* primary bg */
|
||||
--color-background-90: 32, 32, 32; /* secondary bg */
|
||||
--color-background-80: 44, 44, 44; /* tertiary bg */
|
||||
|
||||
--color-shadow-2xs: 0px 0px 1px 0px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-xs: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-sm: 0px 0px 4px 0px rgba(0, 0, 0, 0.2), 0px 2px 6px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-rg: 0px 0px 6px 0px rgba(0, 0, 0, 0.2), 0px 4px 6px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-md: 0px 2px 8px 0px rgba(0, 0, 0, 0.2), 0px 4px 8px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-lg: 0px 4px 12px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.55);
|
||||
--color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
|
||||
--color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
|
||||
--color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--color-text-100: 229, 229, 229; /* primary text */
|
||||
--color-text-200: 163, 163, 163; /* secondary text */
|
||||
--color-text-300: 115, 115, 115; /* tertiary text */
|
||||
--color-text-400: 82, 82, 82; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 82, 82, 82; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 34, 34, 34; /* subtle border= 1 */
|
||||
--color-border-200: 38, 38, 38; /* subtle border- 2 */
|
||||
--color-border-300: 46, 46, 46; /* strong border- 1 */
|
||||
--color-border-400: 58, 58, 58; /* strong border- 2 */
|
||||
}
|
||||
|
||||
[data-theme="dark-contrast"] {
|
||||
--color-text-100: 250, 250, 250; /* primary text */
|
||||
--color-text-200: 241, 241, 241; /* secondary text */
|
||||
--color-text-300: 212, 212, 212; /* tertiary text */
|
||||
--color-text-400: 115, 115, 115; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 115, 115, 115; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
}
|
||||
|
||||
[data-theme="light"],
|
||||
[data-theme="dark"],
|
||||
[data-theme="light-contrast"],
|
||||
[data-theme="dark-contrast"] {
|
||||
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
|
||||
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
|
||||
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
|
||||
|
||||
--color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
|
||||
--color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
|
||||
--color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
|
||||
--color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
|
||||
|
||||
--color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
|
||||
--color-sidebar-border-200: var(--color-border-200); /* subtle sidebar border- 2 */
|
||||
--color-sidebar-border-300: var(--color-border-300); /* strong sidebar border- 1 */
|
||||
--color-sidebar-border-400: var(--color-border-400); /* strong sidebar border- 2 */
|
||||
}
|
||||
/* backdrop filter */
|
||||
.backdrop-blur-custom {
|
||||
@apply backdrop-filter blur-[9px];
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
font-variant-ligatures: none;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgba(var(--color-text-100));
|
||||
}
|
||||
|
||||
/* scrollbar style */
|
||||
@-moz-document url-prefix() {
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.vertical-scrollbar,
|
||||
.horizontal-scrollbar {
|
||||
scrollbar-width: initial;
|
||||
scrollbar-color: rgba(96, 100, 108, 0.1) transparent;
|
||||
}
|
||||
.vertical-scrollbar:hover,
|
||||
.horizontal-scrollbar:hover {
|
||||
scrollbar-color: rgba(96, 100, 108, 0.25) transparent;
|
||||
}
|
||||
.vertical-scrollbar:active,
|
||||
.horizontal-scrollbar:active {
|
||||
scrollbar-color: rgba(96, 100, 108, 0.7) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-scrollbar {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.horizontal-scrollbar {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar,
|
||||
.horizontal-scrollbar::-webkit-scrollbar {
|
||||
display: block;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-track,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-clip: padding-box;
|
||||
background-color: rgba(96, 100, 108, 0.1);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.vertical-scrollbar:hover::-webkit-scrollbar-thumb,
|
||||
.horizontal-scrollbar:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(96, 100, 108, 0.25);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb:hover,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(96, 100, 108, 0.5);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb:active,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb:active {
|
||||
background-color: rgba(96, 100, 108, 0.7);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-corner,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
.vertical-scrollbar-margin-top-md::-webkit-scrollbar-track {
|
||||
margin-top: 44px;
|
||||
}
|
||||
|
||||
/* scrollbar sm size */
|
||||
.scrollbar-sm::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
.scrollbar-sm::-webkit-scrollbar-thumb {
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
/* scrollbar md size */
|
||||
.scrollbar-md::-webkit-scrollbar {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
.scrollbar-md::-webkit-scrollbar-thumb {
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
/* scrollbar lg size */
|
||||
|
||||
.scrollbar-lg::-webkit-scrollbar {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
.scrollbar-lg::-webkit-scrollbar-thumb {
|
||||
border: 4px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
/* end scrollbar style */
|
||||
|
||||
/* progress bar */
|
||||
.progress-bar {
|
||||
fill: currentColor;
|
||||
color: rgba(var(--color-sidebar-background-100));
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder,
|
||||
::placeholder,
|
||||
:-ms-input-placeholder {
|
||||
color: rgb(var(--color-text-400));
|
||||
color: var(--background-color-surface-1);
|
||||
}
|
||||
|
||||
/* Progress Bar Styles */
|
||||
:root {
|
||||
--bprogress-color: rgb(var(--color-primary-100)) !important;
|
||||
--bprogress-color: var(--background-color-accent-primary);
|
||||
--bprogress-height: 2.5px !important;
|
||||
}
|
||||
|
||||
@@ -408,8 +27,8 @@ body {
|
||||
.bprogress .bar {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(var(--color-primary-100), 0.8) 0%,
|
||||
rgba(var(--color-primary-100), 1) 100%
|
||||
--alpha(var(--background-color-accent-primary) / 80%) 0%,
|
||||
--alpha(var(--background-color-accent-primary) / 100%) 100%
|
||||
) !important;
|
||||
will-change: width, opacity;
|
||||
}
|
||||
@@ -417,7 +36,7 @@ body {
|
||||
.bprogress .peg {
|
||||
display: block;
|
||||
box-shadow:
|
||||
0 0 8px rgba(var(--color-primary-100), 0.6),
|
||||
0 0 4px rgba(var(--color-primary-100), 0.4) !important;
|
||||
0 0 8px --alpha(var(--background-color-accent-primary) / 60%),
|
||||
0 0 4px --alpha(var(--background-color-accent-primary) / 40%) !important;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const sharedConfig = require("@plane/tailwind-config/tailwind.config.js");
|
||||
|
||||
module.exports = {
|
||||
presets: [sharedConfig],
|
||||
};
|
||||
@@ -1,16 +1,21 @@
|
||||
{
|
||||
"extends": "@plane/typescript-config/react-router.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"noImplicitOverride": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": false,
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"types": ["node", "vite/client"],
|
||||
"types": ["vite/client"],
|
||||
"paths": {
|
||||
"@/app/*": ["app/*"],
|
||||
"@/*": ["core/*"],
|
||||
"@/plane-admin/*": ["ce/*"],
|
||||
"@/styles/*": ["styles/*"]
|
||||
},
|
||||
"strictNullChecks": true
|
||||
"package.json": ["./package.json"],
|
||||
"ce/*": ["./ce/*"],
|
||||
"@/app/*": ["./app/*"],
|
||||
"@/*": ["./core/*"],
|
||||
"@/plane-admin/*": ["./ce/*"],
|
||||
"@/ce/*": ["./ce/*"],
|
||||
"@/styles/*": ["./styles/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import * as dotenv from "@dotenvx/dotenvx";
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import dotenv from "dotenv";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { joinUrlPath } from "@plane/utils";
|
||||
|
||||
@@ -32,6 +32,9 @@ AWS_S3_ENDPOINT_URL="http://localhost:9000"
|
||||
AWS_S3_BUCKET_NAME="uploads"
|
||||
# Maximum file upload limit
|
||||
FILE_SIZE_LIMIT=5242880
|
||||
# Signed URL expiration time in seconds (default: 3600 = 1 hour)
|
||||
# Set to 30 for 30 seconds, 300 for 5 minutes, etc.
|
||||
SIGNED_URL_EXPIRATION=3600
|
||||
|
||||
# Settings related to Docker
|
||||
DOCKERIZED=1 # deprecated
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.next/
|
||||
.react-router/
|
||||
.turbo/
|
||||
.vite/
|
||||
build/
|
||||
dist/
|
||||
node_modules/
|
||||
out/
|
||||
pnpm-lock.yaml
|
||||
storybook-static/
|
||||
@@ -32,4 +32,7 @@ python manage.py create_bucket
|
||||
# Clear Cache before starting to remove stale values
|
||||
python manage.py clear_cache
|
||||
|
||||
# Collect static files
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
exec gunicorn -w "$GUNICORN_WORKERS" -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:"${PORT:-8000}" --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"description": "API server powering Plane's backend"
|
||||
|
||||
@@ -54,4 +54,5 @@ from .asset import (
|
||||
FileAssetSerializer,
|
||||
)
|
||||
from .invite import WorkspaceInviteSerializer
|
||||
from .member import ProjectMemberSerializer
|
||||
from .member import ProjectMemberSerializer
|
||||
from .sticky import StickySerializer
|
||||
|
||||
@@ -4,7 +4,7 @@ from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import Cycle, CycleIssue, User
|
||||
from plane.db.models import Cycle, CycleIssue, User, Project
|
||||
from plane.utils.timezone_converter import convert_to_utc
|
||||
|
||||
|
||||
@@ -55,6 +55,18 @@ class CycleCreateSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
project_id = self.initial_data.get("project_id") or (
|
||||
self.instance.project_id if self.instance and hasattr(self.instance, "project_id") else None
|
||||
)
|
||||
|
||||
if not project_id:
|
||||
raise serializers.ValidationError("Project ID is required")
|
||||
|
||||
project = Project.objects.filter(id=project_id).first()
|
||||
if not project:
|
||||
raise serializers.ValidationError("Project not found")
|
||||
if not project.cycle_view:
|
||||
raise serializers.ValidationError("Cycles are not enabled for this project")
|
||||
if (
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("end_date", None) is not None
|
||||
@@ -63,13 +75,6 @@ class CycleCreateSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||
|
||||
if data.get("start_date", None) is not None and data.get("end_date", None) is not None:
|
||||
project_id = self.initial_data.get("project_id") or (
|
||||
self.instance.project_id if self.instance and hasattr(self.instance, "project_id") else None
|
||||
)
|
||||
|
||||
if not project_id:
|
||||
raise serializers.ValidationError("Project ID is required")
|
||||
|
||||
data["start_date"] = convert_to_utc(
|
||||
date=str(data.get("start_date").date()),
|
||||
project_id=project_id,
|
||||
|
||||
@@ -10,6 +10,7 @@ from plane.db.models import (
|
||||
ModuleMember,
|
||||
ModuleIssue,
|
||||
ProjectMember,
|
||||
Project,
|
||||
)
|
||||
|
||||
|
||||
@@ -53,6 +54,14 @@ class ModuleCreateSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
project_id = self.context.get("project_id")
|
||||
if not project_id:
|
||||
raise serializers.ValidationError("Project ID is required")
|
||||
project = Project.objects.get(id=project_id)
|
||||
if not project:
|
||||
raise serializers.ValidationError("Project not found")
|
||||
if not project.module_view:
|
||||
raise serializers.ValidationError("Modules are not enabled for this project")
|
||||
if (
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("target_date", None) is not None
|
||||
|
||||
@@ -3,13 +3,7 @@ import random
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectIdentifier,
|
||||
WorkspaceMember,
|
||||
State,
|
||||
Estimate,
|
||||
)
|
||||
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate
|
||||
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
@@ -123,6 +117,7 @@ class ProjectCreateSerializer(BaseSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
identifier = validated_data.get("identifier", "").strip().upper()
|
||||
|
||||
if identifier == "":
|
||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
||||
|
||||
@@ -171,7 +166,7 @@ class ProjectUpdateSerializer(ProjectCreateSerializer):
|
||||
|
||||
if (
|
||||
validated_data.get("estimate", None) is not None
|
||||
and not Estimate.objects.filter(project=instance, id=validated_data.get("estimate")).exists()
|
||||
and not Estimate.objects.filter(project=instance, id=validated_data.get("estimate").id).exists()
|
||||
):
|
||||
# Check if the estimate is a estimate in the project
|
||||
raise serializers.ValidationError("Estimate should be a estimate in the project")
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import Sticky
|
||||
from plane.utils.content_validator import validate_html_content, validate_binary_data
|
||||
|
||||
|
||||
class StickySerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Sticky
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "owner"]
|
||||
extra_kwargs = {"name": {"required": False}}
|
||||
|
||||
def validate(self, data):
|
||||
# Validate description content for security
|
||||
if "description_html" in data and data["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(data["description_html"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"error": "html content is not valid"})
|
||||
# Update the data with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
data["description_html"] = sanitized_html
|
||||
|
||||
if "description_binary" in data and data["description_binary"]:
|
||||
is_valid, error_msg = validate_binary_data(data["description_binary"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_binary": "Invalid binary data"})
|
||||
|
||||
return data
|
||||
@@ -9,6 +9,7 @@ from .state import urlpatterns as state_patterns
|
||||
from .user import urlpatterns as user_patterns
|
||||
from .work_item import urlpatterns as work_item_patterns
|
||||
from .invite import urlpatterns as invite_patterns
|
||||
from .sticky import urlpatterns as sticky_patterns
|
||||
|
||||
urlpatterns = [
|
||||
*asset_patterns,
|
||||
@@ -22,4 +23,5 @@ urlpatterns = [
|
||||
*user_patterns,
|
||||
*work_item_patterns,
|
||||
*invite_patterns,
|
||||
*sticky_patterns,
|
||||
]
|
||||
|
||||
@@ -15,4 +15,4 @@ router.register(r"invitations", WorkspaceInvitationsViewset, basename="workspace
|
||||
# Wrap the router URLs with the workspace slug path
|
||||
urlpatterns = [
|
||||
path("workspaces/<str:slug>/", include(router.urls)),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import ProjectMemberListCreateAPIEndpoint, ProjectMemberDetailAPIEndpoint, WorkspaceMemberAPIEndpoint
|
||||
from plane.api.views import (
|
||||
ProjectMemberListCreateAPIEndpoint,
|
||||
ProjectMemberDetailAPIEndpoint,
|
||||
WorkspaceMemberAPIEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# Project members
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from plane.api.views import StickyViewSet
|
||||
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"stickies", StickyViewSet, basename="workspace-stickies")
|
||||
|
||||
urlpatterns = [
|
||||
path("workspaces/<str:slug>/", include(router.urls)),
|
||||
]
|
||||
@@ -54,4 +54,6 @@ from .asset import UserAssetEndpoint, UserServerAssetEndpoint, GenericAssetEndpo
|
||||
|
||||
from .user import UserEndpoint
|
||||
|
||||
from .invite import WorkspaceInvitationsViewset
|
||||
from .invite import WorkspaceInvitationsViewset
|
||||
|
||||
from .sticky import StickyViewSet
|
||||
|
||||
@@ -13,8 +13,6 @@ from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.generics import GenericAPIView
|
||||
|
||||
@@ -195,9 +195,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Current Cycle
|
||||
if cycle_view == "current":
|
||||
queryset = queryset.filter(
|
||||
start_date__lte=timezone.now(), end_date__gte=timezone.now()
|
||||
)
|
||||
queryset = queryset.filter(start_date__lte=timezone.now(), end_date__gte=timezone.now())
|
||||
data = CycleSerializer(
|
||||
queryset,
|
||||
many=True,
|
||||
@@ -254,9 +252,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Incomplete Cycles
|
||||
if cycle_view == "incomplete":
|
||||
queryset = queryset.filter(
|
||||
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True)
|
||||
)
|
||||
queryset = queryset.filter(Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True))
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
@@ -302,17 +298,10 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
Create a new development cycle with specified name, description, and date range.
|
||||
Supports external ID tracking for integration purposes.
|
||||
"""
|
||||
if (
|
||||
request.data.get("start_date", None) is None
|
||||
and request.data.get("end_date", None) is None
|
||||
) or (
|
||||
request.data.get("start_date", None) is not None
|
||||
and request.data.get("end_date", None) is not None
|
||||
if (request.data.get("start_date", None) is None and request.data.get("end_date", None) is None) or (
|
||||
request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not None
|
||||
):
|
||||
|
||||
serializer = CycleCreateSerializer(
|
||||
data=request.data, context={"request": request}
|
||||
)
|
||||
serializer = CycleCreateSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
@@ -355,9 +344,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "Both start date and end date are either required or are to be null"
|
||||
},
|
||||
{"error": "Both start date and end date are either required or are to be null"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -505,9 +492,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
current_instance = json.dumps(
|
||||
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder)
|
||||
|
||||
if cycle.archived_at:
|
||||
return Response(
|
||||
@@ -520,20 +505,14 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
if "sort_order" in request_data:
|
||||
# Can only change sort order
|
||||
request_data = {
|
||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
||||
}
|
||||
request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)}
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so it cannot be edited"
|
||||
},
|
||||
{"error": "The Cycle has already been completed so it cannot be edited"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = CycleUpdateSerializer(
|
||||
cycle, data=request.data, partial=True, context={"request": request}
|
||||
)
|
||||
serializer = CycleUpdateSerializer(cycle, data=request.data, partial=True, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
@@ -541,9 +520,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
and Cycle.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", cycle.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", cycle.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
@@ -600,11 +577,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
||||
"issue", flat=True
|
||||
)
|
||||
)
|
||||
cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True))
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
@@ -624,9 +597,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
# Delete the user favorite cycle
|
||||
UserFavorite.objects.filter(
|
||||
entity_type="cycle", entity_identifier=pk, project_id=project_id
|
||||
).delete()
|
||||
UserFavorite.objects.filter(entity_type="cycle", entity_identifier=pk, project_id=project_id).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -764,9 +735,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda cycles: CycleSerializer(cycles, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@cycle_docs(
|
||||
@@ -785,9 +754,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
Move a completed cycle to archived status for historical tracking.
|
||||
Only cycles that have ended can be archived.
|
||||
"""
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
|
||||
if cycle.end_date >= timezone.now():
|
||||
return Response(
|
||||
{"error": "Only completed cycles can be archived"},
|
||||
@@ -818,9 +785,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
Restore an archived cycle to active status, making it available for regular use.
|
||||
The cycle will reappear in active cycle lists.
|
||||
"""
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
|
||||
cycle.archived_at = None
|
||||
cycle.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -883,9 +848,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
# List
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
|
||||
)
|
||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -922,9 +885,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issues),
|
||||
on_results=lambda issues: IssueSerializer(
|
||||
issues, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@cycle_docs(
|
||||
@@ -958,9 +919,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=cycle_id)
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
return Response(
|
||||
@@ -972,13 +931,9 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Get all CycleWorkItems already created
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
|
||||
)
|
||||
cycle_issues = list(CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues))
|
||||
existing_issues = [
|
||||
str(cycle_issue.issue_id)
|
||||
for cycle_issue in cycle_issues
|
||||
if str(cycle_issue.issue_id) in issues
|
||||
str(cycle_issue.issue_id) for cycle_issue in cycle_issues if str(cycle_issue.issue_id) in issues
|
||||
]
|
||||
new_issues = list(set(issues) - set(existing_issues))
|
||||
|
||||
@@ -1029,9 +984,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_cycle_issues": update_cycle_issue_activity,
|
||||
"created_cycle_issues": serializers.serialize(
|
||||
"json", created_records
|
||||
),
|
||||
"created_cycle_issues": serializers.serialize("json", created_records),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
@@ -1107,9 +1060,7 @@ class CycleIssueDetailAPIEndpoint(BaseAPIView):
|
||||
cycle_id=cycle_id,
|
||||
issue_id=issue_id,
|
||||
)
|
||||
serializer = CycleIssueSerializer(
|
||||
cycle_issue, fields=self.fields, expand=self.expand
|
||||
)
|
||||
serializer = CycleIssueSerializer(cycle_issue, fields=self.fields, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@cycle_docs(
|
||||
@@ -1214,7 +1165,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
{"error": "New Cycle Id is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
old_cycle = Cycle.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
|
||||
@@ -154,7 +154,6 @@ class ProjectMemberListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
# API endpoint to get and update a project member
|
||||
class ProjectMemberDetailAPIEndpoint(ProjectMemberListCreateAPIEndpoint):
|
||||
|
||||
@extend_schema(
|
||||
operation_id="get_project_member",
|
||||
summary="Get project member",
|
||||
|
||||
@@ -65,9 +65,7 @@ from plane.utils.openapi import (
|
||||
ADMIN_ONLY_RESPONSE,
|
||||
REQUIRED_FIELDS_RESPONSE,
|
||||
MODULE_ISSUE_NOT_FOUND_RESPONSE,
|
||||
ARCHIVED_RESPONSE,
|
||||
CANNOT_ARCHIVE_RESPONSE,
|
||||
UNARCHIVED_RESPONSE,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -210,7 +210,9 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
serializer = ProjectCreateSerializer(data={**request.data}, context={"workspace_id": workspace.id})
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from plane.api.views.base import BaseViewSet
|
||||
from plane.app.permissions import WorkspaceUserPermission
|
||||
from plane.db.models import Sticky, Workspace
|
||||
from plane.api.serializers import StickySerializer
|
||||
|
||||
# OpenAPI imports
|
||||
from plane.utils.openapi.decorators import sticky_docs
|
||||
|
||||
from drf_spectacular.utils import OpenApiRequest, OpenApiResponse
|
||||
from plane.utils.openapi import (
|
||||
STICKY_EXAMPLE,
|
||||
create_paginated_response,
|
||||
DELETED_RESPONSE,
|
||||
)
|
||||
|
||||
|
||||
class StickyViewSet(BaseViewSet):
|
||||
serializer_class = StickySerializer
|
||||
model = Sticky
|
||||
use_read_replica = True
|
||||
permission_classes = [WorkspaceUserPermission]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(owner_id=self.request.user.id)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@sticky_docs(
|
||||
operation_id="create_sticky",
|
||||
summary="Create a new sticky",
|
||||
description="Create a new sticky in the workspace",
|
||||
request=OpenApiRequest(request=StickySerializer),
|
||||
responses={
|
||||
201: OpenApiResponse(description="Sticky created", response=StickySerializer, examples=[STICKY_EXAMPLE])
|
||||
},
|
||||
)
|
||||
def create(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
serializer = StickySerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(workspace_id=workspace.id, owner_id=request.user.id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@sticky_docs(
|
||||
operation_id="list_stickies",
|
||||
summary="List stickies",
|
||||
description="List all stickies in the workspace",
|
||||
responses={
|
||||
200: create_paginated_response(
|
||||
StickySerializer, "Sticky", "List of stickies", example_name="List of stickies"
|
||||
)
|
||||
},
|
||||
)
|
||||
def list(self, request, slug):
|
||||
query = request.query_params.get("query", False)
|
||||
stickies = self.get_queryset().order_by("-created_at")
|
||||
if query:
|
||||
stickies = stickies.filter(description_stripped__icontains=query)
|
||||
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(stickies),
|
||||
on_results=lambda stickies: StickySerializer(stickies, many=True).data,
|
||||
default_per_page=20,
|
||||
)
|
||||
|
||||
@sticky_docs(
|
||||
operation_id="retrieve_sticky",
|
||||
summary="Retrieve a sticky",
|
||||
description="Retrieve a sticky by its ID",
|
||||
responses={200: OpenApiResponse(description="Sticky", response=StickySerializer, examples=[STICKY_EXAMPLE])},
|
||||
)
|
||||
def retrieve(self, request, slug, pk):
|
||||
sticky = self.get_object()
|
||||
return Response(StickySerializer(sticky).data)
|
||||
|
||||
@sticky_docs(
|
||||
operation_id="update_sticky",
|
||||
summary="Update a sticky",
|
||||
description="Update a sticky by its ID",
|
||||
request=OpenApiRequest(request=StickySerializer),
|
||||
responses={200: OpenApiResponse(description="Sticky", response=StickySerializer, examples=[STICKY_EXAMPLE])},
|
||||
)
|
||||
def partial_update(self, request, slug, pk):
|
||||
sticky = self.get_object()
|
||||
serializer = StickySerializer(sticky, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@sticky_docs(
|
||||
operation_id="delete_sticky",
|
||||
summary="Delete a sticky",
|
||||
description="Delete a sticky by its ID",
|
||||
responses={204: DELETED_RESPONSE},
|
||||
)
|
||||
def destroy(self, request, slug, pk):
|
||||
sticky = self.get_object()
|
||||
sticky.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user