Compare commits

..

2 Commits

Author SHA1 Message Date
Aaryan Khandelwal f32458eb7b chore: add reload logic 2026-01-29 14:38:05 +05:30
Aaryan Khandelwal 5a31c6449d feat: comments draft 2026-01-29 13:29:56 +05:30
2334 changed files with 72457 additions and 141673 deletions
-67
View File
@@ -1,67 +0,0 @@
---
name: branch-name
description: Use when starting a new branch or renaming an existing one — produces a branch name in the format `<type>/<work-item-id>-<short-description>` that's compatible with the create-pr skill's work item ID extraction.
user_invocable: true
---
# Branch Naming
Create branch names that follow the convention `<type>/<work-item-id>-<short-description>`, where the work item ID can be cleanly extracted later (e.g., by the create-pr skill).
## Format
```
<type>/<work-item-id>-<short-description>
```
- All lowercase, hyphen-separated
- Work item ID stays in its original form but lowercased (e.g., `SILO-1146``silo-1146`)
- Short description is 25 words in kebab-case, focused on the _what_, not the _how_
## Workflow
1. **Determine the type** based on the work being done:
- `feat` — new functionality
- `fix` — bug fix
- `chore` — tooling, deps, config, non-user-facing housekeeping
- `refactor` — restructuring without behavior change
- `docs` — documentation only
- `perf` — performance improvement
2. **Determine the work item ID**:
- If the user gives one, use it
- If they reference a Plane work item (e.g., a URL or title), extract the ID
- If none exists, ask the user — don't invent one
3. **Write the short description**:
- 25 words in kebab-case
- Describe the outcome, not the implementation (`add-app-tile-visibility`, not `update-tile-component`)
- Skip filler words (`the`, `a`, `for`)
4. **Assemble and create the branch**:
```
git checkout -b <type>/<work-item-id-lowercased>-<short-description>
```
5. **Return the branch name** to the user.
## Examples
```
fix/silo-1146-relative-config-urls
feat/web-1234-app-tile-visibility
chore/web-2201-bump-eslint
refactor/silo-980-extract-auth-middleware
docs/web-1500-pr-template-update
perf/silo-1310-cache-workspace-lookup
```
## Common Mistakes
- Putting the work item ID at the end instead of after the type (breaks extraction)
- Using underscores or camelCase instead of hyphens
- Uppercasing the work item ID inside the branch name (it should be lowercase here, uppercased only when used as the PR title prefix)
- Writing a long, narrative description — keep it scannable
- Omitting the work item ID when one exists in Plane
- Using a type that won't match the eventual PR type (pick the type you'd use in the PR title)
@@ -1,65 +0,0 @@
---
name: create-pull-request
description: Use when creating a pull request for the current branch — gathers branch context, generates a PR description following the repo's pull_request_template.md, and creates the PR with a Plane work item ID prefix in the title.
user_invocable: true
---
# Create PR
Create a pull request using the repo's PR template, a Plane work item ID as the title prefix, and a fully filled-out description based on the actual diff.
## Workflow
1. **Determine the base branch**: Default to `preview` unless the user specifies otherwise.
2. **Gather context** (in parallel):
- `git status -s` — check for uncommitted changes
- `git diff <base>...HEAD --stat` — files changed
- `git log <base>...HEAD --oneline` — all commits on the branch
- `git diff <base>...HEAD --no-color` — full diff for understanding changes (if very large, focus on the most important files first)
- `git rev-parse --abbrev-ref --symbolic-full-name @{u}` — check if branch tracks a remote
- Read `.github/pull_request_template.md` from the repo root
3. **Determine work item ID**:
- Extract from branch name if it contains an identifier (e.g., `chore/silo-1146-foo``SILO-1146`, `feat/web-1234-x``WEB-1234`)
- If not found in branch name, ask the user
4. **Draft the PR** using the template from step 2:
**Title**: `[WORK-ITEM-ID] <type>: <concise summary>` (under 70 chars)
- Type reflects the change: `fix`, `feat`, `chore`, `refactor`, `docs`, `perf`, etc.
**Body**: Fill in every section from the PR template based on the actual diff:
- **Description** — Clear, concise summary of what the PR does and why. Focus on the "what" and "why", not line-by-line changes. Mention important implementation decisions.
- **Type of Change** — Check the appropriate box(es): Bug fix, Feature, Improvement, Code refactoring, Performance improvements, Documentation update.
- **Screenshots and Media** — Leave a placeholder: `<!-- Add screenshots here -->`
- **Test Scenarios** — Suggest concrete scenarios grounded in the actual changes (e.g., "Navigate to project settings and verify the new toggle works"), not generic ones.
- **References** — Include the work item ID, any linked issues the user mentions, and any Sentry issue links/IDs (e.g., `SENTRY-ABC123` or Sentry URLs) referenced earlier in the conversation.
Append a Claude Code session line at the bottom of the body.
5. **Push and create** (in parallel where possible):
- Push branch with `-u` if no upstream is set
- Create PR via `gh pr create` using a HEREDOC for the body
6. **Return the PR URL** to the user.
## Example Title
```
[SILO-1146] fix: allow relative URLs for configuration_url and improve app tile visibility
```
## Guidelines
- Keep the description concise but informative
- Use bullet points when listing multiple changes
- Focus on user-facing impact, not implementation details
- Don't fabricate test scenarios that aren't relevant to the actual changes
## Common Mistakes
- Summarizing only the latest commit instead of all commits on the branch
- Forgetting to check for an upstream before pushing
- Using a work item ID format that doesn't match the branch convention
- Wrapping the PR body in a code fence when passing it to `gh pr create`
-200
View File
@@ -1,200 +0,0 @@
---
name: release-notes
description: "Generate release notes for a Plane release PR in either `makeplane/plane-cloud` (date-based versioning, e.g. `release: vYY.MM.DD-N`) or `makeplane/plane-ee` (semver, e.g. `release: vX.Y.Z`). Reads PR commits, filters out noise, categorizes by conventional-commit type, optionally enriches via Plane MCP, and writes the result as the PR description in the GitHub Releases format."
user_invocable: true
---
# Release Notes Generator
Generate structured release notes from a Plane release PR by parsing its commit list, then update the PR description. Output matches the format used on `github.com/makeplane/plane/releases` (e.g. [v1.2.0](https://github.com/makeplane/plane/releases/tag/v1.2.0)). Works for both `makeplane/plane-cloud` and `makeplane/plane-ee`.
## Repo-specific versioning
Plane uses **different version schemes** across its two release repos. Detect which repo the PR belongs to and use the matching scheme when communicating about the release — the version itself does **not** appear in the release notes body (GitHub's release tag carries it).
| Repo | Version scheme | Example PR title | Source branch | Target branch |
| ----------------------- | -------------- | ---------------------- | ------------- | -------------------- |
| `makeplane/plane-cloud` | Date-based | `release: v26.04.13-1` | `uat` | `master` |
| `makeplane/plane-ee` | Semver | `release: v1.12.0` | `uat` | `master` / `preview` |
- **plane-cloud** ships daily — version is `vYY.MM.DD-N` where `N` is the counter for that date's release.
- **plane-ee** ships on a versioned cadence — version is `vX.Y.Z` (major.minor.patch) following semver.
- Detect the repo with `gh pr view <PR_NUM> --json headRepository,baseRepository` or from the URL the user shared.
## When to Use
- User links/mentions a Plane release PR (e.g. `release: v26.04.13-1` for cloud or `release: v1.12.0` for EE) and asks for release notes
- User asks to "create release notes" / "update PR description" for a PR in `makeplane/plane-cloud` or `makeplane/plane-ee`
- The branch is named `uat` or `release/x.y.z` and the base is `master` or `preview`
## Steps
### 1. Fetch commits
```bash
gh pr view <PR_NUM> --json title,body,baseRefName,headRefName,commits \
--jq '.commits[] | .messageHeadline + "\n---BODY---\n" + .messageBody + "\n===END==="'
```
For a quick scan first:
```bash
gh pr view <PR_NUM> --json commits \
--jq '.commits[] | {oid: .oid[0:10], message: .messageHeadline}'
```
### 2. Filter out noise
**Always exclude** these commits — mechanical, not user-facing:
| Pattern | Reason |
| -------------------------------------------- | ------------------------------------- |
| `Sync: Enterprise Changes #NNNN` | Cross-repo sync, no functional change |
| `fix: merge conflicts` | Merge artifact |
| `Merge branch '...' of github.com:...` | Merge artifact |
| `Revert "..."` (when immediately re-applied) | Internal churn |
### 3. Identify work item IDs (for research only)
Most meaningful commits begin with a Plane work item identifier in brackets:
- `[WEB-XXXX]` — web/frontend product items
- `[SILO-XXXX]` — Silo (integrations: Slack, GitHub, GitLab, Jira/Linear)
- `[MOBILE-XXXX]`, `[API-XXXX]`, etc.
**Do not include these IDs in the release notes.** The GitHub Releases format is end-user-facing — IDs are only useful as a lookup key for fetching context in step 4.
### 4. (Optional) Enrich via Plane MCP
For larger features where the commit headline is terse, fetch the work item to write a richer paragraph:
```
mcp__plane__retrieve_work_item_by_identifier(project_identifier="WEB", issue_identifier=6874)
```
Use the returned `name` and `description_stripped` to flesh out the prose. Skip for routine fixes — commit body is usually enough. Don't enrich every item (slow + descriptions are often empty).
### 5. Categorize commits
Map each surviving commit into one of four sections:
| Commit signal | Section |
| --------------------------------------------------------------------------------------------------------------------- | --------------- |
| `feat:` that introduces a brand-new screen, flow, or capability | ✨ Features |
| `feat:` that improves an existing feature, plus most `refactor:` and behavioural `chore:` items that are user-visible | ⬆️ Enhancements |
| `fix:`, `fix(scope):` | 🐞 Bug fixes |
| CVE upgrades, dependency bumps that close a vulnerability, security hardening | 🛡️ Security |
**Drop entirely** (do not surface to users): pure infra `chore:`, dependabot bumps with no CVE, internal refactors with no behavioural impact, test-only changes, doc-only changes.
### 6. Format
Output follows the GitHub Releases convention — `###` for section headers, with two spaces between the emoji and the label for ✨ / ⬆️ / 🐞 (matches `v1.2.0`).
```markdown
### ✨ Features
#### **Short Feature Name in Title Case**
A 13 sentence paragraph describing what the user gets, why it matters, and any notable behaviour. Write in product-marketing voice, not commit-message voice.
- Optional nested bullets for sub-capabilities or callouts
- Keep them user-facing — what the user can now do
#### **Second Major Feature**
Another descriptive paragraph. Each major feature gets its own `####` subsection.
### ⬆️ Enhancements
- One-line description of an improvement to an existing capability
- Another improvement, written as a clean sentence (no commit prefix, no ticket ID)
### 🐞 Bug fixes
- Plain-English description of what was broken and is now fixed
- Another bug fix
### 🛡️ Security
- Upgraded <component> to <version> to mitigate [CVE-XXXX-NNNNN](https://link-to-advisory). Brief impact note.
- Other security-relevant change
```
Rules:
- Section headers use `###` (three hashes), then emoji + **two spaces** + label — exactly as in the published v1.2.0 release. Exception: 🛡️ Security uses a single space (matches v1.2.0).
- Features use `####` (four hashes) and the feature name is **bolded** inside the heading: `#### **Feature Name**`.
- Each feature gets a real paragraph, not a bullet — written for end users, not engineers.
- Enhancements, Bug fixes, and Security are simple bullets. No nested asterisks, no ticket IDs, no PR numbers.
- **Do not include work item IDs (`[WEB-XXXX]`) or PR numbers (`(#NNNN)`)** in any section — this format is user-facing.
- **Do not add a `# Release vX.Y.Z` heading.** The GitHub release tag carries the version; the body starts directly with the first `### ✨ Features` section.
- **Do not insert images.** The user adds screenshots manually after the notes are drafted. Leave space for them only if the user asks.
- Drop empty sections entirely.
- Blank line between section header and first bullet/feature, and between sections.
### 7. Update the PR description
```bash
gh pr edit <PR_NUM> --body "$(cat <<'EOF'
<release notes markdown>
EOF
)"
```
Always use a HEREDOC with single-quoted `'EOF'` so backticks/dollars in the notes are preserved.
## Quick Reference: end-to-end
```bash
PR=2498
gh pr view $PR --json commits --jq '.commits[] | .messageHeadline + "\n---\n" + .messageBody + "\n==="' > /tmp/commits.txt
# read /tmp/commits.txt, filter, categorize into the four sections, draft notes
gh pr edit $PR --body "$(cat <<'EOF'
### ✨ Features
#### **...**
...
### ⬆️ Enhancements
- ...
### 🐞 Bug fixes
- ...
### 🛡️ Security
- ...
EOF
)"
```
## Reference example
The canonical target format is [v1.2.0](https://github.com/makeplane/plane/releases/tag/v1.2.0) on `makeplane/plane`. When in doubt about heading levels, spacing, bolding, or paragraph voice, match that page exactly (minus images).
## Common Mistakes
- **Including work item IDs in bullets** — the GitHub Releases format is user-facing; `[WEB-XXXX]` belongs in internal research, not the output.
- **Adding a `# Release vX.Y.Z` heading** — GitHub's release tag is the version. The body starts with `### ✨ Features`.
- **Copy-pasting commit subjects verbatim** — rewrite into product-marketing English. "fix: peek overview reload on parent add" → "Fixed peek overview reloading on adding a parent".
- **Bulleting features instead of writing paragraphs** — major features get `#### **Name**` plus a real paragraph; only enhancements/bugs/security use bullets.
- **Including `Sync: Enterprise Changes` commits** — these are sync PRs, never user-visible.
- **Including `fix: merge conflicts`** — merge artifact, no functional content.
- **Inserting images** — leave images for the user; they add screenshots manually.
- **Using `--body` without HEREDOC** — backticks/dollar signs get shell-interpreted and corrupt the notes.
- **Editing the PR title** — release PR titles are version markers; only edit the body.
- **Adding a Chores section** — the GitHub Releases format has no Chores section; user-invisible chores are dropped entirely.
## Plane-Specific Conventions
- Release PRs go from `uat``master` (or `preview`).
- PR title format:
- `plane-cloud`: `release: vYY.MM.DD-N` where N is the daily release counter for that date.
- `plane-ee`: `release: vX.Y.Z` semver (major.minor.patch).
- Commits coming from feature branches always carry a work item ID; commits without one are usually infra/chores and almost always dropped from notes.
- `Sync: Enterprise Changes #NNNN` are automated cross-repo syncs and are _always_ skipped.
- CVE-related upgrades (NextJS, React, Django, nginx, etc.) belong under 🛡️ Security with a link to the advisory and a one-line impact note.
-608
View File
@@ -1,608 +0,0 @@
---
name: translate
description: Translate or update keys in packages/i18n/src/locales. Use whenever adding, changing, or reviewing strings across any target locale — enforces do-not-translate terminology, CLDR plural forms, placeholder/tag preservation, per-locale punctuation/register, and the AI-translation review workflow. Required reading before touching any *.json under src/locales.
user_invocable: true
---
<!--
`user_invocable: true` is advisory and follows the same convention as the existing
`pr-description.md` and `release-notes.md` skills in this repo. No harness behavior
in this codebase keys off the field today; it documents intent for the Claude Code
skill registry. The skill is invoked via the slash command (`/translate`) registered
in `.claude/commands/translate.md`, or by name when an agent infers relevance.
-->
# Translate (Plane i18n)
Single source of truth for turning English UI strings into every target locale under `packages/i18n/src/locales/`. Follow this skill exactly — every mistake here ships to every user in that language.
The locale list grows over time. This skill is intentionally generic: rules apply to any locale present now or added later. When you add a locale not explicitly named below, see **Adding a locale not documented here** at the bottom.
Sources distilled from: Microsoft Localization Style Guides, Mozilla L10n, Unicode CLDR plural rules, W3C i18n, Google developer style guide, Apple HIG, GitHub Primer, MQM error typology, Lokalise/Crowdin/Phrase best-practice docs, and Microsoft's AI/LLM-for-translation guidance.
**Source of truth**: `packages/i18n/src/locales/en/<namespace>.json`. Every other locale mirrors English key-for-key. The `sync-check.ts` script catches missing / stale / collision keys; it does **not** catch value-quality mistakes. That is what this skill is for.
## When to Use
- Adding a new key to any `packages/i18n/src/locales/en/*.json`
- Renaming or rewording an English value (every target is now stale)
- Adding a new language (copy from `en/`, then translate each file)
- Syncing after `pnpm --filter @plane/i18n run sync:check` reports drift
- Reviewing any PR that touches `packages/i18n/src/locales/`
Skip only for trivial English-only typo fixes that don't change meaning or length meaningfully.
## The Two Iron Rules
1. **Trademarks, brand marks, plan tier names, third-party product names, acronyms, and code tokens are never translated.** Plane's brand marks (Plane, Plane AI, Power K, PQL, Active Cycles, Sticky/Stickies, Intake), plan tiers (Pro, Business, Enterprise), third-party products (GitHub, Slack, Notion, etc.), and acronyms (API, OAuth, etc.) stay Latin in every locale.
2. **CLDR plural categories are mandatory.** Every target locale must include **every** plural keyword the language requires. Missing a form renders the wrong word at runtime and passes `sync-check` silently.
Common feature nouns — Cycle, Module, Epic, Page — **are translated** into the target language using the canonical glossary further down. They are not brand marks; they are everyday words that belong in the user's language.
The rest of this skill explains how to execute on those rules.
## Do-Not-Translate (DNT) Glossary
For each term: the source, the required rendering per script group, and **forbidden renderings** that have appeared historically and must be reverted when seen.
### Plane brand & features
| Source term | Latin locales (fr, es, it, de, pt-BR, pl, cs, sk, ro, tr-TR, vi-VN, id) | ja (katakana-default) | ko (Hangul-default) | zh-CN / zh-TW | ru | ua | **Forbidden** (never produce) |
| ---------------------------------- | ----------------------------------------------------------------------- | --------------------- | ------------------- | --------------------------------------------- | ----------------- | ----------------- | --------------------------------------------------------------------------------- |
| **Plane** | Plane | Plane | Plane | Plane | Plane | Plane | 飛行機, 飞机, 비행기, Самолёт, Літак, Avion, Avião, Aereo, Flugzeug |
| **Plane AI** (formerly PI Chat) | Plane AI | Plane AI | Plane AI | Plane AI | Plane AI | Plane AI | Чат ИИ, AI 聊天, AIチャット, AI 채팅, Chat IA, AI Çet, PI Chat (legacy) |
| **Power K** | Power K | Power K | Power K | Power K | Power K | Power K | Command K, Command Palette, コマンドパレット, 命令面板, Палитра команд |
| **PQL** | PQL | PQL | PQL | PQL | PQL | PQL | any expansion of the acronym into the target language |
| **Intake** (feature name) | Intake | Intake | Intake | Intake | Intake | Intake | Inbox, 受信箱, 收件箱, Входящие, Triage, Boîte de réception |
| **Active Cycles** (workspace view) | Active Cycles | Active Cycles | Active Cycles | Active Cycles | Active Cycles | Active Cycles | Translate as a unit; never split into generic "active" + localized "cycles" |
| **Sticky** / **Stickies** | Sticky / Stickies | Sticky / Stickies | Sticky / Stickies | Sticky / Stickies (Latin inside Chinese text) | Sticky / Stickies | Sticky / Stickies | 便签, 便利貼, メモ, 付箋, スティッキー, 메모, 스티키, заметка, Стикер, Note, Nota |
| **Pro** (plan tier) | Pro | Pro | Pro | Pro | Pro | Pro | Профессиональный, プロフェッショナル, 专业版, 專業版, Profesional |
| **Business** (plan tier) | Business | Business | Business | Business | Business | Business | Бизнес, ビジネス, 商业版, 商務版, Negocios, Negócios |
| **Enterprise** (plan tier) | Enterprise | Enterprise | Enterprise | Enterprise | Enterprise | Enterprise | Корпоративный, エンタープライズ, 企业版, 企業版, Empresarial |
### Plane feature noun translation glossary (translate, do not preserve)
These are common nouns. **Translate them into the target language** using the canonical form below. This follows Microsoft, Apple, and Mozilla style guides for product-UI translation: feature common nouns belong in the user's language; only trademarks, brand marks, and acronyms stay Latin. Leaving these in Latin in non-Latin locales reads as half-translated and is a measurable quality defect.
Use the table verbatim — never coin new variants; never leave the Latin form in the locale value.
| Source | zh-CN | zh-TW | ja | ko | ru | ua | de | fr | es | it | pt-BR | pl | cs | sk | ro | tr-TR | vi-VN | id |
| ----------- | ----- | ----- | ---------- | ------ | -------- | -------- | ------ | ------- | ------- | ------ | ------- | ------ | ------- | ------- | ------- | -------- | ------ | ------- |
| **Cycle** | 周期 | 週期 | サイクル | 사이클 | Цикл | Цикл | Zyklus | Cycle | Ciclo | Ciclo | Ciclo | Cykl | Cyklus | Cyklus | Ciclu | Döngü | Chu kỳ | Siklus |
| **Cycles** | 周期 | 週期 | サイクル | 사이클 | Циклы | Цикли | Zyklen | Cycles | Ciclos | Cicli | Ciclos | Cykle | Cykly | Cykly | Cicluri | Döngüler | Chu kỳ | Siklus |
| **Module** | 模块 | 模組 | モジュール | 모듈 | Модуль | Модуль | Modul | Module | Módulo | Modulo | Módulo | Moduł | Modul | Modul | Modul | Modül | Mô-đun | Modul |
| **Modules** | 模块 | 模組 | モジュール | 모듈 | Модули | Модулі | Module | Modules | Módulos | Moduli | Módulos | Moduły | Moduly | Moduly | Module | Modüller | Mô-đun | Modul |
| **Epic** | 史诗 | 史詩 | エピック | 에픽 | Эпик | Епік | Epic | Epic | Epic | Epic | Epic | Epik | Epik | Epik | Epic | Epik | Epic | Epik |
| **Epics** | 史诗 | 史詩 | エピック | 에픽 | Эпики | Епіки | Epics | Epics | Epics | Epics | Epics | Epiki | Epiky | Epiky | Epice | Epikler | Epic | Epik |
| **Page** | 页面 | 頁面 | ページ | 페이지 | Страница | Сторінка | Seite | Page | Página | Pagina | Página | Strona | Stránka | Stránka | Pagină | Sayfa | Trang | Halaman |
| **Pages** | 页面 | 頁面 | ページ | 페이지 | Страницы | Сторінки | Seiten | Pages | Páginas | Pagine | Páginas | Strony | Stránky | Stránky | Pagini | Sayfalar | Trang | Halaman |
Notes:
- Single-form locales (zh-CN, zh-TW, ja, ko, vi-VN, id) use the same form for singular and plural — the column repeats by design.
- Slavic locales (ru, ua, pl, cs, sk) show **nominative singular** and **nominative plural**. Inside ICU `{count, plural, ...}` blocks, use the case-correct form per CLDR keyword (see the Slavic case-form table further down).
- Some Latin locales (fr, vi-VN, id) have forms identical to English (`Cycle`, `Module`, `Page`) — that's the natural cognate, not a Latin-preservation rule.
- **Epic / Epics**: stays Latin in most Latin locales because there's no clean cognate (Spanish `épico` is for poetry/film; same for it/pt-BR/de/fr). Slavic locales use phonetic transliteration that's standard in their software industry (Эпик, Епік, Epik). CJK locales use the literal/transliterated form (史诗, エピック, 에픽).
- **Generic uses translate normally and don't follow this glossary:** `next page` (paginator), `the page` (browser refresh), `status page`, `web page`, `life cycle`, `release cycle`, `rate-limit cycle`, `cycle of releases` — these are not the Plane Cycle/Page feature; translate them as ordinary words in the surrounding prose.
- When editing an existing file, migrate occurrences you are already touching. Do not bulk-rewrite unrelated strings in the same PR — land a separate sweep PR with the `chore(i18n):` prefix.
#### Slavic case forms inside ICU plural blocks
When a Slavic plural block counts a Plane feature noun, use these forms:
| Locale | Term | one (nom.sg.) | few | many | other |
| ------ | -------- | ------------- | -------- | -------- | -------- |
| **ru** | Цикл | Цикл | Цикла | Циклов | Цикла |
| **ru** | Модуль | Модуль | Модуля | Модулей | Модуля |
| **ru** | Эпик | Эпик | Эпика | Эпиков | Эпика |
| **ru** | Страница | Страница | Страницы | Страниц | Страницы |
| **ua** | Цикл | Цикл | Цикла | Циклів | Цикла |
| **ua** | Модуль | Модуль | Модуля | Модулів | Модуля |
| **ua** | Епік | Епік | Епіка | Епіків | Епіка |
| **ua** | Сторінка | Сторінка | Сторінки | Сторінок | Сторінки |
| **pl** | Cykl | Cykl | Cykle | Cykli | Cyklu |
| **pl** | Moduł | Moduł | Moduły | Modułów | Modułu |
| **pl** | Epik | Epik | Epiki | Epików | Epika |
| **pl** | Strona | Strona | Strony | Stron | Strony |
| **cs** | Cyklus | Cyklus | Cykly | Cyklu | Cyklů |
| **cs** | Modul | Modul | Moduly | Modulu | Modulů |
| **cs** | Epik | Epik | Epiky | Epiku | Epiků |
| **cs** | Stránka | Stránka | Stránky | Stránky | Stránek |
| **sk** | Cyklus | Cyklus | Cykly | Cyklu | Cyklov |
| **sk** | Modul | Modul | Moduly | Modulu | Modulov |
| **sk** | Epik | Epik | Epiky | Epiku | Epikov |
| **sk** | Stránka | Stránka | Stránky | Stránky | Stránok |
Per CLDR for ru/ua: `one` fires for 1, 21, 31… (nom.sg.); `few` for 24, 2224… (gen.sg.); `many` for 0, 520, 2530… (gen.pl.); `other` for non-integer counts (gen.sg.). For pl: `one` (1), `few` (24 not 1214, nom.pl.), `many` (0, 520, gen.pl.), `other` (decimals, gen.sg.). cs/sk: `one` (1), `few` (24, nom.pl.), `many` (decimals, gen.sg.), `other` (0, 5+, gen.pl.).
### Third-party products & standards (always Latin, every locale)
GitHub, GitLab, Bitbucket, Slack, Discord, Zoom, Microsoft Teams, Jira, Linear, Asana, Notion, Confluence, Trello, Figma, Google, Google Drive, Google Calendar, Google Docs, Google Sheets, Gmail, YouTube, Dropbox, Zapier, Tiptap, ProseMirror, Yjs, Hocuspocus, Socket.IO, React, TypeScript, JavaScript, Python, Django, PostgreSQL, Redis, RabbitMQ.
Also preserve: OAuth, OIDC, SAML, SSO, LDAP, API, REST, GraphQL, URL, URI, ID, UUID, JSON, YAML, CSV, TSV, XML, HTML, PDF, PNG, JPG, SVG, CSS, HTTP, HTTPS, TLS, SSL, IP, CIDR, DNS, ARIA, WCAG.
### Inside-string tokens (mechanical — preserve exactly)
- **ICU variables**: `{count}`, `{name}`, `{workspace}`, `{userName}` — never translate, never rename, never move inside `{}`.
- **ICU pluralization / select keywords**: `plural`, `select`, `one`, `few`, `many`, `other`, `zero`, `two`, `=0`, `=1`, `#` — never translate.
- **HTML & JSX tags**: `<b>…</b>`, `<a href="…">…</a>`, `<br/>`, `<code>…</code>` — preserve attributes and nesting; translate only the visible text between tags.
- **i18next `Trans` numbered fragments**: `<0>…</0>`, `<1>…</1>` — preserve the numbers exactly; only translate the wrapped text. Never renumber.
- **Markdown**: `**bold**`, `*italic*`, `` `code` ``, `[text](url)` — keep the syntax; translate only human-readable prose.
- **Escape sequences**: `\n` (newline), `\t`, `\"`, `\\` — preserve at the same positions.
- **Keyboard keys & modifiers**: `Ctrl`, `Cmd`, `Shift`, `Alt`, `Option`, `Enter`, `Return`, `Esc`, `Tab`, `Space`, `Backspace`, arrow glyphs (`↑↓←→`), and OS glyphs (`⌘⌥⌃⇧`) — preserve exactly as shown on the physical key.
- **File extensions & formats**: `.json`, `.csv`, `MM/DD/YYYY` format strings — preserve.
## Per-Script Rules
Apply by target-language script, not by locale list. Adding a new Latin-script locale? It follows the Latin-script rules below. Adding a new Cyrillic locale? It follows the Cyrillic section. Scripts not yet shown here (Arabic, Hebrew, Thai, Greek, Hindi/Devanagari, etc.) — see **Adding a locale not documented here**.
### Latin-script locales
Currently includes fr, es, it, de, pt-BR, pl, cs, sk, ro, tr-TR, vi-VN, id — and any future locale that uses the Latin alphabet (hu, nl, sv, da, nb, fi, hr, bg-using-Latin variants, etc.).
**Translate Plane feature nouns** (Cycle, Cycles, Module, Modules, Epic, Epics, Page, Pages) using the per-locale form from the glossary above. **Keep Latin** for Plane brand marks (Plane, Plane AI, Power K, PQL, Active Cycles, Sticky, Stickies, Intake), plan tier names (Pro, Business, Enterprise), and third-party brands (GitHub, Slack, etc.).
```
✅ "Créer un Cycle" (fr — natural cognate from glossary, identical to English)
✅ "Crear un nuevo Ciclo" (es — natural Spanish cognate from glossary)
✅ "Archiviare questo Modulo" (it — natural Italian cognate from glossary)
✅ "Nueva Epic" (es — Epic has no clean cognate; stays Latin per glossary)
✅ "Archivieren Sie diesen Zyklus" (de — natural German form from glossary)
✅ "Buat Siklus baru" (id — natural Indonesian form from glossary)
✅ "Buat Sticky baru" (id — Sticky is a Plane brand mark, stays Latin)
✅ "Wechseln Sie zum Pro-Plan" (de — Pro is a plan tier name, stays Latin)
❌ "Créer un Cycle" WRONG when the locale should be `Zyklus` (de). Always use the glossary form.
❌ "Archivieren Sie diesen Cycle" (de — feature noun was left in Latin; use Zyklus per glossary)
❌ "Créer un Cercle" (fr — invented translation; use the glossary form)
❌ "Nueva Saga" (es — translated "Epic" with the wrong cognate)
❌ "Buat Catatan Tempel" (id — translated "Sticky" which is a brand mark; keep Latin)
```
Generic uses translate normally and do not follow the glossary: a paginator's `next page` is `nächste Seite` / `página siguiente` (generic noun, not the Plane Pages feature). The glossary applies only when EN refers to the Plane product feature.
### Japanese (ja) — natural Japanese rendering
**Plane feature nouns** (Cycle, Module, Epic, Page) use the natural Japanese form per the glossary above (サイクル, モジュール, エピック, ページ — katakana for foreign-origin nouns; native Japanese where one applies, like 付箋 for an Apple/Microsoft-style "sticky note"). **Brand marks** stay in Latin: Plane, Plane AI, Power K, PQL, Active Cycles, Sticky, Stickies, Intake, GitHub, Slack, Pro/Business/Enterprise.
For new katakana coinages and existing translations, the long-vowel mark `ー` is added for words ending in `-er`, `-or`, `-ar`, `-y` in English. This is the Microsoft Japanese style-guide convention and the current industry default:
- user → **ユーザー** (not ユーザ)
- server → **サーバー** (not サーバ)
- editor → **エディター** (not エディタ)
- property → **プロパティー** (_kept as_ プロパティ where the existing codebase already does — match the surrounding file's convention)
Tone: polite form です・ます. Never plain form だ・である. Avoid over-formal 尊敬語・謙譲語 for SaaS product copy — it reads stilted.
Quotation marks: 「」 for primary quotes, 『』 for nested or for titles of works. Full-width punctuation: 。、()・!?.
### Korean (ko) — Hangul rendering
**Plane feature nouns** (Cycle, Module, Epic, Page) use the natural Korean form per the glossary above (사이클, 모듈, 에픽, 페이지 — Hangul transliteration where the term is product-coined; native Korean word where one applies). **Brand marks** stay in Latin: Plane, Plane AI, Power K, PQL, Active Cycles, Sticky, Stickies, Intake, GitHub, Slack, Pro/Business/Enterprise.
For new terms not on the glossary: prefer the native Korean word where one exists (`설정` for Settings); use Hangul phonetic transliteration for product-coined nouns with no native equivalent. Do not coerce a phonetic loanword when a natural Korean word exists — `버킷` for "Bucket" reads as a foreign trademark, `장바구니` reads as a Korean noun.
**Korean particle agreement**: when introducing a consonant-ending noun (사이클, 에픽, 모듈), follow with the consonant-form particle (을/은/이/과), not the vowel-form (를/는/가/와). `사이클을 추가` not `사이클를 추가`.
Register:
- **System and error messages** → 합니다체 (high-formal: 합니다, 됩니다, 입니다). Existing files in this register today: `ko/auth.json`, `ko/error/*.json`.
- **Empty states, onboarding microcopy, tooltips** → 해요체 acceptable (softer: 해요, 돼요, 이에요) if the existing file uses it. Existing files in this register today: `ko/empty-state.json`, `ko/tour.json`. Always match the surrounding file rather than introducing a new register mid-namespace.
- Never casual 반말.
Punctuation: Western punctuation (`. , ? !`); straight quotes `"…"` and `'…'`.
### Simplified Chinese (zh-CN) and Traditional Chinese (zh-TW) — translate feature nouns
**Plane feature nouns** (Cycle, Module, Epic, Page) translate to natural Chinese per the glossary above (zh-CN: 周期, 模块, 史诗, 页面 — zh-TW: 週期, 模組, 史詩, 頁面). This is what Microsoft's zh-CN style guide and every mainstream zh-localized SaaS product (Notion, Slack, Atlassian) does for common feature nouns.
**Brand marks stay Latin**: Plane, Plane AI, Power K, PQL, Active Cycles, Sticky, Stickies, Intake, GitHub, Slack, Pro/Business/Enterprise — these are trademark-style terms.
**Insert a half-width space on each side of embedded Latin tokens** — this is Microsoft's zh-CN guideline and required for legibility. **Exception**: no space between a Latin token and adjacent full-width punctuation (`。,;:?!`); the punctuation already supplies visual breathing room.
```
✅ "使用 GitHub 登录" (Latin brand → half-width spaces around)
✅ "创建周期" (zh-CN feature noun translated; no Latin = no spaces)
✅ "归档此史诗" (Plane Epic → 史诗 per glossary)
✅ "添加 Sticky" (Sticky is a brand mark → Latin + half-width space)
✅ "升级到 Pro" (Pro is a plan tier → Latin)
✅ "登录 GitHub。" (no space between Latin token and full-width period)
❌ "使用GitHub登录" (Latin brand without surrounding half-width space)
❌ "创建 Cycle" (feature noun left in Latin — should be 周期)
❌ "创建Cycle" (feature noun in Latin AND missing space)
❌ "登录 GitHub 。" (stray space before full-width period)
❌ "创建赛克" (invented transliteration of Cycle — use the glossary's 周期)
```
Punctuation is **full-width**: 。,?!;:. Quotes:
- zh-CN: `"…"` (primary) and `'…'` (nested)
- zh-TW: `「…」` (primary) and `『…』` (nested)
- Work titles (book/movie/app/article names): `《…》`
Variant discipline: zh-CN ≠ zh-TW. They differ in script (简体 vs 繁體) **and** vocabulary (视频 vs 影片; 软件 vs 軟體; 网络 vs 網路). Never mass-copy between the two directories.
Register: 您 (formal polite) in Plane UI — the product is B2B/SaaS. Reserve 你 for consumer/youth contexts (not applicable here).
### Cyrillic locales
Currently includes ru, ua — and any future Cyrillic locale (bg-BG, sr-Cyrl, mk, be, kk-Cyrl, etc.).
**Plane feature nouns** (Cycle, Module, Epic, Page) use the natural Cyrillic form per the glossary above (Цикл/Циклы, Модуль/Модули, Эпик/Эпики, Страница/Страницы in ru — Цикл/Цикли, Модуль/Модулі, Епік/Епіки, Сторінка/Сторінки in ua). **Brand marks** stay in Latin: Plane, Plane AI, Power K, PQL, Active Cycles, Sticky, Stickies, Intake, GitHub, Slack, Pro/Business/Enterprise.
For new terms not on the glossary: prefer the native Cyrillic word where one exists; use phonetic transliteration only when no native word applies (`Бакет` is wrong for "Bucket" — use `Корзина`/`Кошик`).
**Slavic case forms inside `{count, plural, ...}` blocks** are case-correct per CLDR keyword (one=nom.sg., few=gen.sg., many=gen.pl., other=gen.sg.). See the case-form table in the glossary section.
Register:
- **ru**: formal **Вы** (capitalized) when addressing a **single user directly** in respectful/formal contexts (onboarding, settings, confirmation dialogs); lowercase **вы** for plural/general references ("все вы"). Default to capitalized Вы in tooltips/dialogs and lowercase вы in descriptive copy.
- **ua**: modern Ukrainian software convention is lowercase **ви** by default; capitalized **Ви** is reserved for very formal direct address. This is the opposite convention from Russian — do not copy-paste Russian capitalization rules into Ukrainian files.
Punctuation: primary quotes `«…»`, nested `„…"`.
**Plural forms — critical**: both ru and ua require `one / few / many / other`. See CLDR section below.
## CLDR Plural Rules
Every `{count, plural, …}` string must contain **every** keyword the target language's CLDR rule requires. `other` is always required, even in single-form languages.
**Canonical source**: Unicode CLDR Language Plural Rules chart — the definitive, versioned list of required categories for every language. Always verify against the current CLDR chart when adding a locale, since rules occasionally shift across CLDR releases. Libraries like `Intl.PluralRules` can resolve the rule at runtime.
Below are the required keywords for locales currently in the repo. When adding a new locale, look up its categories in CLDR and add the row here.
| Locale | Required keywords | Example mapping |
| -------------------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| en, es, de, tr-TR, vi-VN, id, ja, ko, zh-CN, zh-TW | `one, other` (single-form locales may use `other` alone, but **always emit `other`**) | 1 → one; 2+ → other |
| fr | `one, many, other` | 0, 1 → one; 1 000 000 → many; else other |
| it | `one, many, other` | 1 → one; 1 000 000 → many; else other |
| pt-BR | `one, many, other` | 0, 1 → one; 1 000 000 → many; else other |
| ro | `one, few, other` | 1 → one; 0, 219 → few; 20+ → other |
| pl | `one, few, many, other` | 1 → one; 24 (not 1214) → few; 0, 520, many-digit → many; decimals → other |
| cs | `one, few, many, other` | 1 → one; 24 → few; decimals → many; 0, 5+ → other |
| sk | `one, few, many, other` | same pattern as cs |
| ru | `one, few, many, other` | 1, 21, 31… → one; 24, 2224… → few; 0, 520, 2530… → many; decimals → other |
| ua | `one, few, many, other` | same pattern as ru |
| ar (when added) | `zero, one, two, few, many, other` | Six categories — the maximum any language uses |
> **Note on the consolidated row.** CLDR itself only requires `other` for `tr-TR`, `vi-VN`, `id`, `ja`, `ko`, `zh-CN`, `zh-TW` (single-form locales). Emitting both `one` and `other` with identical content is a **project convention** — it keeps tooling and linters consistent across the codebase. It is not a CLDR requirement, and `Intl.PluralRules` will return only `"other"` for these locales at runtime.
For any locale not listed: look up the CLDR categories before writing a single plural string. Arabic has six; Welsh has six; most Slavic languages have four; most Romance languages have two or three; CJK / Turkic / Thai have one (still emit `other`).
Rules in practice:
1. **Never drop a required form**, even when the word is identical across forms — emit each one explicitly.
2. **Never add a form the target doesn't have.** German does **not** use `few`. Any `few` clause in `de/*.json` is a bug and must be removed.
3. In single-form locales (ja/ko/zh/tr/vi/id), emit `one` + `other` with identical content so tooling and linters stay consistent.
4. The `other` case is always the fallback — it must render a grammatically complete sentence on its own.
Examples:
```json
// ✅ Correct — Russian (four forms; `other` fires for non-integer counts and takes genitive singular)
"members": "{count, plural, one {# участник} few {# участника} many {# участников} other {# участника}}"
// ✅ Correct — Polish (four forms)
"items": "{count, plural, one {# element} few {# elementy} many {# elementów} other {# elementu}}"
// ✅ Correct — Romanian (three forms)
"days": "{count, plural, one {# zi} few {# zile} other {# de zile}}"
// ✅ Correct — French (three forms; `many` covers 1 000 000, 2 000 000, …)
"members": "{count, plural, one {# membre} many {# membres} other {# membres}}"
// ✅ Correct — Japanese (single form, both keywords emitted)
"label": "{count, plural, one {サイクル} other {サイクル}}"
// ❌ Wrong — German with spurious `few`
"label": "{count, plural, one {Zyklus} few {Zyklen} other {Zyklen}}"
// ❌ Wrong — Russian missing `few` and `many`
"label": "{count, plural, one {Цикл} other {Циклы}}"
```
## Placeholders, HTML, Markdown (preservation checklist)
For every translated string, verify before saving:
- [ ] Identical set of `{variables}` in source and target (character-for-character — `{userName}` is not `{username}`)
- [ ] Identical HTML/JSX tags with identical attributes and nesting
- [ ] Identical i18next `Trans` numbered tags (`<0>…</0>`, `<1>…</1>` — never renumber)
- [ ] Identical Markdown syntax (`**bold**`, `` `code` ``, `[link text](url)`)
- [ ] Identical `\n` positions (layout relies on these)
- [ ] No additions (no extra explanatory words), no omissions (no collapsed clauses)
Variables **may be repositioned** for grammatical fit:
```
en: "Welcome, {name}! You have {count} new work items."
fr: "Bienvenue, {name} ! Vous avez {count} nouveaux work items."
ja: "{name}さん、ようこそ。{count}件の新しい作業項目があります。"
```
## Per-Locale Punctuation & Spacing
Apply in every translated string. These are MQM "Locale Conventions" violations when missed — a measurable quality defect.
Table below covers locales currently in the repo; add a row when you add a locale.
| Locale | Key rules |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **fr** | Narrow NBSP (U+202F, fallback U+00A0) **before** `:`, `;`, `?`, `!`, `%`, `»` and **after** `«`. Primary quotes `« »` (with NBSPs), nested `"…"`. |
| **de** | Primary quotes `„…"` (low-high); **no** NBSP before punctuation. Avoid imperatives in system strings; prefer infinitive constructions ("Werkelement erstellen", not "Erstelle ein Werkelement"). |
| **es** | Inverted `¿…?` and `¡…!` at the start of questions/exclamations. Primary quotes `«…»` or `"…"`. |
| **it** | Primary quotes `«…»` or `"…"`. |
| **pt-BR** | Straight quotes `"…"`. |
| **pl** | Primary quotes `„…"`. |
| **cs / sk** | Primary quotes `„…"`. |
| **ro** | Primary quotes `„…"`. |
| **tr-TR / vi-VN / id** | Straight quotes `"…"`. Vietnamese diacritics are mandatory — never strip (no `bạn``ban`). |
| **ru / ua** | Primary `«…»`, nested `„…"`. |
| **ja** | Full-width `。、()!?・`. Primary quotes 「」, nested 『』. Ellipsis `…` (U+2026), typically doubled as `……`. |
| **ko** | Western `. , ? !`; straight quotes `"…"`. |
| **zh-CN** | Full-width `。,?!;:`. Primary `"…"`, nested `'…'`. Work titles `《…》`. Half-width space around embedded Latin tokens. |
| **zh-TW** | Full-width `。,?!;:`. Primary `「…」`, nested `『…』`. Work titles `《…》`. Half-width space around embedded Latin tokens. |
## Tone & Register (SaaS defaults)
Formality defaults for locales currently in the repo. Add a row for each new locale, consulting the Microsoft Style Guide for that locale as the default authority on product-UI register.
| Locale | Default "you" | Notes |
| ------- | ---------------------------------------------------------------------------- | ------------------------------------------------ |
| fr | **vous** | Never "tu" in product UI |
| es | **usted** + **ustedes** | Neutral Spanish — no "vosotros", no "tú" |
| it | **Lei** (formal third-person) | B2B/enterprise convention |
| de | **Sie** | Use infinitive constructions for system messages |
| pt-BR | **você** | Semi-formal default; never "tu" |
| pl | **Pan/Pani** + 3rd-person, or impersonal | Never "ty" |
| cs / sk | **Vy** (formal, 3rd-person plural) | Never "ty" |
| ro | **dumneavoastră** (formal) | Or impersonal |
| tr-TR | **siz** + `-iniz` endings | Never "sen" |
| vi-VN | **bạn** (safe neutral) | Never "mày/tao"; consistency > variety |
| id | **Anda** (always capitalized) | Never "kamu" in product UI |
| ja | **です・ます体** | Never plain form; avoid 尊敬語・謙譲語 |
| ko | **합니다체** for system/errors; **해요체** acceptable for onboarding | Match surrounding file |
| zh-CN | **您** | B2B convention; never 你 |
| zh-TW | **您** | B2B convention; never 你 |
| ru | **Вы** (cap.) for singular direct address, **вы** (lower) for plural/general | Context-dependent |
| ua | **ви** (lowercase, modern convention) | Capitalized Ви only for very formal |
General writing rules (apply across all locales):
- Sentence case in buttons, headings, tooltips — not Title Case. Only capitalize proper nouns.
- No exclamation marks except in genuine celebrations (first success, milestone).
- No emoji in UI copy.
- No slang, idioms, humor, cultural references — they don't translate.
- Consistent terminology — if "work item" is used once, never switch to "issue" / "ticket" mid-flow.
- Active voice, present tense.
- Write out abbreviations on first use if not on the DNT list.
## Text Expansion Budget
Design strings knowing translations will grow. Typical expansion vs. English for locales currently in the repo; lookup values from Andiamo / Eriksen / W3C tables when adding a locale.
| Locale | Expansion | Implication |
| ------------ | ------------------------------------------------- | ----------------------------------------------- |
| de | +1035%, single words up to +180% | Reserve generous space on buttons, tabs, labels |
| fr | +1525% | |
| es | +1530% | |
| it | +1025% | |
| pt-BR | +1530% | |
| pl | +2030% | |
| cs, sk | +1020% | |
| ro | +1525% | |
| ru, ua | +15% typical, spikes to +30% | |
| tr-TR | +1030% (agglutination) | |
| vi-VN | +3040% | Diacritic-heavy, many small words |
| id | +1020% | |
| ja | 10 to 55% character count (similar pixel width) | |
| ko | 10 to 15% | |
| zh-CN, zh-TW | 40% character count (≈2× char width) | |
Rule of thumb: any UI surface must absorb **+35%** without truncation. If a string is length-capped, add a comment or context in the source.
## Numbers, Dates, Currency, Units
**Never hard-code formats.** Use ICU skeletons so the runtime localizes automatically:
```json
"updated_on": "Updated {date, date, medium}",
"percent_done": "{ratio, number, percent} complete",
"item_count": "{count, number} items"
```
Never write `"{date}MM/DD/YYYY"` in the source — that guarantees a locale bug.
Decimal/thousands separators (runtime-handled):
- en: `1,234.56` — de/it/es/pt-BR/ru/ua/pl/cs/sk/tr-TR: `1.234,56` or `1 234,56` — fr: `1 234,56` (thin space) — ja/ko/zh: `1,234.56`.
Date formats (runtime-handled):
- en-US: `MM/DD/YYYY` — most of Europe: `DD.MM.YYYY` — fr: `DD/MM/YYYY` — ja: `YYYY/MM/DD` or `YYYY年MM月DD日` — zh: `YYYY年MM月DD日` — ko: `YYYY년 MM월 DD일`.
Currency: prefer ISO codes (`USD`, `EUR`, `INR`) in dense UI; localize symbol position via ICU. Units: keep the system (metric/imperial) a product decision, localized consistently.
## AI Translation Workflow
The repo has no machine-readable translation-status field today (no sidecar, no `__meta`, no per-key flag) — so "preserve human-reviewed strings" must be enforced via git history, not status metadata. The disciplines below are what actually fires:
1. **Inject the DNT glossary into every AI translation prompt** — both approved targets and forbidden renderings. LLMs do not natively honor glossaries; they obey them only when prompted.
2. **Prompt the variant explicitly**: `target=zh-CN` vs `target=zh-TW`, `target=pt-BR` vs `target=pt-PT`. Auto-detect calls blend them and produce mixed vocabulary.
3. **Pass 25 sentences of real context** per string (surrounding UI, screen, flow). Keep DNT instructions out of context — route them through the glossary/system prompt channel.
4. **Hallucination check**: AI output may add or drop content. Diff placeholder/tag inventory before and after; any mismatch is a reject.
5. **On re-translation, preserve human-touched lines.** Before overwriting any value in an existing locale file, run `git log -- <file>` (or `git blame -L <line>,<line>`) on the key. If the most recent non-bot, non-mass-rewrite commit was authored by a human, treat that line as human-reviewed and keep it. Only overwrite lines whose last edit was a machine sweep or a copy-from-en migration.
6. **Variants by language-resource tier**: expect more review iterations for id, vi-VN, ua, ro (lower-resource LLMs) than for de, fr, es, ja (higher-resource).
### Per-string review rubric (MQM-aligned)
When reviewing a translation — yours or an AI's — score against these categories and reject if any Major issue is present:
1. **Terminology** — DNT violation, inconsistent with the glossary above, or inconsistent with past strings in the same namespace
2. **Accuracy** — Mistranslation, addition (invented content), omission (dropped content), wrong variant (zh-CN vs zh-TW), hallucination
3. **Linguistic** — Grammar, punctuation (per-locale table), spelling, encoding (mojibake, half-width where full-width required)
4. **Style / Register** — Informal pronoun used where formal is required; inconsistent tone; idiom/cultural reference
5. **Locale conventions** — Date / number / currency format hard-coded; decimal separator wrong; keyboard key translated
6. **Markup** — Placeholder changed, HTML tag dropped/modified, Markdown broken, `Trans` numbered fragment renumbered
7. **Plural** — Missing required CLDR form; spurious form the language doesn't have; `#` or `=0` dropped
## Workflow
### Add a new key
1. Add to `packages/i18n/src/locales/en/<namespace>.json` first. (If the namespace file does not yet exist, see **Add a new namespace** below — that case has extra steps.)
2. Use in the component via `t("my.new_key")`.
3. Translate into every target locale present in `src/locales/` — one file at a time. **Do not** copy the English value into the non-English files as a shortcut — `sync-check` treats presence as synced and will silently ship English copy to every locale.
4. Run `pnpm --filter @plane/i18n run generate:types` (or let the build do it).
5. Run `pnpm --filter @plane/i18n run sync:check` — expect `0 missing, 0 stale, 0 collisions`.
6. Spot-check one non-Latin locale manually for punctuation/plural correctness.
### Add a new namespace
A "namespace" is a top-level JSON file (e.g. `common.json`, `auth.json`, `epic.json`). The set of valid namespace names is a hard-coded const array — adding a JSON file alone is not enough; the runtime will not load it.
1. Add the new namespace name to the `NAMESPACES` array in `packages/i18n/src/constants/namespaces.ts`. Keep alphabetical order with the existing entries.
2. Create `<namespace>.json` in **every** locale directory under `packages/i18n/src/locales/` — start with `en/<namespace>.json` (the source of truth), then create the file in all 18 target locales. An empty `{}` is fine for the targets at this step; `sync-check` will report missing keys as you add them in step 4.
3. Add at least one key in `en/<namespace>.json` so the namespace has content.
4. Translate every key from `en/<namespace>.json` into each target locale — apply every rule in this skill.
5. Run `pnpm --filter @plane/i18n run generate:types` to regenerate the `TTranslationKeys` union so component-level `t()` calls type-check.
6. Run `pnpm --filter @plane/i18n run sync:check` — expect `0 missing, 0 stale, 0 collisions`.
7. Use the new namespace from a component via the namespace-prefixed key (e.g. `t("<namespace>.my_key")`) and spot-check the rendered UI in at least one non-Latin locale.
### Update an English value
The meaning changed — every target is now stale even if the key still exists. `sync-check` will **not** flag this.
1. Edit `en/<namespace>.json`.
2. Update the same key in every target locale in the same PR.
3. If a locale has no native speaker available in the PR, mark the string status as `machine_translated` (in PR description or commit message) and open a follow-up for native review.
### Add a new language
1. `cp -r packages/i18n/src/locales/en packages/i18n/src/locales/<xx>`
2. Translate every file, applying every rule in this skill.
3. Register in `packages/i18n/src/constants/language.ts` (`SUPPORTED_LANGUAGES`) and `packages/i18n/src/types/language.ts` (`TLanguage`).
4. Run `sync:check` — must show 100% coverage before merging.
5. Before merging, **pseudolocalize**: temporarily replace the new locale's values with bracketed expanded forms (`"Plane" → "[Plàññéé——]"`) in a local build and click through the UI. Catches truncation, unextracted strings, and layout bugs before real users see them.
6. If the new locale isn't yet covered by this skill (script, plural rules, punctuation, register), follow **Adding a locale not documented here** below and update this file in the same PR.
### Commands
```bash
# Regenerate the TTranslationKeys union (auto on build)
pnpm --filter @plane/i18n run generate:types
# Report drift
pnpm --filter @plane/i18n run sync:check
# Same, exit 1 on drift (for CI)
pnpm --filter @plane/i18n run check:sync
```
## Quick Reference
| Question | Answer |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Translate "Plane" / "Plane AI" / "Power K" / "PQL"? | Never. Latin, every locale. |
| Translate "Sticky" / "Stickies" / "Intake"? | Never. Plane brand marks. Latin, every locale. |
| Translate "Active Cycles"? | Never as a unit (it's a feature page name). Lowercase generic "active cycles" in prose translates normally per the glossary. |
| Translate "Pro" / "Business" / "Enterprise"? | Never. Plan tier names. Latin, every locale. |
| Translate "Cycle" / "Module" / "Epic" / "Page"? | **Yes** — use the per-locale form from the translation glossary (zh-CN 周期/模块/史诗/页面, ja サイクル/モジュール/エピック/ページ, de Zyklus/Modul/Epic/Seite, etc.). |
| Translate "GitHub" / "Slack" / third-party brands? | Never. Latin, every locale. |
| Translate a variable name `{name}`? | Never. Preserve exactly. |
| Translate `<0>…</0>`? | Translate only the inside text. Never renumber. |
| Russian plural forms? | `one / few / many / other` — four forms mandatory; case-correct per CLDR. |
| German plural forms? | `one / other` — never `few`. |
| French plural forms? | `one / many / other``many` covers 1M+. |
| CJK plural forms? | `one / other` (single-form) — still emit both. |
| Informal "you" in de/fr/ru/ja? | Never in product UI. |
| Chinese + embedded "GitHub"? | `使用 GitHub 登录` — half-width space around Latin **brand** tokens. Feature nouns translate (`创建周期`, no space because no Latin). |
| Japanese "user"? | ユーザー with long-vowel ー, not ユーザ. |
| Hard-code date formats? | Never. Use ICU `{d, date, medium}`. |
| Copy English into non-English locale? | Never. Worse than leaving the key missing. |
## Common Mistakes (revert on sight)
- **Translating Plane brand marks** — `Plane → 飞机`, `Plane AI → AI 聊天`, `Power K → 命令面板`, `Sticky → 便签` (Sticky is a brand, even though "sticky note" generally translates), `Intake → 收件箱`. Brand marks stay Latin.
- **Leaving feature nouns in Latin in non-Latin locales** — `创建 Cycle` (zh-CN), `Создать Cycle` (ru), `エピックを作成` is fine but `Epicを作成` is not. Use the per-locale form from the glossary.
- **Coining new feature-noun translations** — `Cycle → Cercle` (fr — invented; the natural cognate is the same `Cycle`), `Cycle → 循环` (zh-CN — non-glossary; use `周期`), `Epic → Saga` (es — non-glossary; use `Epic`). The glossary is the source of truth.
- **Missing `few` / `many` in Slavic languages** — Russian/Polish/Czech/Slovak/Ukrainian strings with only `one / other` are grammatically wrong for counts 24 and 5+. Fix in the same PR.
- **Wrong Slavic case form inside ICU plurals** — for ru/ua, `few` is genitive singular (`# Цикла`), not nominative plural (`# Циклы`). See the case-form table.
- **Inventing `few` in German, `many` in Spanish, etc.** — languages not in CLDR for that form. Remove the spurious keyword.
- **Translating `{count}` or renaming `{name}`** — ICU variables are lookup keys; rename breaks runtime substitution.
- **Dropping `<0>`/`<1>` numbering in `Trans`** — react-i18next matches by number; renumbering breaks the component.
- **Copying English as a translation** — `sync-check` passes, users see English. The fastest way to ship bad i18n.
- **No half-width space around Latin tokens in Chinese** — `使用GitHub登录` is a readability bug. The space rule applies around any remaining Latin token (brand mark), not around translated feature nouns.
- **Missing `ー` in Japanese katakana (`ユーザ` instead of `ユーザー`)** — inconsistent with the Microsoft style guide and the rest of the product.
- **Using informal pronouns** — `du/tu/ты/tú/you (informal)` breaks Plane's formal register in languages that distinguish.
- **Translating keyboard keys** — `Ctrl` stays `Ctrl`, never `Strg` or `コントロール`, because the physical key says `Ctrl`.
- **Hard-coded date/number strings** — `"Due on MM/DD/YYYY"` is a locale bug waiting to ship; use ICU formatters.
- **Translating `PQL`, `SSO`, `API`** — acronyms are DNT. The prose around them may be translated.
- **Translating plan tier names** — `Pro → Профессиональный`, `Business → 商业版`, `Enterprise → エンタープライズ`. Plan tiers stay Latin per industry convention (Notion/Slack/Linear/Asana).
- **Korean particle mismatch after Hangul transliteration** — `사이클를` is wrong (vowel-particle after consonant-ending noun); use `사이클을`. Same for 모듈, 에픽.
- **Vietnamese diacritic stripping** — `bạn` is not `ban`; diacritics are mandatory.
- **Lowercase `anda` in Indonesian** — `Anda` is always capitalized in product UI.
- **Mixing zh-CN and zh-TW vocabulary** — `视频``影片`, `软件``軟體`. Never copy-paste between the two directories.
## Red Flags — Stop and Revert
You see yourself about to do any of the following → stop, delete, restart this string:
- Replacing `Plane`, `Plane AI`, `Power K`, `PQL`, `Active Cycles`, `Sticky`, `Stickies`, `Intake`, `Pro`, `Business`, `Enterprise`, or any third-party brand (`GitHub`, `Slack`, etc.) with a translated form.
- Leaving `Cycle` / `Module` / `Epic` / `Page` in Latin in a non-Latin locale (zh-CN, zh-TW, ja, ko, ru, ua) — these are common nouns and must be translated per the glossary.
- Coining a feature-noun translation that's not in the glossary (`Cycle → 循环`, `Cycle → Cercle`, `Epic → Saga`).
- Using a vowel-form Korean particle (을/은/이/과 vs. 를/는/가/와) that doesn't agree with the noun's final character.
- Pasting the English value into a non-English file.
- Renaming or removing a `{variable}`, `<tag>`, numbered `<0>`, or Markdown marker.
- Dropping `few` or `many` from a Slavic-language plural, or adding `few` to German.
- Using nominative plural for `few` in ru/ua plural blocks (it should be genitive singular per CLDR).
- Editing one locale file and deciding to "do the others later" without updating the PR description.
- Using `du`, `tu`, `ты`, `tú`, plain-form Japanese, or 반말 Korean in any product string.
- Stripping diacritics from Vietnamese, lowercasing `Anda` in Indonesian.
- Hard-coding a date or number format.
## Rationalizations — Use Reality Column Instead
| Excuse | Reality |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| "Keeping `Cycle` Latin in zh-CN keeps it consistent with English docs." | That's a minority position; Microsoft, Apple, Mozilla, Notion, Atlassian, Slack all translate. Monolingual zh users can't read Latin words; the glossary form is the standard. |
| "史诗 is the established Chinese word for Epic, but Latin reads more brand-y." | Brand-y is the wrong goal for a feature noun. The glossary form (`史诗` for zh-CN, `Эпик` for ru) is what users in those locales expect from a SaaS product. |
| "Our Russian users understand `Cycle` Latin." | They understand it; they don't expect to encounter it. The glossary form (`Цикл`) is what every other Russian SaaS product uses for the same concept. |
| "I'll just transliterate the Plane brand name into Cyrillic for accessibility." | Brand marks stay Latin in every locale. `Плейн` is wrong; `Plane` is right. |
| "Plan tier names should be translated since they're plain words." | Industry standard (Notion, Slack, Linear, Asana, GitHub) is to keep `Pro`, `Business`, `Enterprise` Latin for marketing consistency. Don't translate. |
| "This string is internal / rarely seen, good-enough is fine." | Every string is someone's main screen. Rules are cheap to follow; consistency compounds. |
| "`sync-check` passed, I'm done." | `sync-check` only compares presence, not value quality. Green does not mean translated. |
| "I'll fix the missing plural forms later." | Missing `few` in Russian renders the wrong word for counts 24 right now in production. Same PR. |
| "The old file already used Latin Cycle everywhere — stay consistent." | Consistency with a bug is still a bug. Migrate occurrences you touch; open a sweep PR for the rest. |
| "AI said this was the right Japanese word for Cycle." | AI does not know our glossary unless you inject it. Check against the glossary table above; the glossary wins. |
| "The German translation is a bit long but still fits on my screen." | It won't fit on every user's screen. Design for +35% expansion; test in a narrow viewport. |
| "I renamed `{userName}` to `{nomeUtente}` so the Italian reads naturally." | Variables are code. The runtime has no `{nomeUtente}` in scope — it renders as literal text. Revert. |
| "Capitalizing `Anda` / `您` / `Sie` looks over-formal." | It's Plane's register in those languages. Deviating breaks brand voice across the product. |
## Adding a locale not documented here
When you add a language whose script, plural rules, punctuation, or register defaults aren't covered above:
1. **Plural rules** — Look up the target language in the Unicode CLDR Language Plural Rules chart. Note every required keyword (`zero`, `one`, `two`, `few`, `many`, `other`). Add a row to the CLDR table above.
2. **Script & transliteration** — Identify the script family. Latin → preserve DNT verbatim. Non-Latin → decide per-script: transliterate common-noun feature names phonetically (Cyrillic / Devanagari / Greek / Thai), keep brand marks in Latin. For RTL scripts (Arabic, Hebrew, Persian, Urdu) set `dir="rtl"` at the root and ensure Latin tokens remain LTR inside RTL context; preserve `&rlm;`/`&lrm;` markers if present.
3. **Punctuation & spacing** — Consult the Microsoft Style Guide for the locale (canonical authority on SaaS-style product punctuation). Add a row to the Per-Locale Punctuation & Spacing table.
4. **Register** — Default to the formal "you" and whatever honorific/polite register the Microsoft guide prescribes for B2B SaaS. Add a row to the Tone & Register table.
5. **Text expansion** — Look up typical expansion in Andiamo / Eriksen / W3C text-size tables; add a row to the Text Expansion Budget.
6. **DNT glossary** — Add a column (or row entries) for the new locale across the DNT tables. Specify forbidden literal translations (the words an LLM would produce if uninstructed). Example: if adding Arabic, the forbidden rendering for `Plane` includes `طائرة`; for `Epic` includes `ملحمة`; for `Sticky` includes `ملاحظة`.
7. **Commit this file in the same PR** that introduces the locale. The skill must stay ahead of the codebase — empty per-locale rows guarantee inconsistent translations in future PRs.
Canonical references to consult:
- **Unicode CLDR** — plural categories, number/date formats, collation.
- **Microsoft Style Guides** — per-locale register, punctuation, abbreviation handling, trademark rules (90+ locales covered).
- **Mozilla L10n Style Guides** — additional per-locale typography and brand-preservation rules.
- **W3C i18n** — RTL/bidi, placeholder handling, UTF-8, escapes, text expansion.
## References
- Microsoft Localization Style Guides — per-locale definitive source, formal-register defaults
- Unicode CLDR Plural Rules — canonical per-locale plural category list
- Mozilla L10n Style Guides — trademark/brand preservation rules, per-locale typography
- W3C i18n Quick Tips — placeholder handling, text expansion, UTF-8 and escapes
- Google developer style guide "Writing for a global audience" — source-string best practices
- MQM (Multidimensional Quality Metrics) — review rubric used in this skill's `Per-string review rubric` section
- Microsoft AI/LLMs for translation guidance — glossary injection, variant prompting, human-review gates
+7
View File
@@ -0,0 +1,7 @@
[codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = .git*,*.svg,i18n,*-lock.yaml,*.css,.codespellrc,migrations,*.js,*.map,*.mjs
check-hidden = true
# ignore all CamelCase and camelCase
ignore-regex = \b[A-Za-z][a-z]+[A-Z][a-zA-Z]+\b
ignore-words-list = tread
+1 -1
View File
@@ -1,6 +1,6 @@
contact_links:
- name: Help and support
about: Reach out to us on our Forum or GitHub discussions.
about: Reach out to us on our Discord server or GitHub discussions.
- name: Dedicated support
url: mailto:support@plane.so
about: Write to us if you'd like dedicated support using Plane
+14 -15
View File
@@ -134,7 +134,7 @@ jobs:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v6
uses: actions/checkout@v4
branch_build_push_admin:
name: Build-Push Admin Docker Image
@@ -142,7 +142,7 @@ jobs:
needs: [branch_build_setup]
steps:
- name: Admin Build and Push
uses: makeplane/actions/build-push@v1.4.0
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -164,7 +164,7 @@ jobs:
needs: [branch_build_setup]
steps:
- name: Web Build and Push
uses: makeplane/actions/build-push@v1.4.0
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -186,7 +186,7 @@ jobs:
needs: [branch_build_setup]
steps:
- name: Space Build and Push
uses: makeplane/actions/build-push@v1.4.0
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -208,7 +208,7 @@ jobs:
needs: [branch_build_setup]
steps:
- name: Live Build and Push
uses: makeplane/actions/build-push@v1.4.0
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -230,7 +230,7 @@ jobs:
needs: [branch_build_setup]
steps:
- name: Backend Build and Push
uses: makeplane/actions/build-push@v1.4.0
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -252,7 +252,7 @@ jobs:
needs: [branch_build_setup]
steps:
- name: Proxy Build and Push
uses: makeplane/actions/build-push@v1.4.0
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -282,7 +282,7 @@ jobs:
- branch_build_push_proxy
steps:
- name: Checkout Files
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Prepare AIO Assets
id: prepare_aio_assets
@@ -298,13 +298,13 @@ jobs:
echo "AIO_BUILD_VERSION=${aio_version}" >> $GITHUB_OUTPUT
- name: Upload AIO Assets
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
path: ./deployments/aio/community/dist
name: aio-assets-dist
- name: AIO Build and Push
uses: makeplane/actions/build-push@v1.4.0
uses: makeplane/actions/build-push@v1.1.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -337,7 +337,7 @@ jobs:
- branch_build_push_proxy
steps:
- name: Checkout Files
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Update Assets
run: |
@@ -352,7 +352,7 @@ jobs:
# sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deployments/cli/community/variables.env
- name: Upload Assets
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: community-assets
path: |
@@ -381,7 +381,7 @@ jobs:
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Update Assets
run: |
@@ -391,13 +391,12 @@ jobs:
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2.6.1
uses: softprops/action-gh-release@v2.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ env.REL_VERSION }}
name: ${{ env.REL_VERSION }}
target_commitish: ${{ github.sha }}
draft: false
prerelease: ${{ env.IS_PRERELEASE }}
generate_release_notes: true
+2 -2
View File
@@ -10,13 +10,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
- name: Get PR Branch version
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
+27 -7
View File
@@ -16,27 +16,47 @@ jobs:
contents: read
security-events: write
env:
CODEQL_ACTION_FILE_COVERAGE_ON_PRS: "false"
strategy:
fail-fast: false
matrix:
language: ["python", "javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+25
View File
@@ -0,0 +1,25 @@
# Codespell configuration is within .codespellrc
---
name: Codespell
on:
push:
branches: [preview]
pull_request:
branches: [preview]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Annotate locations with typos
uses: codespell-project/codespell-problem-matcher@v1
- name: Codespell
uses: codespell-project/actions-codespell@v2
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: "1.22"
+6 -6
View File
@@ -48,7 +48,7 @@ jobs:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v6
uses: actions/checkout@v4
full_build_push:
runs-on: ubuntu-22.04
@@ -63,23 +63,23 @@ jobs:
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
steps:
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
with:
driver: ${{ env.BUILDX_DRIVER }}
version: ${{ env.BUILDX_VERSION }}
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Check out the repo
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v7.0.0
uses: docker/build-push-action@v6.9.0
with:
context: .
file: ./aio/Dockerfile-app
@@ -112,7 +112,7 @@ jobs:
sudo apt-get install -y python3-pip
pip3 install awscli
- name: Tailscale
uses: tailscale/github-action@v4
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
-51
View File
@@ -1,51 +0,0 @@
name: i18n sync check
on:
workflow_dispatch:
pull_request:
branches:
- "preview"
types:
- "opened"
- "synchronize"
- "reopened"
- "ready_for_review"
paths:
- "packages/i18n/**"
- ".github/workflows/i18n-sync-check.yml"
push:
branches:
- "preview"
paths:
- "packages/i18n/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
sync-check:
name: check:sync
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.event_name == 'push' || github.event.pull_request.draft == false
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "22.18.0"
- name: Enable Corepack and pnpm
run: corepack enable pnpm
- name: Run sync check
run: pnpm dlx tsx packages/i18n/scripts/sync-check.ts --ci
@@ -8,6 +8,8 @@ on:
types:
- "opened"
- "synchronize"
- "ready_for_review"
- "review_requested"
- "reopened"
concurrency:
@@ -44,7 +46,7 @@ jobs:
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
@@ -87,7 +89,7 @@ jobs:
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
@@ -95,7 +97,7 @@ jobs:
pnpm-store-${{ runner.os }}-
- name: Restore Turbo cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
@@ -110,59 +112,21 @@ jobs:
run: pnpm turbo run build --affected
- name: Save Turbo cache
uses: actions/cache/save@v5
uses: actions/cache/save@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
# Lint check - no build dependency, OxLint is a standalone Rust binary
check-lint:
name: check:lint
runs-on: ubuntu-latest
timeout-minutes: 10
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 }}
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@v5
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: Run check:lint
run: pnpm turbo run check:lint --affected
# Type check depends on build artifacts
check-types:
name: check:types
# 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 }}
@@ -185,7 +149,7 @@ jobs:
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
@@ -193,7 +157,7 @@ jobs:
pnpm-store-${{ runner.os }}-
- name: Restore Turbo cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
@@ -201,5 +165,5 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run check:types
run: pnpm turbo run check:types --affected
- name: Run ${{ matrix.task }}
run: pnpm turbo run ${{ matrix.task }} --affected
+52
View File
@@ -0,0 +1,52 @@
name: Create PR on Sync
on:
workflow_dispatch:
push:
branches:
- "sync/**"
env:
CURRENT_BRANCH: ${{ github.ref_name }}
TARGET_BRANCH: "preview" # The target branch that you would like to merge changes like develop
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows
ACCOUNT_USER_NAME: ${{ vars.ACCOUNT_USER_NAME }}
ACCOUNT_USER_EMAIL: ${{ vars.ACCOUNT_USER_EMAIL }}
jobs:
create_pull_request:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for all branches and tags
- name: Setup Git
run: |
git config user.name "$ACCOUNT_USER_NAME"
git config user.email "$ACCOUNT_USER_EMAIL"
- name: Setup GH CLI and Git Config
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh -y
- name: Create PR to Target Branch
run: |
# get all pull requests and check if there is already a PR
PR_EXISTS=$(gh pr list --base $TARGET_BRANCH --head $CURRENT_BRANCH --state open --json number | jq '.[] | .number')
if [ -n "$PR_EXISTS" ]; then
echo "Pull Request already exists: $PR_EXISTS"
else
echo "Creating new pull request"
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $CURRENT_BRANCH --title "${{ vars.SYNC_PR_TITLE }}" --body "")
echo "Pull Request created: $PR_URL"
fi
+44
View File
@@ -0,0 +1,44 @@
name: Sync Repositories
on:
workflow_dispatch:
push:
branches:
- preview
env:
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
jobs:
sync_changes:
runs-on: ubuntu-22.04
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
- name: Setup GH CLI
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh -y
- name: Push Changes to Target Repo
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
TARGET_REPO="${{ vars.SYNC_TARGET_REPO }}"
TARGET_BRANCH="${{ vars.SYNC_TARGET_BRANCH_NAME }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH
git remote add target-origin-a "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
git push target-origin-a $SOURCE_BRANCH:$TARGET_BRANCH
-7
View File
@@ -110,10 +110,3 @@ build/
.react-router/
temp/
scripts/
!packages/i18n/scripts/
# i18n auto-generated types (regenerated on every build)
packages/i18n/src/types/keys.generated.ts
# Local security notes (not for version control)
/security/
-53
View File
@@ -1,53 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "jsx-a11y", "import", "promise", "unicorn", "oxc"],
"categories": {
"correctness": "warn",
"suspicious": "warn",
"perf": "warn"
},
"env": {
"browser": true,
"node": true,
"es2024": true
},
"settings": {
"react": {
"version": "18.3"
},
"jsx-a11y": {
"polymorphicPropName": "as"
}
},
"ignorePatterns": [
".cache/**",
".next/**",
".react-router/**",
".storybook/**",
".turbo/**",
".vite/**",
"*.config.{js,mjs,cjs,ts}",
"build/**",
"coverage/**",
"dist/**",
"**/public/**",
"storybook-static/**"
],
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"unicorn/filename-case": "off",
"unicorn/no-null": "off",
"unicorn/prevent-abbreviations": "off",
"no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"ignoreRestSiblings": true
}
]
}
}
+6 -8
View File
@@ -1,11 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5",
"sortTailwindcss": {
"stylesheet": "packages/tailwind-config/index.css",
"functions": ["cn", "clsx", "cva"]
},
"$schema": "https://json.schemastore.org/prettierrc",
"overrides": [
{
"files": ["packages/codemods/**/*"],
@@ -13,5 +7,9 @@
"printWidth": 80
}
}
]
],
"plugins": ["@prettier/plugin-oxc"],
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
-10
View File
@@ -1,10 +0,0 @@
# Trivy ignore file
# Document the rationale for each suppressed finding.
# CVE-2026-30242: SSRF in Plane webhook URL serializer.
# False positive: Trivy matches our backend's distribution name "Plane" +
# version 0.24.0 against the makeplane/plane CVE. The "fixed in 1.2.3" refers
# to the upstream public release version scheme, not this distribution's
# pyproject.toml version - the SSRF mitigation has been in place for the
# applicable webhook validation code path.
CVE-2026-30242
+3 -15
View File
@@ -5,7 +5,7 @@
- `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` - OxLint across all packages
- `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
@@ -15,22 +15,10 @@
- **Imports**: Use `workspace:*` for internal packages, `catalog:` for external deps
- **TypeScript**: Strict mode enabled, all files must be typed
- **Formatting**: oxfmt, run `pnpm fix:format`
- **Linting**: OxLint with shared `.oxlintrc.json` config
- **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
## Backend tests (Docker)
The Django/pytest suite for `apps/api` runs in an isolated stack defined by `docker-compose-test.yml` at the repo root.
Prereq (once): `./setup.sh` — generates `apps/api/.env` from `.env.example`.
- Full suite: `docker compose -f docker-compose-test.yml up --build --abort-on-container-exit --exit-code-from api-tests`
- Subset: `docker compose -f docker-compose-test.yml run --rm api-tests pytest -m unit`
- Teardown: `docker compose -f docker-compose-test.yml down -v`
See `apps/api/tests/RUNNING_TESTS.md` for the full walkthrough and troubleshooting; see `apps/api/tests/TESTING_GUIDE.md` for test conventions and fixtures.
+1 -8
View File
@@ -1,8 +1 @@
.oxlintrc.json @sriramveeraghanta @lifeiscontent
.oxfmtrc.json @sriramveeraghanta @lifeiscontent
apps/api/ @dheeru0198 @pablohashescobar
apps/web/ @sriramveeraghanta
apps/space/ @sriramveeraghanta
apps/admin/ @sriramveeraghanta
apps/live/ @Palanikannan1437
deployments/ @mguptahub
eslint.config.mjs @lifeiscontent
+2 -2
View File
@@ -91,7 +91,7 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
- All features or bug fixes must be tested by one or more specs (unit-tests).
- We lint with [OxLint](https://oxc.rs/docs/guide/usage/linter) using the shared `.oxlintrc.json` and format with [oxfmt](https://oxc.rs/docs/guide/usage/formatter) using `.oxfmtrc.json`.
- 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
@@ -244,4 +244,4 @@ Happy translating! 🌍✨
## Need help? Questions and suggestions
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Forum](https://forum.plane.so).
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Discord Server](https://discord.com/invite/A92xrEGCge).
+12 -5
View File
@@ -7,10 +7,17 @@
</p>
<p align="center"><b>Modern project management for all teams</b></p>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
<img alt="Discord online members" src="https://img.shields.io/discord/1031547764020084846?color=5865F2&label=Discord&style=for-the-badge" />
</a>
<img alt="Commit activity per month" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
</p>
<p align="center">
<a href="https://plane.so/"><b>Website</b></a> •
<a href="https://forum.plane.so"><b>Forum</b></a> •
<a href="https://x.com/planepowers"><b>X</b></a> •
<a href="https://github.com/makeplane/plane/releases"><b>Releases</b></a> •
<a href="https://twitter.com/planepowers"><b>Twitter</b></a> •
<a href="https://docs.plane.so/"><b>Documentation</b></a>
</p>
@@ -26,7 +33,7 @@
Meet [Plane](https://plane.so/), an open-source project management tool to track issues, run ~sprints~ cycles, and manage product roadmaps without the chaos of managing the tool itself. 🧘‍♀️
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Forum](https://forum.plane.so) or raise a GitHub issue. We read everything and respond to most.
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Discord](https://discord.com/invite/A92xrEGCge) or raise a GitHub issue. We read everything and respond to most.
## 🚀 Installation
@@ -129,7 +136,7 @@ Explore Plane's [product documentation](https://docs.plane.so/) and [developer d
## ❤️ Community
Join the Plane community on [GitHub Discussions](https://github.com/orgs/makeplane/discussions) and our [Forum](https://forum.plane.so). We follow a [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) in all our community channels.
Join the Plane community on [GitHub Discussions](https://github.com/orgs/makeplane/discussions) and our [Discord server](https://discord.com/invite/A92xrEGCge). We follow a [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) in all our community channels.
Feel free to ask questions, report bugs, participate in discussions, share ideas, request features, or showcase your projects. Wed love to hear from you!
@@ -145,7 +152,7 @@ There are many ways you can contribute to Plane:
- Report [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) or submit [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+).
- Review the [documentation](https://docs.plane.so/) and submit [pull requests](https://github.com/makeplane/docs) to improve it—whether it's fixing typos or adding new content.
- Talk or write about Plane or any other ecosystem integration and [let us know](https://forum.plane.so)!
- Talk or write about Plane or any other ecosystem integration and [let us know](https://discord.com/invite/A92xrEGCge)!
- Show your support by upvoting [popular feature requests](https://github.com/makeplane/plane/issues).
Please read [CONTRIBUTING.md](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md) for details on the process for submitting pull requests to us.
+2 -4
View File
@@ -4,7 +4,7 @@ WORKDIR /app
ENV TURBO_TELEMETRY_DISABLED=1
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PNPM_HOME/bin:$PATH"
ENV PATH="$PNPM_HOME:$PATH"
ENV CI=1
RUN corepack enable pnpm
@@ -13,7 +13,7 @@ RUN corepack enable pnpm
FROM base AS builder
RUN pnpm add -g turbo@2.9.14
RUN pnpm add -g turbo@2.6.3
COPY . .
@@ -77,8 +77,6 @@ RUN pnpm turbo run build --filter=admin
FROM nginx:1.29-alpine AS production
RUN apk update && apk upgrade --no-cache && rm -rf /var/cache/apk/*
COPY apps/admin/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=installer /app/apps/admin/build/client /usr/share/nginx/html/god-mode
+3 -3
View File
@@ -120,16 +120,16 @@ export function InstanceAIForm(props: IInstanceAIForm) {
</div>
</div>
<div className="flex flex-col items-start gap-4">
<div className="flex flex-col gap-4 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-1.5 rounded-sm border border-accent-subtle bg-accent-subtle px-4 py-2 text-caption-sm-regular text-accent-secondary">
<div className="relative inline-flex items-center gap-1.5 rounded-sm border border-accent-subtle bg-accent-subtle px-4 py-2 text-caption-sm-regular text-accent-secondary ">
<Lightbulb className="size-4" />
<div>
If you have a preferred AI models vendor, please get in{" "}
<a className="font-medium underline" href="https://plane.so/contact">
<a className="underline font-medium" href="https://plane.so/contact">
touch with us.
</a>
</div>
+1 -1
View File
@@ -34,7 +34,7 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="40%" />
<div className="grid w-2/3 grid-cols-2 gap-x-8 gap-y-4">
<div className="w-2/3 grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</div>
@@ -176,8 +176,8 @@ export function InstanceGiteaConfigForm(props: Props) {
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">Gitea-provided details for Plane</div>
{GITEA_FORM_FIELDS.map((field) => (
<ControllerInput
@@ -211,7 +211,7 @@ export function InstanceGiteaConfigForm(props: Props) {
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 pt-1.5 pb-4">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-1 rounded-lg">
<div className="pt-2 text-18 font-medium">Plane-provided details for Gitea</div>
{GITEA_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
@@ -197,8 +197,8 @@ export function InstanceGithubConfigForm(props: Props) {
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">GitHub-provided details for Plane</div>
{GITHUB_FORM_FIELDS.map((field) => (
<ControllerInput
@@ -231,24 +231,24 @@ export function InstanceGithubConfigForm(props: Props) {
</div>
</div>
</div>
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
<div className="pt-2 text-18 font-medium">Plane-provided details for GitHub</div>
<div className="flex flex-col gap-y-4">
{/* common service details */}
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 py-4">
<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} />
))}
</div>
{/* web service details */}
<div className="flex flex-col overflow-hidden rounded-lg">
<div className="flex items-center gap-x-3 bg-layer-3 px-6 py-3 text-11 font-medium text-secondary uppercase">
<Monitor className="h-3 w-3" />
<div className="flex flex-col rounded-lg overflow-hidden">
<div className="px-6 py-3 bg-layer-3 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary">
<Monitor className="w-3 h-3" />
Web
</div>
<div className="flex flex-col gap-y-4 bg-layer-1 px-6 py-4">
<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} />
))}
@@ -180,8 +180,8 @@ export function InstanceGitlabConfigForm(props: Props) {
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">GitLab-provided details for Plane</div>
{GITLAB_FORM_FIELDS.map((field) => (
<ControllerInput
@@ -215,7 +215,7 @@ export function InstanceGitlabConfigForm(props: Props) {
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-3 px-6 pt-1.5 pb-4">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-3 rounded-lg">
<div className="pt-2 text-18 font-medium">Plane-provided details for GitLab</div>
{GITLAB_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
@@ -185,8 +185,8 @@ export function InstanceGoogleConfigForm(props: Props) {
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">Google-provided details for Plane</div>
{GOOGLE_FORM_FIELDS.map((field) => (
<ControllerInput
@@ -219,24 +219,24 @@ export function InstanceGoogleConfigForm(props: Props) {
</div>
</div>
</div>
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
<div className="pt-2 text-18 font-medium">Plane-provided details for Google</div>
<div className="flex flex-col gap-y-4">
{/* common service details */}
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 py-4">
<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} />
))}
</div>
{/* web service details */}
<div className="flex flex-col overflow-hidden rounded-lg">
<div className="flex items-center gap-x-3 bg-layer-3 px-6 py-3 text-11 font-medium text-secondary uppercase">
<Monitor className="h-3 w-3" />
<div className="flex flex-col rounded-lg overflow-hidden">
<div className="px-6 py-3 bg-layer-3 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary">
<Monitor className="w-3 h-3" />
Web
</div>
<div className="flex flex-col gap-y-4 bg-layer-1 px-6 py-4">
<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} />
))}
@@ -117,11 +117,11 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
>
{formattedConfig ? (
<div className="space-y-3">
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
<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="pb-1 text-16 font-medium">Allow anyone to sign up even without an invite</div>
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
<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>
@@ -143,7 +143,7 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
</div>
</div>
</div>
<div className="text-lg pt-6 font-medium">Available authentication modes</div>
<div className="text-lg font-medium pt-6">Available authentication modes</div>
{authenticationModes.map((method) => (
<AuthenticationMethodCard
key={method.key}
@@ -179,7 +179,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
</CustomSelect>
</div>
</div>
<div className="my-6 flex flex-col gap-6 border-t border-subtle pt-4">
<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">
@@ -207,7 +207,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
</div>
</div>
</div>
<div className="flex max-w-4xl items-center gap-4 py-1">
<div className="flex max-w-4xl items-center py-1 gap-4">
<Button
variant="primary"
size="lg"
@@ -91,8 +91,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 w-full transform rounded-lg bg-surface-1 p-5 px-4 text-left shadow-raised-200 transition-all sm:max-w-xl">
<h3 className="text-16 leading-6 font-medium text-primary">
<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
@@ -121,7 +121,7 @@ export function SendTestEmailModal(props: Props) {
</div>
)}
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-13">{error}</div>}
<div className="mt-5 flex items-center justify-end gap-2">
<div className="flex items-center gap-2 justify-end mt-5">
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={2}>
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Cancel" : "Close"}
</Button>
@@ -16,6 +16,8 @@ import { Input, ToggleSwitch } from "@plane/ui";
import { ControllerInput } from "@/components/common/controller-input";
// hooks
import { useInstance } from "@/hooks/store";
// components
import { IntercomConfig } from "./intercom";
export interface IGeneralConfigurationForm {
instance: IInstance;
@@ -25,13 +27,14 @@ export interface IGeneralConfigurationForm {
export const GeneralConfigurationForm = observer(function GeneralConfigurationForm(props: IGeneralConfigurationForm) {
const { instance, instanceAdmins } = props;
// hooks
const { updateInstanceInfo } = useInstance();
const { instanceConfigurations, updateInstanceInfo, updateInstanceConfigurations } = useInstance();
// form data
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
watch,
} = useForm<Partial<IInstance>>({
defaultValues: {
instance_name: instance?.instance_name,
@@ -42,6 +45,17 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
const onSubmit = async (formData: Partial<IInstance>) => {
const payload: Partial<IInstance> = { ...formData };
// update the intercom configuration
const isIntercomEnabled =
instanceConfigurations?.find((config) => config.key === "IS_INTERCOM_ENABLED")?.value === "1";
if (!payload.is_telemetry_enabled && isIntercomEnabled) {
try {
await updateInstanceConfigurations({ IS_INTERCOM_ENABLED: "0" });
} catch (error) {
console.error(error);
}
}
await updateInstanceInfo(payload)
.then(() =>
setToast({
@@ -98,17 +112,18 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
</div>
<div className="space-y-6">
<div className="border-b border-subtle pb-1.5 text-16 font-medium text-primary">Telemetry</div>
<div className="text-16 font-medium text-primary pb-1.5 border-b border-subtle">Chat + telemetry</div>
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
<div className="flex items-center gap-14">
<div className="flex grow items-center gap-4">
<div className="grow flex items-center gap-4">
<div className="shrink-0">
<div className="flex size-11 items-center justify-center rounded-lg bg-layer-1">
<div className="flex items-center justify-center size-11 bg-layer-1 rounded-lg">
<Telescope className="size-5 text-tertiary" />
</div>
</div>
<div className="grow">
<div className="text-13 leading-5 font-medium text-primary">Let Plane collect anonymous usage data</div>
<div className="text-11 leading-5 font-regular text-tertiary">
<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
@@ -0,0 +1,86 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
import { MessageSquare } from "lucide-react";
import type { IFormattedInstanceConfiguration } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
// hooks
import { useInstance } from "@/hooks/store";
type TIntercomConfig = {
isTelemetryEnabled: boolean;
};
export const IntercomConfig = observer(function IntercomConfig(props: TIntercomConfig) {
const { isTelemetryEnabled } = props;
// hooks
const { instanceConfigurations, updateInstanceConfigurations, fetchInstanceConfigurations } = useInstance();
// states
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// derived values
const isIntercomEnabled = isTelemetryEnabled
? instanceConfigurations
? instanceConfigurations?.find((config) => config.key === "IS_INTERCOM_ENABLED")?.value === "1"
? true
: false
: undefined
: false;
const { isLoading } = useSWR(isTelemetryEnabled ? "INSTANCE_CONFIGURATIONS" : null, () =>
isTelemetryEnabled ? fetchInstanceConfigurations() : null
);
const initialLoader = isLoading && isIntercomEnabled === undefined;
const submitInstanceConfigurations = async (payload: Partial<IFormattedInstanceConfiguration>) => {
try {
await updateInstanceConfigurations(payload);
} catch (error) {
console.error(error);
} finally {
setIsSubmitting(false);
}
};
const enableIntercomConfig = () => {
void submitInstanceConfigurations({ IS_INTERCOM_ENABLED: isIntercomEnabled ? "0" : "1" });
};
return (
<>
<div className="flex items-center gap-14">
<div className="grow flex items-center gap-4">
<div className="shrink-0">
<div className="flex items-center justify-center size-11 bg-layer-1 rounded-lg">
<MessageSquare className="size-5 text-tertiary p-0.5" />
</div>
</div>
<div className="grow">
<div className="text-13 font-medium text-primary leading-5">Chat with us</div>
<div className="text-11 font-regular text-tertiary leading-5">
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
automatically.
</div>
</div>
<div className="ml-auto">
<ToggleSwitch
value={isIntercomEnabled ? true : false}
onChange={enableIntercomConfig}
size="sm"
disabled={!isTelemetryEnabled || isSubmitting || initialLoader}
/>
</div>
</div>
</div>
</>
);
});
+2 -2
View File
@@ -11,7 +11,7 @@ import { Outlet } from "react-router";
// components
import { AdminHeader } from "@/components/common/header";
import { LogoSpinner } from "@/components/common/logo-spinner";
import { NewUserPopup } from "@/components/common/new-user-popup";
import { NewUserPopup } from "@/components/new-user-popup";
// hooks
import { useUser } from "@/hooks/store";
// local components
@@ -42,7 +42,7 @@ function AdminLayout(_props: Route.ComponentProps) {
<AdminSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
<AdminHeader />
<div className="vertical-scrollbar scrollbar-md h-full w-full overflow-hidden overflow-y-scroll">
<div className="h-full w-full overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md">
<Outlet />
</div>
</main>
@@ -39,14 +39,14 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
const getSidebarMenuItems = () => (
<Menu.Items
className={cn(
"shadow-lg 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 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="truncate px-2 text-secondary">{currentUser?.email}</span>
<span className="px-2 text-secondary truncate">{currentUser?.email}</span>
</div>
<div className="py-2">
<Menu.Item
@@ -7,11 +7,11 @@
import { useState, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { HelpCircle, MessageSquare, MoveLeft } from "lucide-react";
import { HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
import { WEB_BASE_URL } from "@plane/constants";
// plane internal packages
import { GithubIcon, NewTabIcon, PageIcon } from "@plane/propel/icons";
import { DiscordIcon, GithubIcon, NewTabIcon, PageIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// hooks
@@ -25,9 +25,9 @@ const helpOptions = [
Icon: PageIcon,
},
{
name: "Join our Forum",
href: "https://forum.plane.so",
Icon: MessageSquare,
name: "Join our Discord",
href: "https://discord.com/invite/A92xrEGCge",
Icon: DiscordIcon,
},
{
name: "Report a bug",
@@ -50,9 +50,9 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
return (
<div
className={cn(
"flex h-14 w-full flex-shrink-0 items-center justify-between gap-1 self-baseline border-t border-subtle bg-surface-1 px-4",
"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",
{
"h-auto flex-col py-1.5": isSidebarCollapsed,
"flex-col h-auto py-1.5": isSidebarCollapsed,
}
)}
>
@@ -60,7 +60,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
<Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<a
href={redirectionLink}
className={`relative flex items-center gap-1 rounded-sm bg-layer-1 px-2 py-1 text-body-xs-medium whitespace-nowrap text-secondary`}
className={`relative px-2 py-1 flex items-center gap-1 rounded-sm bg-layer-1 text-body-xs-medium text-secondary whitespace-nowrap`}
>
<NewTabIcon width={14} height={14} />
{!isSidebarCollapsed && "Redirect to Plane"}
@@ -101,9 +101,9 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
leaveTo="transform opacity-0 scale-95"
>
<div
className={`absolute bottom-2 z-[15] min-w-[10rem] ${
className={`absolute bottom-2 min-w-[10rem] z-[15] ${
isSidebarCollapsed ? "left-full" : "-left-[75px]"
} divide-y divide-subtle-1 rounded-sm bg-surface-1 p-1 whitespace-nowrap shadow-raised-100`}
} 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">
@@ -134,7 +134,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
);
})}
</div>
<div className="px-2 pt-2 pb-1 text-10">Version: v{instance?.current_version}</div>
<div className="px-2 pb-1 pt-2 text-10">Version: v{instance?.current_version}</div>
</div>
</Transition>
</div>
@@ -29,7 +29,7 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
};
return (
<div className="vertical-scrollbar flex scrollbar-sm h-full w-full flex-col gap-2.5 overflow-y-scroll px-4 py-4">
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 py-4">
{sidebarMenu.map((item, index) => {
const isActive = item.href === pathName || pathName?.includes(item.href);
return (
@@ -38,9 +38,9 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
<div
className={cn(
"group flex w-full items-center gap-3 rounded-md px-3 py-2 transition-colors outline-none",
"group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none transition-colors",
{
"!bg-layer-transparent-active text-primary": isActive,
"text-primary !bg-layer-transparent-active": isActive,
"text-secondary hover:bg-layer-transparent-hover active:bg-layer-transparent-active": !isActive,
},
isSidebarCollapsed ? "justify-center" : "w-[260px]"
@@ -48,7 +48,7 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
>
{<item.Icon className="h-4 w-4 flex-shrink-0" />}
{!isSidebarCollapsed && (
<div className="w-full">
<div className="w-full ">
<div className={cn(`text-body-xs-medium transition-colors`)}>{item.name}</div>
<div className={cn(`text-caption-sm-regular transition-colors`)}>{item.description}</div>
</div>
+7 -1
View File
@@ -44,7 +44,13 @@ export const AdminSidebar = observer(function AdminSidebar() {
return (
<div
className={`fixed 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 md:relative ${isSidebarCollapsed ? "-ml-[290px]" : ""} sm:${isSidebarCollapsed ? "-ml-[290px]" : ""} md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} `}
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]" : ""}
md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"}
lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"}
`}
>
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
<AdminSidebarDropdown />
@@ -14,7 +14,6 @@ import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { InstanceWorkspaceService } from "@plane/services";
import type { IWorkspace } from "@plane/types";
import { validateSlug, validateWorkspaceName } from "@plane/utils";
// components
import { CustomSelect, Input } from "@plane/ui";
// hooks
@@ -97,7 +96,14 @@ export function WorkspaceCreateForm() {
control={control}
name="name"
rules={{
validate: (value) => validateWorkspaceName(value, true),
required: "This is a required field.",
validate: (value) =>
/^[\w\s-]*$/.test(value) ||
`Workspaces names can contain only (" "), ( - ), ( _ ) and alphanumeric characters.`,
maxLength: {
value: 80,
message: "Limit your name to 80 characters.",
},
}}
render={({ field: { value, ref, onChange } }) => (
<Input
@@ -123,13 +129,17 @@ export function WorkspaceCreateForm() {
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Set your workspace&apos;s URL</h4>
<div className="flex w-full items-center gap-0.5 rounded-md border-[0.5px] border-subtle px-3">
<span className="text-13 whitespace-nowrap text-secondary">{workspaceBaseURL}</span>
<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"
rules={{
validate: (value) => validateSlug(value),
required: "The URL is a required field.",
maxLength: {
value: 48,
message: "Limit your URL to 48 characters.",
},
}}
render={({ field: { onChange, value, ref } }) => (
<Input
@@ -188,7 +198,7 @@ export function WorkspaceCreateForm() {
</div>
</div>
</div>
<div className="flex max-w-4xl items-center gap-4 py-1">
<div className="flex max-w-4xl items-center py-1 gap-4">
<Button
variant="primary"
size="lg"
@@ -83,11 +83,11 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
>
<div className="space-y-3">
{formattedConfig ? (
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
<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="pb-1 text-16 font-medium">Prevent anyone else from creating a workspace.</div>
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
<div className="text-16 font-medium pb-1">Prevent anyone else from creating a workspace.</div>
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
Toggling this on will let only you create workspaces. You will have to invite users to new workspaces.
</div>
</div>
@@ -116,15 +116,15 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
)}
{workspaceLoader !== "init-loader" ? (
<>
<div className="flex items-center justify-between gap-2 pt-6">
<div className="pt-6 flex items-center justify-between gap-2">
<div className="flex flex-col items-start gap-x-2">
<div className="flex items-center gap-2 text-16 font-medium">
All workspaces on this instance <span className="text-tertiary"> {workspaceIds.length}</span>
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
<LoaderIcon className="h-4 w-4 animate-spin" />
<LoaderIcon className="w-4 h-4 animate-spin" />
)}
</div>
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
You can&apos;t yet delete workspaces and you can only go to the workspace if you are an Admin or a
Member.
</div>
@@ -149,7 +149,7 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
disabled={workspaceLoader === "pagination"}
>
Load more
{workspaceLoader === "pagination" && <LoaderIcon className="h-3 w-3 animate-spin" />}
{workspaceLoader === "pagination" && <LoaderIcon className="w-3 h-3 animate-spin" />}
</Button>
</div>
)}
+4 -4
View File
@@ -20,16 +20,16 @@ export function AuthBanner(props: TAuthBanner) {
if (!bannerData) return <></>;
return (
<div className="relative flex items-center gap-2 rounded-md border border-accent-strong/50 bg-accent-primary/10 p-2">
<div className="relative flex h-4 w-4 flex-shrink-0 items-center justify-center">
<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-accent-primary" />
</div>
<div className="w-full text-13 font-medium text-accent-primary">{bannerData?.message}</div>
<div
className="relative ml-auto flex h-6 w-6 cursor-pointer items-center justify-center rounded-xs text-accent-primary transition-all hover:bg-accent-primary/20"
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"
onClick={() => handleBannerData && handleBannerData(undefined)}
>
<CloseIcon className="h-4 w-4 flex-shrink-0" />
<CloseIcon className="w-4 h-4 flex-shrink-0" />
</div>
</div>
);
+1 -1
View File
@@ -9,7 +9,7 @@ import { PlaneLockup } from "@plane/propel/icons";
export function AuthHeader() {
return (
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6">
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
<Link href="/">
<PlaneLockup height={20} width={95} className="text-primary" />
</Link>
+2 -2
View File
@@ -50,7 +50,7 @@ const errorCodeMessages: {
message: () => (
<div>
Admin user already exists.&nbsp;
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
@@ -62,7 +62,7 @@ const errorCodeMessages: {
message: () => (
<div>
Admin user does not exist.&nbsp;
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
+1 -1
View File
@@ -22,7 +22,7 @@ function RootLayout() {
}, [replace, isUserLoggedIn]);
return (
<div className="relative z-10 flex h-screen w-screen flex-col items-center overflow-hidden overflow-y-auto bg-surface-1 px-8 pt-6 pb-10">
<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>
);
+1 -1
View File
@@ -22,7 +22,7 @@ function HomePage() {
// if instance is not fetched, show loading
if (!instance && !error) {
return (
<div className="flex h-screen w-full items-center justify-center">
<div className="flex items-center justify-center h-screen w-full">
<LogoSpinner />
</div>
);
+9 -9
View File
@@ -16,7 +16,7 @@ import { Input, Spinner } from "@plane/ui";
// components
import { Banner } from "@/components/common/banner";
// local components
import { FormHeader } from "@/components/instance/form-header";
import { FormHeader } from "../../../core/components/instance/form-header";
import { AuthBanner } from "./auth-banner";
import { AuthHeader } from "./auth-header";
import { authErrorHandler } from "./auth-helpers";
@@ -111,8 +111,8 @@ export function InstanceSignInForm() {
return (
<>
<AuthHeader />
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
<FormHeader
heading="Manage your Plane instance"
subHeading="Configure instance-wide settings to secure your instance"
@@ -134,7 +134,7 @@ export function InstanceSignInForm() {
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="email">
<label className="text-13 text-tertiary font-medium" htmlFor="email">
Email <span className="text-danger-primary">*</span>
</label>
<Input
@@ -146,13 +146,13 @@ export function InstanceSignInForm() {
placeholder="name@company.com"
value={formData.email}
onChange={(e) => handleFormChange("email", e.target.value)}
autoComplete="off"
autoComplete="on"
autoFocus
/>
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="password">
<label className="text-13 text-tertiary font-medium" htmlFor="password">
Password <span className="text-danger-primary">*</span>
</label>
<div className="relative">
@@ -165,12 +165,12 @@ export function InstanceSignInForm() {
placeholder="Enter your password"
value={formData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
autoComplete="off"
autoComplete="on"
/>
{showPassword ? (
<button
type="button"
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
@@ -178,7 +178,7 @@ export function InstanceSignInForm() {
) : (
<button
type="button"
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
+21
View File
@@ -4,10 +4,31 @@
* See the LICENSE file for details.
*/
import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
Sentry.init({
dsn: process.env.VITE_SENTRY_DSN,
environment: process.env.VITE_SENTRY_ENVIRONMENT,
sendDefaultPii: process.env.VITE_SENTRY_SEND_DEFAULT_PII ? process.env.VITE_SENTRY_SEND_DEFAULT_PII === "1" : false,
release: process.env.VITE_APP_VERSION,
tracesSampleRate: process.env.VITE_SENTRY_TRACES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_TRACES_SAMPLE_RATE)
: 0.1,
profilesSampleRate: process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE)
: 0.1,
replaysSessionSampleRate: process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE)
: 0.1,
replaysOnErrorSampleRate: process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE)
: 1.0,
integrations: [],
});
startTransition(() => {
hydrateRoot(
document,
+7 -2
View File
@@ -7,6 +7,7 @@
import type { ReactNode } from "react";
import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router";
import * as Sentry from "@sentry/react-router";
import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url";
import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url";
import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url";
@@ -74,7 +75,7 @@ export const meta: Route.MetaFunction = () => [
export default function Root() {
return (
<div className="min-h-screen bg-canvas">
<div className="bg-canvas min-h-screen">
<Outlet />
</div>
);
@@ -88,7 +89,11 @@ export function HydrateFallback() {
);
}
export function ErrorBoundary({ error: _error }: Route.ErrorBoundaryProps) {
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (error) {
Sentry.captureException(error);
}
return (
<div>
<p>Something went wrong.</p>
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { enableStaticRendering } from "mobx-react";
// stores
import { CoreRootStore } from "@/store/root.store";
enableStaticRendering(typeof window === "undefined");
export class RootStore extends CoreRootStore {
constructor() {
super();
}
hydrate(initialData: any) {
super.hydrate(initialData);
}
resetOnSignOut() {
super.resetOnSignOut();
}
}
@@ -22,8 +22,8 @@ export function AuthenticationMethodCard(props: Props) {
return (
<div
className={cn("flex w-full items-center gap-14 rounded-lg bg-layer-2", {
"border border-subtle px-4 py-3": withBorder,
className={cn("w-full flex items-center gap-14 rounded-lg bg-layer-2", {
"px-4 py-3 border border-subtle": withBorder,
})}
>
<div
@@ -36,7 +36,7 @@ export function AuthenticationMethodCard(props: Props) {
</div>
<div className="grow">
<div
className={cn("leading-5 font-medium text-primary", {
className={cn("font-medium leading-5 text-primary", {
"text-13": withBorder,
"text-18": !withBorder,
})}
@@ -44,7 +44,7 @@ export function AuthenticationMethodCard(props: Props) {
{name}
</div>
<div
className={cn("leading-5 font-regular text-tertiary", {
className={cn("font-regular leading-5 text-tertiary", {
"text-11": withBorder,
"text-13": !withBorder,
})}
@@ -16,12 +16,12 @@ export function Banner(props: TBanner) {
return (
<div
className={`w-full rounded-md border p-2 ${type === "error" ? "border-danger-strong bg-danger-subtle" : "border-success-strong bg-success-subtle"}`}
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-danger-subtle border-danger-strong" : "bg-success-subtle border-success-strong"}`}
>
<div className="flex items-center justify-center">
<div className="flex-shrink-0">
{type === "error" ? (
<span className="flex h-6 w-6 items-center justify-center rounded-full">
<span className="flex items-center justify-center h-6 w-6 rounded-full">
<AlertCircle className="h-5 w-5 text-danger-primary" aria-hidden="true" />
</span>
) : (
@@ -22,12 +22,12 @@ export function BreadcrumbLink(props: Props) {
{href ? (
<Link className="flex items-center gap-1 text-13 font-medium text-tertiary hover:text-primary" href={href}>
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-16">{icon}</div>}
<div className="relative line-clamp-1 block max-w-[150px] truncate overflow-hidden">{label}</div>
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
</Link>
) : (
<div className="flex cursor-default items-center gap-1 text-13 font-medium text-primary">
{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] truncate overflow-hidden">{label}</div>
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
</div>
)}
</div>
@@ -16,9 +16,9 @@ export function CodeBlock({ children, className, darkerShade }: TProps) {
return (
<span
className={cn(
"rounded-md border border-subtle bg-surface-2 px-0.5 text-11 font-semibold text-tertiary",
"px-0.5 text-11 text-tertiary bg-surface-2 font-semibold rounded-md border border-subtle",
{
"border-subtle bg-layer-1 text-secondary": darkerShade,
"text-secondary bg-layer-1 border-subtle": darkerShade,
},
className
)}
@@ -46,10 +46,10 @@ export function ConfirmDiscardModal(props: Props) {
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]">
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-16 leading-6 font-medium text-tertiary">
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-tertiary">
You have unsaved changes
</Dialog.Title>
<div className="mt-2">
@@ -60,7 +60,7 @@ export function ConfirmDiscardModal(props: Props) {
</div>
</div>
</div>
<div className="flex items-center justify-end gap-2 p-4 sm:px-6">
<div className="flex justify-end items-center p-4 sm:px-6 gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Keep editing
</Button>
@@ -67,7 +67,7 @@ export function ControllerInput(props: Props) {
(showPassword ? (
<button
tabIndex={-1}
className="absolute top-2.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-2.5 flex items-center justify-center text-placeholder"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
@@ -75,7 +75,7 @@ export function ControllerInput(props: Props) {
) : (
<button
tabIndex={-1}
className="absolute top-2.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-2.5 flex items-center justify-center text-placeholder"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
@@ -25,7 +25,7 @@ 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="mt-6 mb-3 text-18 font-semibold sm:mt-8">{title}</h6>
<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 && (
@@ -10,7 +10,7 @@ import { Menu, Settings } from "lucide-react";
// icons
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "../breadcrumb-link";
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
// hooks
import { useTheme } from "@/hooks/store";
// local imports
@@ -21,10 +21,10 @@ export const HamburgerToggle = observer(function HamburgerToggle() {
const { isSidebarCollapsed, toggleSidebar } = useTheme();
return (
<button
className="group flex size-7 cursor-pointer items-center justify-center rounded-sm bg-layer-1 transition-all hover:bg-layer-1-hover md:hidden"
className="size-7 rounded-sm flex justify-center items-center bg-layer-1 transition-all hover:bg-layer-1-hover cursor-pointer group md:hidden"
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<Menu size={14} className="text-secondary transition-all group-hover:text-primary" />
<Menu size={14} className="text-secondary group-hover:text-primary transition-all" />
</button>
);
});
@@ -24,25 +24,25 @@ export const PageWrapper = (props: TPageWrapperProps) => {
return (
<div
className={cn("mx-auto h-full w-full space-y-6 py-4", {
"max-w-[1000px] md:px-4 2xl:max-w-[1200px]": size === "md",
className={cn("mx-auto w-full h-full space-y-6 py-4", {
"md:px-4 max-w-[1000px] 2xl:max-w-[1200px]": size === "md",
"px-4 lg:px-12": size === "lg",
})}
>
{customHeader ? (
<div className="mx-4 shrink-0 space-y-1 border-b border-subtle py-4">{customHeader}</div>
<div className="border-b border-subtle mx-4 py-4 space-y-1 shrink-0">{customHeader}</div>
) : (
header && (
<div className="mx-4 flex shrink-0 items-center justify-between gap-4 space-y-1 border-b border-subtle py-4">
<div className="flex items-center justify-between gap-4 border-b border-subtle mx-4 py-4 space-y-1 shrink-0">
<div className={header.actions ? "flex flex-col gap-1" : "space-y-1"}>
<div className="text-h5-semibold text-primary">{header.title}</div>
<div className="text-body-sm-regular text-secondary">{header.description}</div>
<div className="text-primary text-h5-semibold">{header.title}</div>
<div className="text-secondary text-body-sm-regular">{header.description}</div>
</div>
{header.actions && <div className="shrink-0">{header.actions}</div>}
</div>
)
)}
<div className="vertical-scrollbar scrollbar-sm flex-grow overflow-hidden overflow-y-scroll px-4 pb-4">
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 pb-4">
{children}
</div>
</div>
@@ -24,12 +24,12 @@ export const InstanceFailureView = observer(function InstanceFailureView() {
return (
<>
<AuthHeader />
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
<div className="relative flex flex-col items-center justify-center space-y-4">
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
<div className="relative flex flex-col justify-center items-center space-y-4">
<img src={instanceImage} alt="Instance failure illustration" />
<h3 className="text-center text-20 font-medium text-on-color">Unable to fetch instance details.</h3>
<p className="text-center text-14 font-medium">
<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>
@@ -7,8 +7,8 @@
export function FormHeader({ heading, subHeading }: { heading: string; subHeading: string }) {
return (
<div className="flex flex-col gap-1">
<span className="text-20 leading-7 font-semibold text-primary">{heading}</span>
<span className="text-16 leading-7 font-semibold text-placeholder">{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>
);
}
@@ -11,12 +11,12 @@ import PlaneTakeOffImage from "@/app/assets/images/plane-takeoff.png?url";
export function InstanceNotReady() {
return (
<div className="relative container mx-auto flex h-full w-full items-center justify-center px-5">
<div className="relative w-auto max-w-2xl space-y-8 py-10">
<div className="relative flex flex-col items-center justify-center space-y-4">
<h1 className="pb-3 text-24 font-bold">Welcome aboard Plane!</h1>
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<h1 className="text-24 font-bold pb-3">Welcome aboard Plane!</h1>
<img src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="text-14 font-medium text-placeholder">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>
@@ -13,11 +13,11 @@ import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { AuthService } from "@plane/services";
import { Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
import { getPasswordStrength, validatePersonName, validateCompanyName } from "@plane/utils";
import { getPasswordStrength } from "@plane/utils";
// components
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
import { Banner } from "../common/banner";
import { FormHeader } from "./form-header";
import { Banner } from "@/components/common/banner";
import { FormHeader } from "@/components/instance/form-header";
// service initialization
const authService = new AuthService();
@@ -139,8 +139,8 @@ export function InstanceSetupForm() {
return (
<>
<AuthHeader />
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
<FormHeader
heading="Setup your Plane Instance"
subHeading="Post setup you will be able to manage this Plane instance."
@@ -160,9 +160,9 @@ export function InstanceSetupForm() {
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<input type="hidden" name="is_telemetry_enabled" value={formData.is_telemetry_enabled ? "True" : "False"} />
<div className="flex flex-col items-center gap-4 sm:flex-row">
<div className="flex flex-col sm:flex-row items-center gap-4">
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="first_name">
<label className="text-13 text-tertiary font-medium" htmlFor="first_name">
First name <span className="text-danger-primary">*</span>
</label>
<Input
@@ -173,19 +173,13 @@ export function InstanceSetupForm() {
inputSize="md"
placeholder="Wilber"
value={formData.first_name}
onChange={(e) => {
const validation = validatePersonName(e.target.value);
if (validation === true || e.target.value === "") {
handleFormChange("first_name", e.target.value);
}
}}
autoComplete="off"
onChange={(e) => handleFormChange("first_name", e.target.value)}
autoComplete="on"
autoFocus
maxLength={50}
/>
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="last_name">
<label className="text-13 text-tertiary font-medium" htmlFor="last_name">
Last name <span className="text-danger-primary">*</span>
</label>
<Input
@@ -196,20 +190,14 @@ export function InstanceSetupForm() {
inputSize="md"
placeholder="Wright"
value={formData.last_name}
onChange={(e) => {
const validation = validatePersonName(e.target.value);
if (validation === true || e.target.value === "") {
handleFormChange("last_name", e.target.value);
}
}}
autoComplete="off"
maxLength={50}
onChange={(e) => handleFormChange("last_name", e.target.value)}
autoComplete="on"
/>
</div>
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="email">
<label className="text-13 text-tertiary font-medium" htmlFor="email">
Email <span className="text-danger-primary">*</span>
</label>
<Input
@@ -222,7 +210,7 @@ export function InstanceSetupForm() {
value={formData.email}
onChange={(e) => handleFormChange("email", e.target.value)}
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false}
autoComplete="off"
autoComplete="on"
/>
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
<p className="px-1 text-11 text-danger-primary">{errorData.message}</p>
@@ -230,7 +218,7 @@ export function InstanceSetupForm() {
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="company_name">
<label className="text-13 text-tertiary font-medium" htmlFor="company_name">
Company name <span className="text-danger-primary">*</span>
</label>
<Input
@@ -241,18 +229,12 @@ export function InstanceSetupForm() {
inputSize="md"
placeholder="Company name"
value={formData.company_name}
onChange={(e) => {
const validation = validateCompanyName(e.target.value, false);
if (validation === true || e.target.value === "") {
handleFormChange("company_name", e.target.value);
}
}}
maxLength={80}
onChange={(e) => handleFormChange("company_name", e.target.value)}
/>
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="password">
<label className="text-13 text-tertiary font-medium" htmlFor="password">
Set a password <span className="text-danger-primary">*</span>
</label>
<div className="relative">
@@ -268,13 +250,13 @@ export function InstanceSetupForm() {
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="new-password"
autoComplete="on"
/>
{showPassword.password ? (
<button
type="button"
tabIndex={-1}
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => handleShowPassword("password")}
>
<EyeOff className="h-4 w-4" />
@@ -283,7 +265,7 @@ export function InstanceSetupForm() {
<button
type="button"
tabIndex={-1}
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => handleShowPassword("password")}
>
<Eye className="h-4 w-4" />
@@ -297,7 +279,7 @@ export function InstanceSetupForm() {
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="confirm_password">
<label className="text-13 text-tertiary font-medium" htmlFor="confirm_password">
Confirm password <span className="text-danger-primary">*</span>
</label>
<div className="relative">
@@ -312,13 +294,12 @@ export function InstanceSetupForm() {
className="w-full border border-subtle !bg-surface-1 pr-12 placeholder:text-placeholder"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
autoComplete="new-password"
/>
{showPassword.retypePassword ? (
<button
type="button"
tabIndex={-1}
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => handleShowPassword("retypePassword")}
>
<EyeOff className="h-4 w-4" />
@@ -327,7 +308,7 @@ export function InstanceSetupForm() {
<button
type="button"
tabIndex={-1}
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => handleShowPassword("retypePassword")}
>
<Eye className="h-4 w-4" />
@@ -344,21 +325,21 @@ export function InstanceSetupForm() {
<div className="relative flex gap-2">
<div>
<Checkbox
className="h-4 w-4"
className="w-4 h-4"
iconClassName="w-3 h-3"
id="is_telemetry_enabled"
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
checked={formData.is_telemetry_enabled}
/>
</div>
<label className="cursor-pointer text-13 font-medium text-tertiary" 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-blue-500 hover:text-blue-600 flex-shrink-0 text-13 font-medium"
className="text-13 font-medium text-blue-500 hover:text-blue-600 flex-shrink-0"
>
See More
</a>
@@ -24,7 +24,7 @@ export const NewUserPopup = observer(function NewUserPopup() {
if (!isNewUserPopup) return <></>;
return (
<div className="shadow-md absolute right-8 bottom-8 w-96 rounded-lg border border-subtle bg-surface-1 p-6">
<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-14 font-semibold">Create workspace</div>
@@ -41,7 +41,7 @@ export const NewUserPopup = observer(function NewUserPopup() {
</Button>
</div>
</div>
<div className="flex shrink-0 items-center justify-center">
<div className="shrink-0 flex items-center justify-center">
<img
src={resolveGeneralTheme(resolvedTheme) === "dark" ? TakeoffIconDark : TakeoffIconLight}
height={80}
@@ -30,19 +30,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 gap-2.5 truncate rounded-lg border border-subtle bg-layer-1 p-3 hover:border-subtle-1 hover:bg-layer-1-hover hover:shadow-raised-100"
className="group flex items-center justify-between p-3 gap-2.5 truncate border border-subtle hover:border-subtle-1 bg-layer-1 hover:bg-layer-1-hover hover:shadow-raised-100 rounded-lg"
rel="noreferrer"
>
<div className="flex items-start gap-4">
<span
className={`relative mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 text-11 uppercase ${
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 mt-1 text-11 uppercase ${
!workspace?.logo_url && "rounded-lg bg-accent-primary text-on-color"
}`}
>
{workspace?.logo_url && workspace.logo_url !== "" ? (
<img
src={getFileURL(workspace.logo_url)}
className="absolute top-0 left-0 h-full w-full rounded-sm object-cover"
className="absolute left-0 top-0 h-full w-full rounded-sm object-cover"
alt="Workspace Logo"
/>
) : (
@@ -50,7 +50,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
)}
</span>
<div className="flex flex-col items-start gap-1">
<div className="flex w-full flex-wrap items-center gap-2.5">
<div className="flex flex-wrap w-full items-center gap-2.5">
<h3 className={`text-14 font-medium capitalize`}>{workspace.name}</h3>/
<Tooltip tooltipContent="The unique URL of your workspace">
<h4 className="text-13 text-tertiary">[{workspace.slug}]</h4>
@@ -58,14 +58,14 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
</div>
{workspace.owner.email && (
<div className="flex items-center gap-1 text-11">
<h3 className="font-medium text-secondary">Owned by:</h3>
<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-11">
{workspace.total_projects !== null && (
<span className="flex items-center gap-1">
<h3 className="font-medium text-secondary">Total projects:</h3>
<h3 className="text-secondary font-medium">Total projects:</h3>
<h4 className="text-tertiary">{workspace.total_projects}</h4>
</span>
)}
@@ -73,7 +73,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
<>
<span className="flex items-center gap-1">
<h3 className="font-medium text-secondary">Total members:</h3>
<h3 className="text-secondary font-medium">Total members:</h3>
<h4 className="text-tertiary">{workspace.total_members}</h4>
</span>
</>
@@ -6,7 +6,7 @@
import { createContext } from "react";
// plane admin store
import { RootStore } from "../store/root.store";
import { RootStore } from "@/plane-admin/store/root.store";
let rootStore = new RootStore();

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