diff --git a/.claude/skills/branch-name/SKILL.md b/.claude/skills/branch-name/SKILL.md deleted file mode 100644 index a6fc6e55c..000000000 --- a/.claude/skills/branch-name/SKILL.md +++ /dev/null @@ -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 `/-` 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 `/-`, where the work item ID can be cleanly extracted later (e.g., by the create-pr skill). - -## Format - -``` -/- -``` - -- All lowercase, hyphen-separated -- Work item ID stays in its original form but lowercased (e.g., `SILO-1146` → `silo-1146`) -- Short description is 2–5 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**: - - 2–5 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 /- -``` - -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) diff --git a/.claude/skills/create-pull-request/SKILL.md b/.claude/skills/create-pull-request/SKILL.md deleted file mode 100644 index e6fa2d82f..000000000 --- a/.claude/skills/create-pull-request/SKILL.md +++ /dev/null @@ -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 ...HEAD --stat` — files changed - - `git log ...HEAD --oneline` — all commits on the branch - - `git diff ...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] : ` (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: `` - - **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` diff --git a/.claude/skills/react-doctor/SKILL.md b/.claude/skills/react-doctor/SKILL.md deleted file mode 100644 index 3afcd67aa..000000000 --- a/.claude/skills/react-doctor/SKILL.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: react-doctor -description: Use when finishing a feature, fixing a bug, before committing React code, or when the user types `/doctor`, asks to scan, triage, or clean up React diagnostics. Covers lint, accessibility, bundle size, architecture. Includes a regression check and a full local-triage workflow that fetches the canonical playbook. -version: "1.1.0" ---- - -# React Doctor - -Scans React codebases for security, performance, correctness, and architecture issues. Outputs a 0–100 health score. - -## After making React code changes: - -Run `npx react-doctor@latest --verbose --diff` and check the score did not regress. - -If the score dropped, fix the regressions before committing. - -## For general cleanup or code improvement: - -Run `npx react-doctor@latest --verbose` (without `--diff`) to scan the full codebase. Fix issues by severity — errors first, then warnings. - -## /doctor — full local triage workflow - -When the user types `/doctor`, says "run react doctor", or asks for a full triage / cleanup pass (not just a regression check), fetch the canonical local-triage playbook and follow every step in it: - -```bash -curl --fail --silent --show-error \ - --header 'Cache-Control: no-cache' \ - https://www.react.doctor/prompts/react-doctor-agent.md -``` - -The playbook is the single source of truth — a scan → filter → triage → fix → validate loop that edits the working tree directly (never commits, never opens PRs). Updating the prompt at its source updates every agent on its next fetch — no skill reinstall needed. - -Pair it with the matching per-rule prompts at `https://www.react.doctor/prompts/rules//.md` (fetched on demand inside the playbook) so each fix uses the canonical, reviewer-tested recipe. - -## Configuring or explaining rules - -When the user wants to understand a rule, disagrees with one, or wants to disable / tune which rules run (not fix code), use the `doctor-explain` skill (alias `/doctor-config`). Start with `npx react-doctor@latest rules explain `, then apply the narrowest control via `npx react-doctor@latest rules disable|set|category|ignore-tag …`, which edits your `doctor.config.*` (or `package.json#reactDoctor`). - -## Command - -```bash -npx react-doctor@latest --verbose --diff -``` - -| Flag | Purpose | -| ----------- | --------------------------------------------- | -| `.` | Scan current directory | -| `--verbose` | Show affected files and line numbers per rule | -| `--diff` | Only scan changed files vs base branch | -| `--score` | Output only the numeric score | diff --git a/.claude/skills/release-notes/SKILL.md b/.claude/skills/release-notes/SKILL.md deleted file mode 100644 index 9db8a12b7..000000000 --- a/.claude/skills/release-notes/SKILL.md +++ /dev/null @@ -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 --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 --json title,body,baseRefName,headRefName,commits \ - --jq '.commits[] | .messageHeadline + "\n---BODY---\n" + .messageBody + "\n===END==="' -``` - -For a quick scan first: - -```bash -gh pr view --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 1–3 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 to 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 --body "$(cat <<'EOF' - -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. diff --git a/.claude/skills/translate/SKILL.md b/.claude/skills/translate/SKILL.md deleted file mode 100644 index ce8392904..000000000 --- a/.claude/skills/translate/SKILL.md +++ /dev/null @@ -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 ---- - - - -# 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/.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 2–4, 22–24… (gen.sg.); `many` for 0, 5–20, 25–30… (gen.pl.); `other` for non-integer counts (gen.sg.). For pl: `one` (1), `few` (2–4 not 12–14, nom.pl.), `many` (0, 5–20, gen.pl.), `other` (decimals, gen.sg.). cs/sk: `one` (1), `few` (2–4, 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**: `…`, `…`, `
`, `…` — preserve attributes and nesting; translate only the visible text between tags. -- **i18next `Trans` numbered fragments**: `<0>…`, `<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, 2–19 → few; 20+ → other | -| pl | `one, few, many, other` | 1 → one; 2–4 (not 12–14) → few; 0, 5–20, many-digit → many; decimals → other | -| cs | `one, few, many, other` | 1 → one; 2–4 → few; decimals → many; 0, 5+ → other | -| sk | `one, few, many, other` | same pattern as cs | -| ru | `one, few, many, other` | 1, 21, 31… → one; 2–4, 22–24… → few; 0, 5–20, 25–30… → 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>…`, `<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 | +10–35%, single words up to +180% | Reserve generous space on buttons, tabs, labels | -| fr | +15–25% | | -| es | +15–30% | | -| it | +10–25% | | -| pt-BR | +15–30% | | -| pl | +20–30% | | -| cs, sk | +10–20% | | -| ro | +15–25% | | -| ru, ua | +15% typical, spikes to +30% | | -| tr-TR | +10–30% (agglutination) | | -| vi-VN | +30–40% | Diacritic-heavy, many small words | -| id | +10–20% | | -| 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 2–5 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 -- ` (or `git blame -L ,`) 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/.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 `.json` in **every** locale directory under `packages/i18n/src/locales/` — start with `en/.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/.json` so the namespace has content. -4. Translate every key from `en/.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(".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/.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/` -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>…`? | 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 2–4 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}`, ``, 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 2–4 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 `‏`/`‎` 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 diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index e0b681fef..000000000 --- a/.dockerignore +++ /dev/null @@ -1,69 +0,0 @@ -.git -*.pyc -.env -venv -.venv -node_modules/ -**/node_modules/ -npm-debug.log -.next/ -**/.next/ -.turbo/ -**/.turbo/ -build/ -**/build/ -out/ -**/out/ -dist/ -**/dist/ -# Logs -npm-debug.log* -pnpm-debug.log* -.pnpm-debug.log* -yarn-debug.log* -yarn-error.log* - -# OS junk -.DS_Store -Thumbs.db - -# Editor settings -.vscode -.idea - -# Coverage and test output -coverage/ -**/coverage/ -*.lcov -.junit/ -test-results/ - -# Caches and build artifacts -.cache/ -**/.cache/ -storybook-static/ -*storybook.log -*.tsbuildinfo - -# Local env and secrets -.env.local -.env.development.local -.env.test.local -.env.production.local -.secrets -tmp/ -temp/ - -# Database/cache dumps -*.rdb -*.rdb.gz - -# Misc -*.pem -*.key - -# React Router - https://github.com/remix-run/react-router-templates/blob/dc79b1a065f59f3bfd840d4ef75cc27689b611e6/default/.dockerignore -.react-router/ -build/ -node_modules/ -README.md diff --git a/.env.example b/.env.example deleted file mode 100644 index 90efa8b41..000000000 --- a/.env.example +++ /dev/null @@ -1,56 +0,0 @@ -# Database Settings -POSTGRES_USER="plane" -POSTGRES_PASSWORD="plane" -POSTGRES_DB="plane" -PGDATA="/var/lib/postgresql/data" - -# Redis Settings -REDIS_HOST="plane-redis" -REDIS_PORT="6379" - -# RabbitMQ Settings -RABBITMQ_HOST="plane-mq" -RABBITMQ_PORT="5672" -RABBITMQ_USER="plane" -RABBITMQ_PASSWORD="plane" -RABBITMQ_VHOST="plane" - -LISTEN_HTTP_PORT=80 -LISTEN_HTTPS_PORT=443 - -# AWS Settings -AWS_REGION="" -AWS_ACCESS_KEY_ID="access-key" -AWS_SECRET_ACCESS_KEY="secret-key" -AWS_S3_ENDPOINT_URL="http://plane-minio:9000" -# Changing this requires change in the proxy config for uploads if using minio setup -AWS_S3_BUCKET_NAME="uploads" -# Maximum file upload limit -FILE_SIZE_LIMIT=5242880 - -# GPT settings -OPENAI_API_BASE="https://api.openai.com/v1" # deprecated -OPENAI_API_KEY="sk-" # deprecated -GPT_ENGINE="gpt-3.5-turbo" # deprecated - -# Settings related to Docker -DOCKERIZED=1 # deprecated - -# set to 1 If using the pre-configured minio setup -USE_MINIO=1 - -# If SSL Cert to be generated, set CERT_EMAIl="email " -CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory -TRUSTED_PROXIES=0.0.0.0/0 -SITE_ADDRESS=:80 -CERT_EMAIL= - -# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL -# CERT_ACME_DNS="acme_dns " -CERT_ACME_DNS= - -# Force HTTPS for handling SSL Termination -MINIO_ENDPOINT_SSL=0 - -# API key rate limit -API_KEY_RATE_LIMIT="60/minute" diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index b87d52134..95ab84af1 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -16,12 +16,6 @@ jobs: - name: Checkout Code uses: https://github.com/actions/checkout@v4 - - name: Package Source Code - run: | - set -e - echo "==> Packaging source code..." - tar -czf deploy/source.tar.gz --exclude='.git' --exclude='node_modules' --exclude='deploy/source.tar.gz' . - - name: Deploy to Host env: SSH_KEY: ${{ secrets.CISYSTEM_SSH_KEY || secrets.BETA_EC2_SSH_KEY || secrets.SSH_KEY }} @@ -39,23 +33,26 @@ jobs: fi TARGET_USER="${REMOTE_USER:-ubuntu}" - TARGET_DIR="/home/ubuntu/plane" + BASE_DIR="/home/ubuntu/plane-selfhost" + TARGET_DIR="${BASE_DIR}/plane-app" - echo "==> Deploying Plane Project Tracker to ${TARGET_USER}@${TARGET_HOST}..." + echo "==> Deploying Plane Self-Host to ${TARGET_USER}@${TARGET_HOST} (${TARGET_DIR})..." - # Case 1: Direct Host Access (Runner has direct access to /home/ubuntu/plane) + # Case 1: Direct Host Access (Runner has direct access to /home/ubuntu/plane-selfhost) if [ -d "$TARGET_DIR" ] && command -v docker >/dev/null 2>&1; then echo "==> Direct host access detected! Deploying locally to ${TARGET_DIR}..." - mkdir -p "${TARGET_DIR}/patches" "${TARGET_DIR}/logs" - cp deploy/docker-compose.yml "${TARGET_DIR}/docker-compose.yml" - cp deploy/source.tar.gz "${TARGET_DIR}/source.tar.gz" - cp -r deploy/patches/* "${TARGET_DIR}/patches/" - cp deploy/logs.sh "${TARGET_DIR}/logs.sh" 2>/dev/null || true - chmod +x "${TARGET_DIR}/logs.sh" 2>/dev/null || true - cp deploy/install.sh "${TARGET_DIR}/install.sh" - chmod +x "${TARGET_DIR}/install.sh" + mkdir -p "${TARGET_DIR}/patches" + cp docker-compose.yaml "${TARGET_DIR}/docker-compose.yaml" + cp setup.sh "${BASE_DIR}/setup.sh" 2>/dev/null || true + chmod +x "${BASE_DIR}/setup.sh" 2>/dev/null || true + cp -r patches/* "${TARGET_DIR}/patches/" 2>/dev/null || true + cd "$TARGET_DIR" - ./install.sh + if [ -f plane.env ]; then + docker compose --env-file=plane.env up -d --remove-orphans + else + docker compose up -d --remove-orphans + fi echo "==> Direct host deployment completed successfully!" exit 0 fi @@ -80,22 +77,25 @@ jobs: SSH_OPTS="-o StrictHostKeyChecking=no" fi - echo "==> Preparing target directories on ${TARGET_HOST}..." - ssh $SSH_OPTS $TARGET_USER@$TARGET_HOST "mkdir -p ${TARGET_DIR}/patches ${TARGET_DIR}/logs" + echo "==> Ensuring target directories exist on ${TARGET_HOST}..." + ssh $SSH_OPTS $TARGET_USER@$TARGET_HOST "mkdir -p ${TARGET_DIR}/patches" - echo "==> Transferring deployment artifacts..." - scp $SSH_OPTS deploy/docker-compose.yml $TARGET_USER@$TARGET_HOST:${TARGET_DIR}/docker-compose.yml - scp $SSH_OPTS deploy/source.tar.gz $TARGET_USER@$TARGET_HOST:${TARGET_DIR}/source.tar.gz - scp $SSH_OPTS deploy/patches/* $TARGET_USER@$TARGET_HOST:${TARGET_DIR}/patches/ - scp $SSH_OPTS deploy/logs.sh $TARGET_USER@$TARGET_HOST:${TARGET_DIR}/logs.sh 2>/dev/null || true - scp $SSH_OPTS deploy/install.sh $TARGET_USER@$TARGET_HOST:${TARGET_DIR}/install.sh + echo "==> Transferring docker compose configuration and patches..." + scp $SSH_OPTS docker-compose.yaml $TARGET_USER@$TARGET_HOST:${TARGET_DIR}/docker-compose.yaml + scp $SSH_OPTS setup.sh $TARGET_USER@$TARGET_HOST:${BASE_DIR}/setup.sh 2>/dev/null || true + scp $SSH_OPTS patches/* $TARGET_USER@$TARGET_HOST:${TARGET_DIR}/patches/ 2>/dev/null || true - echo "==> Executing deployment script on host..." + echo "==> Executing deployment on host..." ssh $SSH_OPTS $TARGET_USER@$TARGET_HOST 'bash -s' << 'EOF' set -e - cd /home/ubuntu/plane - chmod +x install.sh - ./install.sh + cd /home/ubuntu/plane-selfhost/plane-app + if [ -f plane.env ]; then + docker compose --env-file=plane.env up -d --remove-orphans + else + docker compose up -d --remove-orphans + fi + echo "==> Checking Plane services status..." + docker compose ps EOF - echo "==> Plane deployment and service verification completed successfully!" + echo "==> Plane deployment completed successfully!" diff --git a/.github/ISSUE_TEMPLATE/--bug-report.yaml b/.github/ISSUE_TEMPLATE/--bug-report.yaml deleted file mode 100644 index bfec6986d..000000000 --- a/.github/ISSUE_TEMPLATE/--bug-report.yaml +++ /dev/null @@ -1,76 +0,0 @@ -name: Bug report -description: Create a bug report to help us improve Plane -title: "[bug]: " -type: Bug -labels: [🐛bug, plane] -assignees: [vihar, pushya22] -body: -- type: markdown - attributes: - value: | - Thank you for taking the time to fill out this bug report. -- type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue already exists for the bug you encountered - options: - - label: I have searched the existing issues - required: true -- type: textarea - attributes: - label: Current behavior - description: A concise description of what you're experiencing and what you expect - placeholder: | - When I do , happens and I see the error message attached below: - ```...``` - What I expect is - validations: - required: true -- type: textarea - attributes: - label: Steps to reproduce - description: Add steps to reproduce this behaviour, include console or network logs and screenshots - placeholder: | - 1. Go to '...' - 2. Click on '....' - 3. Scroll down to '....' - 4. See error - validations: - required: true -- type: dropdown - id: env - attributes: - label: Environment - options: - - Production - - Deploy preview - validations: - required: true -- type: dropdown - id: browser - attributes: - label: Browser - options: - - Google Chrome - - Mozilla Firefox - - Safari - - Other -- type: dropdown - id: edition - attributes: - label: Edition - description: Which edition of Plane are you using? - options: - - Community - - Commercial - - Cloud - validations: - required: true -- type: input - id: version - attributes: - label: Version - description: Which version of Plane are you using? If Cloud, mention it as latest - placeholder: v0.17.0-dev - validations: - required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/--feature-request.yaml b/.github/ISSUE_TEMPLATE/--feature-request.yaml deleted file mode 100644 index db154f897..000000000 --- a/.github/ISSUE_TEMPLATE/--feature-request.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: Feature request -description: Suggest a feature to improve Plane -title: "[feature]: " -type: Feature -labels: [✨feature, plane] -assignees: [vihar, pushya22] -body: -- type: markdown - attributes: - value: | - Thank you for taking the time to request a feature for Plane -- type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue related to this feature request already exists - options: - - label: I have searched the existing issues - required: true -- type: textarea - attributes: - label: Summary - description: One paragraph description of the feature - validations: - required: true -- type: textarea - attributes: - label: Why should this be worked on? - description: A concise description of the problems or use cases for this feature request - validations: - required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml deleted file mode 100644 index 301080b75..000000000 --- a/.github/ISSUE_TEMPLATE/config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -contact_links: - - name: Help and support - about: Reach out to us on our Forum or GitHub discussions. - - name: Dedicated support - url: mailto:support@plane.so - about: Write to us if you'd like dedicated support using Plane diff --git a/.github/instructions/bash.instructions.md b/.github/instructions/bash.instructions.md deleted file mode 100644 index b14d3c9ed..000000000 --- a/.github/instructions/bash.instructions.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -description: Guidelines for bash commands and tooling in the monorepo -applyTo: "**/*.sh" ---- - -# Bash & Tooling Instructions - -This document outlines the standard tools and commands used in this monorepo. - -## Package Manager - -We use **pnpm** for package management. -- **Do not use `npm` or `yarn`.** -- Lockfile: `pnpm-lock.yaml` -- Workspace configuration: `pnpm-workspace.yaml` - -### Common Commands -- Install dependencies: `pnpm install` -- Run a script in a specific package: `pnpm --filter run - - - - diff --git a/apps/api/templates/csrf_failure.html b/apps/api/templates/csrf_failure.html deleted file mode 100644 index b5a58cb02..000000000 --- a/apps/api/templates/csrf_failure.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - CSRF Verification Failed - - - -
-
-

CSRF Verification Failed

-
-
-

- It looks like your form submission has expired or there was a problem - with your request. -

-

Please try the following:

-
    -
  • Refresh the page and try submitting the form again.
  • -
  • Ensure that cookies are enabled in your browser.
  • -
- Go to Home Page -
-
- - diff --git a/apps/api/templates/emails/auth/forgot_password.html b/apps/api/templates/emails/auth/forgot_password.html deleted file mode 100644 index 29c9b4663..000000000 --- a/apps/api/templates/emails/auth/forgot_password.html +++ /dev/null @@ -1,306 +0,0 @@ - - - - - - Reset your Plane password - - - - - - - - Reset your Plane password with this secure link. - - - - - - - - \ No newline at end of file diff --git a/apps/api/templates/emails/auth/magic_signin.html b/apps/api/templates/emails/auth/magic_signin.html deleted file mode 100644 index a52700fa2..000000000 --- a/apps/api/templates/emails/auth/magic_signin.html +++ /dev/null @@ -1,265 +0,0 @@ - - - - - - Your Plane login code - - - - - - - - Your Plane login code {{code}} is valid for 10 minutes. - - - - - - - - \ No newline at end of file diff --git a/apps/api/templates/emails/exports/analytics.html b/apps/api/templates/emails/exports/analytics.html deleted file mode 100644 index f9a250d36..000000000 --- a/apps/api/templates/emails/exports/analytics.html +++ /dev/null @@ -1,215 +0,0 @@ - - - - - - Your Plane Analytics export is ready - - - - - - - - Your requested Plane Analytics data export is attached as a CSV file. - - - - - - - - \ No newline at end of file diff --git a/apps/api/templates/emails/invitations/project_invitation.html b/apps/api/templates/emails/invitations/project_invitation.html deleted file mode 100644 index f18579c64..000000000 --- a/apps/api/templates/emails/invitations/project_invitation.html +++ /dev/null @@ -1,349 +0,0 @@ - - - - - - - - {{ first_name }} invited you to join {{ project_name }} on Plane - - - - - - - - - - - - - - - - - - diff --git a/apps/api/templates/emails/invitations/workspace_invitation.html b/apps/api/templates/emails/invitations/workspace_invitation.html deleted file mode 100644 index 84ba654c1..000000000 --- a/apps/api/templates/emails/invitations/workspace_invitation.html +++ /dev/null @@ -1,306 +0,0 @@ - - - - - - {{first_name}} invited you to {{workspace_name}} on Plane - - - - - - - - {{first_name}} has invited you to join {{workspace_name}} on Plane. - - - - - - - - \ No newline at end of file diff --git a/apps/api/templates/emails/notifications/issue-updates.html b/apps/api/templates/emails/notifications/issue-updates.html deleted file mode 100644 index d82614ded..000000000 --- a/apps/api/templates/emails/notifications/issue-updates.html +++ /dev/null @@ -1,243 +0,0 @@ - - - - - - Updates on {{entity_type}} - - - - - -
- -
- - - - -
-
-
-
- -
-
- - - - -
-

{{ issue.issue_identifier }} updates

-

{{workspace}}/{{project}}/{{issue.issue_identifier}}: {{ issue.name }}

-
-
- {% if actors_involved == 1 %} -

{{summary}} {% if data|length > 0 %} {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name}} {% else %} {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name}} {% endif %} .

- {% else %} -

{{summary}} {% if data|length > 0 %} {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name}} {% else %} {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name}} {% endif %} and others.

- {% endif %} {% for update in data %} {% if update.changes.name %} -

The {{entity_type}} title has been updated to {{ issue.name}}

- {% endif %} {% if data %} -
- -
-

Updates

-
- -
- - - - - - - -
- {% if update.actor_detail.avatar_url %} {% else %} - - - - -
{{ update.actor_detail.first_name.0 }}
- {% endif %} -
-

{{ update.actor_detail.first_name }} {{ update.actor_detail.last_name }}

-
-

{{ update.activity_time }}

-
- {% if update.changes.target_date %} - - - - - - -
-
-

Due Date:

-
-
- {% if update.changes.target_date.new_value.0 %} -

{{ update.changes.target_date.new_value.0 }}

- {% else %} -

{{ update.changes.target_date.old_value.0 }}

- {% endif %} -
- {% endif %} {% if update.changes.duplicate %} - - - - {% if update.changes.duplicate.new_value.0 %} - - {% endif %} {% if update.changes.duplicate.new_value.2 %} - - {% endif %} {% if update.changes.duplicate.old_value.0 %} - - {% endif %} {% if update.changes.duplicate.old_value.2 %} - - {% endif %} - -
Duplicate: {% for duplicate in update.changes.duplicate.new_value|slice:":2" %} {{ duplicate }} {% endfor %} +{{ update.changes.duplicate.new_value|length|add:"-2" }} more {% for duplicate in update.changes.duplicate.old_value|slice:":2" %} {{ duplicate }} {% endfor %} +{{ update.changes.duplicate.old_value|length|add:"-2" }} more
- {% endif %} {% if update.changes.assignees %} - - - - - -
Assignee: {% if update.changes.assignees.new_value.0 %} {{update.changes.assignees.new_value.0}} {% endif %} {% if update.changes.assignees.new_value.1 %} +{{ update.changes.assignees.new_value|length|add:"-1"}} more {% endif %} {% if update.changes.assignees.old_value.0 %} {{update.changes.assignees.old_value.0}} {% endif %} {% if update.changes.assignees.old_value.1 %} +{{ update.changes.assignees.old_value|length|add:"-1"}} more {% endif %}
- {% endif %} {% if update.changes.labels %} - - - - - -
Labels: {% if update.changes.labels.new_value.0 %} {{update.changes.labels.new_value.0}} {% endif %} {% if update.changes.labels.new_value.1 %} +{{ update.changes.labels.new_value|length|add:"-1"}} more {% endif %} {% if update.changes.labels.old_value.0 %} {{update.changes.labels.old_value.0}} {% endif %} {% if update.changes.labels.old_value.1 %} +{{ update.changes.labels.old_value|length|add:"-1"}} more {% endif %}
- {% endif %} {% if update.changes.state %} - - - - - {% if update.changes.state.old_value.0 == 'Backlog' or update.changes.state.old_value.0 == 'In Progress' or update.changes.state.old_value.0 == 'Done' or update.changes.state.old_value.0 == 'Cancelled' %} - - {% endif %} - - - {% if update.changes.state.new_value|last == 'Backlog' or update.changes.state.new_value|last == 'In Progress' or update.changes.state.new_value|last == 'Done' or update.changes.state.new_value|last == 'Cancelled' %} - - {% endif %} - - -
-

State:

-
-

{{ update.changes.state.old_value.0 }}

-
-

{{update.changes.state.new_value|last }}

-
- {% endif %} {% if update.changes.link %} - - - - - - -
-

Links:

-
- {% for link in update.changes.link.new_value %} {{ link }} {% endfor %} {% if update.changes.link.old_value|length > 0 %} {% if update.changes.link.old_value.0 != "None" %} -

2 Links were removed

- {% endif %} {% endif %} -
- {% endif %} {% if update.changes.priority %} - - - - - - - - -
-

Priority:

-
-

{{ update.changes.priority.old_value.0 }}

-
-

{{ update.changes.priority.new_value|last }}

-
- {% endif %} {% if update.changes.blocking.new_value %} - - - - {% if update.changes.blocking.new_value.0 %} - - {% endif %} {% if update.changes.blocking.new_value.2 %} - - {% endif %} {% if update.changes.blocking.old_value.0 %} - - {% endif %} {% if update.changes.blocking.old_value.2 %} - - {% endif %} - -
Blocking: {% for blocking in update.changes.blocking.new_value|slice:":2" %} {{ blocking }} {% endfor %} +{{ update.changes.blocking.new_value|length|add:"-2" }} more {% for blocking in update.changes.blocking.old_value|slice:":2" %} {{ blocking }} {% endfor %} +{{ update.changes.blocking.old_value|length|add:"-2" }} more
- {% endif %} -
-
- {% endif %} {% endfor %} {% if comments.0 %} -
- -

Comments

- {% for comment in comments %} - - - - - -
- {% if comment.actor_detail.avatar_url %} {% else %} - - - - -
{{ comment.actor_detail.first_name.0 }}
- {% endif %} -
- - - - - {% for actor_comment in comment.actor_comments.new_value %} - - - - {% endfor %} -
-

{{ comment.actor_detail.first_name }} {{ comment.actor_detail.last_name }}

-
-
-

{{ actor_comment|safe }}

-
-
-
- {% endfor %} -
- {% endif %} -
- -
View {{entity_type}}
-
-
- - - - - -
-
- This email was sent to {{ receiver.email }}. If you'd rather not receive this kind of email, you can unsubscribe to the {{entity_type}} or manage your email preferences. - -
-
-
- - \ No newline at end of file diff --git a/apps/api/templates/emails/notifications/project_addition.html b/apps/api/templates/emails/notifications/project_addition.html deleted file mode 100644 index bc779f5ac..000000000 --- a/apps/api/templates/emails/notifications/project_addition.html +++ /dev/null @@ -1,307 +0,0 @@ - - - - - - {{inviter_first_name}} invited you to {{project_name}} on Plane - - - - - - - - {{inviter_first_name}} has invited you to the {{project_name}} project in - {{workspace_name}} on Plane. - - - - - - - - diff --git a/apps/api/templates/emails/notifications/webhook-deactivate.html b/apps/api/templates/emails/notifications/webhook-deactivate.html deleted file mode 100644 index c094eaa76..000000000 --- a/apps/api/templates/emails/notifications/webhook-deactivate.html +++ /dev/null @@ -1,298 +0,0 @@ - - - - - - - - {{ message }} - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/api/templates/emails/test_email.html b/apps/api/templates/emails/test_email.html deleted file mode 100644 index 4e4d3d940..000000000 --- a/apps/api/templates/emails/test_email.html +++ /dev/null @@ -1,6 +0,0 @@ - - -

This is a test email sent to verify if email configuration is working as expected in your Plane instance.

- -

Regards,
Team Plane

- \ No newline at end of file diff --git a/apps/api/templates/emails/user/email_updated.html b/apps/api/templates/emails/user/email_updated.html deleted file mode 100644 index cb1cad459..000000000 --- a/apps/api/templates/emails/user/email_updated.html +++ /dev/null @@ -1,1058 +0,0 @@ - - - - - - - - Plane email address successfully updated - - - - - - - - - - - - - - diff --git a/apps/api/templates/emails/user/user_activation.html b/apps/api/templates/emails/user/user_activation.html deleted file mode 100644 index 004aa95ff..000000000 --- a/apps/api/templates/emails/user/user_activation.html +++ /dev/null @@ -1,1570 +0,0 @@ - - - - - - - - Your Plane account is now active - - - - - - - - - - - - - - - diff --git a/apps/api/templates/emails/user/user_deactivation.html b/apps/api/templates/emails/user/user_deactivation.html deleted file mode 100644 index a3020acb7..000000000 --- a/apps/api/templates/emails/user/user_deactivation.html +++ /dev/null @@ -1,1571 +0,0 @@ - - - - - - - - Your Plane account has been deactivated - - - - - - - - - - - - - - - diff --git a/apps/api/tests/RUNNING_TESTS.md b/apps/api/tests/RUNNING_TESTS.md deleted file mode 100644 index 9b60b2b78..000000000 --- a/apps/api/tests/RUNNING_TESTS.md +++ /dev/null @@ -1,82 +0,0 @@ -# Running the API Test Suite - -This guide covers running the Django/pytest suite for `apps/api` inside Docker via `docker-compose-test.yml` at the repo root. The compose file boots an isolated stack — Postgres, Valkey (Redis), RabbitMQ, MinIO — with tmpfs-backed data dirs, so every run begins from a clean slate and a single teardown command removes everything. - -For background on the test layout, markers, and fixtures, see [`TESTING_GUIDE.md`](./TESTING_GUIDE.md) and [`README.md`](./README.md). - -## Prerequisites - -- Docker and Docker Compose v2 (`docker compose ...`) -- Env files generated via the setup script: - - ```bash - ./setup.sh - ``` - - This copies `apps/api/.env.example` → `apps/api/.env` (along with the other app env files). The compose file reads `apps/api/.env`, so this step must run **before** the first `docker compose` invocation. - -## Running the suite - -All commands are run from the repo root. - -### Full suite - -```bash -docker compose -f docker-compose-test.yml up \ - --build \ - --abort-on-container-exit \ - --exit-code-from api-tests -``` - -- `--build` rebuilds the `api-tests` image when `Dockerfile.dev` or `requirements/*.txt` change. -- `--abort-on-container-exit` stops the dependency services as soon as `api-tests` exits. -- `--exit-code-from api-tests` propagates pytest's exit code so this works in CI. - -### Filtered runs - -Use `docker compose run` to override the default `pytest` command. Anything you pass after the service name is forwarded to pytest. - -```bash -# Only unit tests (marker defined in pytest.ini) -docker compose -f docker-compose-test.yml run --rm --build api-tests pytest -m unit - -# A single directory, filtered by name -docker compose -f docker-compose-test.yml run --rm api-tests \ - pytest plane/tests/unit -k "test_workspace" - -# Single file with verbose output -docker compose -f docker-compose-test.yml run --rm api-tests \ - pytest plane/tests/unit/models/test_workspace.py -vv -``` - -The available markers (`unit`, `contract`, `smoke`, `slow`) are declared in `apps/api/pytest.ini`. - -### Teardown - -```bash -docker compose -f docker-compose-test.yml down -v -``` - -`-v` removes the ephemeral volumes and the `test_env` network. Because the data directories are tmpfs, no host state survives a teardown — every run starts clean. Run this between unrelated test sessions to free Docker resources. - -## How it works - -| Service | Image | Purpose | -| ------------ | ------------------------------------ | --------------------------------------------- | -| `test-db` | `postgres:15.7-alpine` | Application database | -| `test-redis` | `valkey/valkey:7.2.11-alpine` | Cache / Celery broker | -| `test-mq` | `rabbitmq:3.13.6-management-alpine` | Task queue | -| `test-minio` | `minio/minio` | S3-compatible object storage | -| `api-tests` | built from `apps/api/Dockerfile.dev` | Installs `requirements/test.txt`, runs pytest | - -All four dependencies expose health checks; `api-tests` waits for `service_healthy` on each via `depends_on`, so pytest only starts once the stack is ready. - -Test-time env overrides live in the compose file itself (`POSTGRES_HOST=test-db`, `REDIS_URL=redis://test-redis:6379/`, `AWS_S3_ENDPOINT_URL=http://test-minio:9000`, `DJANGO_SETTINGS_MODULE=plane.settings.test`). Everything else is inherited from `apps/api/.env`. - -## Troubleshooting - -- **`./apps/api/.env: no such file or directory`** — run `./setup.sh` from the repo root. -- **Port already in use** — none of the test services publish host ports; if you see this it's coming from a different compose stack. Stop the local stack (`docker compose -f docker-compose-local.yml down`). -- **Stale image after dependency changes** — rebuild explicitly: `docker compose -f docker-compose-test.yml build --no-cache api-tests`. -- **MinIO bucket missing** — the `test-minio` entrypoint creates the bucket named by `AWS_S3_BUCKET_NAME` (default `uploads`). Change the value in `apps/api/.env` and re-run. -- **Database state leaking between runs** — confirm you ran `down -v` (not just `down`). The tmpfs mounts are torn down with the container, but the network and any externally created volumes need `-v` to clear. diff --git a/apps/live/.env.example b/apps/live/.env.example deleted file mode 100644 index 5fc90d75f..000000000 --- a/apps/live/.env.example +++ /dev/null @@ -1,14 +0,0 @@ -PORT=3100 -API_BASE_URL="http://localhost:8000" - -WEB_BASE_URL="http://localhost:3000" - -LIVE_BASE_URL="http://localhost:3100" -LIVE_BASE_PATH="/live" - -LIVE_SERVER_SECRET_KEY="secret-key" - -# If you prefer not to provide a Redis URL, you can set the REDIS_HOST and REDIS_PORT environment variables instead. -REDIS_PORT=6379 -REDIS_HOST=localhost -REDIS_URL="redis://localhost:6379/" diff --git a/apps/live/.prettierignore b/apps/live/.prettierignore deleted file mode 100644 index b0b8bc622..000000000 --- a/apps/live/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -.next/ -.react-router/ -.turbo/ -.vite/ -build/ -dist/ -node_modules/ -out/ -pnpm-lock.yaml -storybook-static/ diff --git a/apps/live/Dockerfile.dev b/apps/live/Dockerfile.dev deleted file mode 100644 index 5e0f53726..000000000 --- a/apps/live/Dockerfile.dev +++ /dev/null @@ -1,15 +0,0 @@ -FROM node:22-alpine - -RUN apk add --no-cache libc6-compat -WORKDIR /app - -COPY . . -RUN corepack enable pnpm && pnpm add -g turbo -RUN pnpm install -EXPOSE 3003 - -ENV TURBO_TELEMETRY_DISABLED=1 - -VOLUME [ "/app/node_modules", "/app/live/node_modules"] - -CMD ["pnpm","dev", "--filter=live"] diff --git a/apps/live/Dockerfile.live b/apps/live/Dockerfile.live deleted file mode 100644 index 9cecc2e01..000000000 --- a/apps/live/Dockerfile.live +++ /dev/null @@ -1,85 +0,0 @@ -# syntax=docker/dockerfile:1.7 -FROM node:22-alpine AS base - -# Setup pnpm package manager with corepack and configure global bin directory for caching -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PNPM_HOME/bin:$PATH" -RUN corepack enable - -# ***************************************************************************** -# STAGE 1: Prune the project -# ***************************************************************************** -FROM base AS builder -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk update -RUN apk add --no-cache libc6-compat -# Set working directory -WORKDIR /app -ARG TURBO_VERSION=2.9.18 -RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION} -COPY . . -RUN turbo prune --scope=live --docker - -# ***************************************************************************** -# STAGE 2: Install dependencies & build the project -# ***************************************************************************** -# Add lockfile and package.json's of isolated subworkspace -FROM base AS installer -RUN apk update -RUN apk add --no-cache libc6-compat -WORKDIR /app - -# First install dependencies (as they change less often) -COPY .gitignore .gitignore -COPY --from=builder /app/out/json/ . -COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml -RUN corepack enable pnpm - -# Copy full directory structure before fetch to ensure all package.json files are available -COPY --from=builder /app/out/full/ . -COPY turbo.json turbo.json - -# Fetch dependencies to cache store, then install offline with dev deps -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store - -ENV TURBO_TELEMETRY_DISABLED=1 - -RUN pnpm turbo run build --filter=live - -# ***************************************************************************** -# STAGE 3: Run the project -# ***************************************************************************** - -FROM base AS runner -WORKDIR /app - -# OS security updates. --available forces reinstall from the current repos; -# bump APK_SECURITY_PATCH to bust buildx's cached layer (keyed on this command -# string), otherwise a rebuild silently re-ships the old packages. -ARG APK_SECURITY_PATCH=2026-09-07 -RUN echo "apk-security-patch ${APK_SECURITY_PATCH}" && apk update && apk upgrade --no-cache --available - -# Remove go from Alpine APK database; not needed at runtime and carries stdlib CVEs -RUN apk del go 2>/dev/null || true - -# npm is not used at runtime (CMD is plain node); its bundled dependencies carry -# known vulnerabilities (tar, brace-expansion, pacote, sigstore, ip-address, -# fast-uri, browserslist), so remove the whole CLI rather than picking off one -# package at a time. -RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx - -COPY --from=installer /app/packages ./packages -COPY --from=installer /app/apps/live/dist ./apps/live/dist -COPY --from=installer /app/apps/live/node_modules ./apps/live/node_modules -COPY --from=installer /app/node_modules ./node_modules -COPY --from=installer /app/apps/live/package.json ./apps/live/package.json - -# esbuild and tsgolint are build-only Go binaries; remove from runtime image to eliminate stdlib CVEs -RUN find /app/node_modules \( -name 'esbuild' -o -name 'tsgolint' \) -type f -delete 2>/dev/null || true - -ENV TURBO_TELEMETRY_DISABLED=1 - -EXPOSE 3000 - -CMD ["node", "apps/live"] diff --git a/apps/live/package.json b/apps/live/package.json deleted file mode 100644 index 9d418e14a..000000000 --- a/apps/live/package.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "name": "live", - "version": "1.4.2", - "private": true, - "description": "A realtime collaborative server powers Plane's rich text editor", - "license": "AGPL-3.0", - "author": "Plane Software Inc.", - "type": "module", - "main": "./dist/start.mjs", - "module": "./dist/start.mjs", - "exports": { - ".": "./dist/start.mjs", - "./package.json": "./package.json" - }, - "scripts": { - "build": "tsc --noEmit && tsdown", - "dev": "tsdown --watch --no-clean --onSuccess \"node --env-file=.env .\"", - "start": "node --env-file=.env .", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "check:lint": "oxlint --max-warnings=119 .", - "check:types": "tsc --noEmit", - "check:format": "oxfmt --check .", - "fix:lint": "oxlint --fix .", - "fix:format": "oxfmt .", - "clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist" - }, - "dependencies": { - "@effect/platform": "catalog:", - "@effect/platform-node": "catalog:", - "@fontsource/inter": "catalog:", - "@hocuspocus/extension-database": "catalog:", - "@hocuspocus/extension-logger": "catalog:", - "@hocuspocus/extension-redis": "catalog:", - "@hocuspocus/server": "catalog:", - "@hocuspocus/transformer": "catalog:", - "@plane/decorators": "workspace:*", - "@plane/editor": "workspace:*", - "@plane/logger": "workspace:*", - "@plane/types": "workspace:*", - "@plane/utils": "workspace:*", - "@react-pdf/renderer": "catalog:", - "@react-pdf/types": "catalog:", - "@tiptap/core": "catalog:", - "@tiptap/html": "catalog:", - "axios": "catalog:", - "compression": "catalog:", - "cors": "catalog:", - "dotenv": "catalog:", - "effect": "catalog:", - "express": "catalog:", - "express-ws": "catalog:", - "helmet": "catalog:", - "ioredis": "catalog:", - "react": "catalog:", - "sharp": "catalog:", - "uuid": "catalog:", - "ws": "catalog:", - "y-prosemirror": "catalog:", - "y-protocols": "catalog:", - "yjs": "catalog:", - "zod": "catalog:" - }, - "devDependencies": { - "@plane/typescript-config": "workspace:*", - "@types/compression": "catalog:", - "@types/cors": "catalog:", - "@types/express": "catalog:", - "@types/express-ws": "catalog:", - "@types/node": "catalog:", - "@types/pdf-parse": "catalog:", - "@types/react": "catalog:", - "@types/ws": "catalog:", - "@vitest/coverage-v8": "catalog:", - "pdf-parse": "catalog:", - "tsdown": "catalog:", - "typescript": "catalog:", - "vitest": "catalog:" - } -} diff --git a/apps/live/src/controllers/collaboration.controller.ts b/apps/live/src/controllers/collaboration.controller.ts deleted file mode 100644 index 92ead80d2..000000000 --- a/apps/live/src/controllers/collaboration.controller.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { Hocuspocus } from "@hocuspocus/server"; -import type { Request } from "express"; -import type WebSocket from "ws"; -// plane imports -import { Controller, WebSocket as WSDecorator } from "@plane/decorators"; -import { logger } from "@plane/logger"; - -@Controller("/collaboration") -export class CollaborationController { - [key: string]: unknown; - private readonly hocusPocusServer: Hocuspocus; - - constructor(hocusPocusServer: Hocuspocus) { - this.hocusPocusServer = hocusPocusServer; - } - - @WSDecorator("/") - handleConnection(ws: WebSocket, req: Request) { - try { - // Initialize the connection with Hocuspocus - this.hocusPocusServer.handleConnection(ws, req); - - // Set up error handling for the connection - ws.on("error", (error: Error) => { - logger.error("COLLABORATION_CONTROLLER: WebSocket connection error:", error); - ws.close(1011, "Internal server error"); - }); - } catch (error) { - logger.error("COLLABORATION_CONTROLLER: WebSocket connection error:", error); - ws.close(1011, "Internal server error"); - } - } -} diff --git a/apps/live/src/controllers/document.controller.ts b/apps/live/src/controllers/document.controller.ts deleted file mode 100644 index 3a0282f90..000000000 --- a/apps/live/src/controllers/document.controller.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { Request, Response } from "express"; -import { z } from "zod"; -// helpers -import { Controller, Post } from "@plane/decorators"; -import { convertHTMLDocumentToAllFormats } from "@plane/editor"; -// logger -import { logger } from "@plane/logger"; -import type { TConvertDocumentRequestBody } from "@/types"; - -// Define the schema with more robust validation -const convertDocumentSchema = z.object({ - description_html: z - .string() - .min(1, "HTML content cannot be empty") - .refine((html) => html.trim().length > 0, "HTML content cannot be just whitespace") - .refine((html) => html.includes("<") && html.includes(">"), "Content must be valid HTML"), - variant: z.enum(["rich", "document"]), -}); - -@Controller("/convert-document") -export class DocumentController { - @Post("/") - async convertDocument(req: Request, res: Response) { - try { - // Validate request body - const validatedData = convertDocumentSchema.parse(req.body as TConvertDocumentRequestBody); - const { description_html, variant } = validatedData; - - // Process document conversion - const { description_json, description_binary } = convertHTMLDocumentToAllFormats({ - document_html: description_html, - variant, - }); - - // Return successful response - res.status(200).json({ - description_json, - description_binary, - }); - } catch (error) { - if (error instanceof z.ZodError) { - const validationErrors = error.errors.map((err) => ({ - path: err.path.join("."), - message: err.message, - })); - logger.error("DOCUMENT_CONTROLLER: Validation error", { - validationErrors, - }); - return res.status(400).json({ - message: `Validation error`, - context: { - validationErrors, - }, - }); - } else { - logger.error("DOCUMENT_CONTROLLER: Internal server error", error); - return res.status(500).json({ - message: `Internal server error.`, - }); - } - } - } -} diff --git a/apps/live/src/controllers/health.controller.ts b/apps/live/src/controllers/health.controller.ts deleted file mode 100644 index 463d98672..000000000 --- a/apps/live/src/controllers/health.controller.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { Request, Response } from "express"; -import { Controller, Get } from "@plane/decorators"; -import { env } from "@/env"; - -@Controller("/health") -export class HealthController { - @Get("/") - async healthCheck(_req: Request, res: Response) { - res.status(200).json({ - status: "OK", - timestamp: new Date().toISOString(), - version: env.APP_VERSION, - }); - } -} diff --git a/apps/live/src/controllers/index.ts b/apps/live/src/controllers/index.ts deleted file mode 100644 index 2ae3bcea0..000000000 --- a/apps/live/src/controllers/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { CollaborationController } from "./collaboration.controller"; -import { DocumentController } from "./document.controller"; -import { HealthController } from "./health.controller"; -import { PdfExportController } from "./pdf-export.controller"; - -export const CONTROLLERS = [CollaborationController, DocumentController, HealthController, PdfExportController]; diff --git a/apps/live/src/controllers/pdf-export.controller.ts b/apps/live/src/controllers/pdf-export.controller.ts deleted file mode 100644 index 34c03d217..000000000 --- a/apps/live/src/controllers/pdf-export.controller.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { Request, Response } from "express"; -import { Effect, Schema, Cause } from "effect"; -import { Controller, Post } from "@plane/decorators"; -import { logger } from "@plane/logger"; -import { AppError } from "@/lib/errors"; -import { PdfExportRequestBody, PdfValidationError, PdfAuthenticationError } from "@/schema/pdf-export"; -import { PdfExportService, exportToPdf } from "@/services/pdf-export"; -import type { PdfExportInput } from "@/services/pdf-export"; - -@Controller("/pdf-export") -export class PdfExportController { - /** - * Parses and validates the request, returning a typed input object - */ - private parseRequest( - req: Request, - requestId: string - ): Effect.Effect { - return Effect.gen(function* () { - const cookie = req.headers.cookie || ""; - if (!cookie) { - return yield* Effect.fail( - new PdfAuthenticationError({ - message: "Authentication required", - }) - ); - } - - const body = yield* Schema.decodeUnknown(PdfExportRequestBody)(req.body).pipe( - Effect.mapError( - (cause) => - new PdfValidationError({ - message: "Invalid request body", - cause, - }) - ) - ); - - return { - pageId: body.pageId, - workspaceSlug: body.workspaceSlug, - projectId: body.projectId, - title: body.title, - author: body.author, - subject: body.subject, - pageSize: body.pageSize, - pageOrientation: body.pageOrientation, - fileName: body.fileName, - noAssets: body.noAssets, - cookie, - requestId, - }; - }); - } - - /** - * Maps domain errors to HTTP responses - */ - private mapErrorToHttpResponse(error: unknown): { status: number; error: string } { - if (error && typeof error === "object" && "_tag" in error) { - const tag = (error as { _tag: string })._tag; - const message = (error as { message?: string }).message || "Unknown error"; - - switch (tag) { - case "PdfValidationError": - return { status: 400, error: message }; - case "PdfAuthenticationError": - return { status: 401, error: message }; - case "PdfContentFetchError": - return { - status: message.includes("not found") ? 404 : 502, - error: message, - }; - case "PdfTimeoutError": - return { status: 504, error: message }; - case "PdfGenerationError": - return { status: 500, error: message }; - case "PdfMetadataFetchError": - case "PdfImageProcessingError": - return { status: 502, error: message }; - default: - return { status: 500, error: message }; - } - } - return { status: 500, error: "Failed to generate PDF" }; - } - - @Post("/") - async exportToPdf(req: Request, res: Response) { - const requestId = crypto.randomUUID(); - - const effect = Effect.gen(this, function* () { - // Parse request - const input = yield* this.parseRequest(req, requestId); - - // Delegate to service - return yield* exportToPdf(input); - }).pipe( - // Log errors before catching them - Effect.tapError((error) => Effect.logError("PDF_EXPORT: Export failed", { requestId, error })), - // Map all tagged errors to HTTP responses - Effect.catchAll((error) => Effect.succeed(this.mapErrorToHttpResponse(error))), - // Handle unexpected defects - Effect.catchAllDefect((defect) => { - const appError = new AppError(Cause.pretty(Cause.die(defect)), { - context: { requestId, operation: "exportToPdf" }, - }); - logger.error("PDF_EXPORT: Unexpected failure", appError); - return Effect.succeed({ status: 500, error: "Failed to generate PDF" }); - }) - ); - - const result = await Effect.runPromise(Effect.provide(effect, PdfExportService.Default)); - - // Check if result is an error response - if ("error" in result && "status" in result) { - return res.status(result.status).json({ message: result.error }); - } - - // Success - send PDF - const { pdfBuffer, outputFileName } = result; - - // Sanitize filename for Content-Disposition header to prevent header injection - const sanitizedFileName = outputFileName - .replace(/["\\\r\n]/g, "") // Remove quotes, backslashes, and CRLF - .replace(/[^\x20-\x7E]/g, "_"); // Replace non-ASCII with underscore - - res.setHeader("Content-Type", "application/pdf"); - res.setHeader( - "Content-Disposition", - `attachment; filename="${sanitizedFileName}"; filename*=UTF-8''${encodeURIComponent(outputFileName)}` - ); - res.setHeader("Content-Length", pdfBuffer.length); - return res.send(pdfBuffer); - } -} diff --git a/apps/live/src/env.ts b/apps/live/src/env.ts deleted file mode 100644 index c9b61bd43..000000000 --- a/apps/live/src/env.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import * as dotenv from "dotenv"; -import { z } from "zod"; - -dotenv.config(); - -// Environment variable validation -const envSchema = z.object({ - APP_VERSION: z.string().default("1.0.0"), - HOSTNAME: z.string().optional(), - PORT: z.string().default("3000"), - API_BASE_URL: z.string().url("API_BASE_URL must be a valid URL"), - // CORS configuration - CORS_ALLOWED_ORIGINS: z.string().default(""), - // Live running location - LIVE_BASE_PATH: z.string().default("/live"), - // Compression options - COMPRESSION_LEVEL: z.string().default("6").transform(Number), - COMPRESSION_THRESHOLD: z.string().default("5000").transform(Number), - // secret - LIVE_SERVER_SECRET_KEY: z.string(), - // Redis configuration - REDIS_HOST: z.string().optional(), - REDIS_PORT: z.string().default("6379").transform(Number), - REDIS_URL: z.string().optional(), -}); - -const validateEnv = () => { - const result = envSchema.safeParse(process.env); - if (!result.success) { - console.error("❌ Invalid environment variables:", JSON.stringify(result.error.format(), null, 4)); - process.exit(1); - } - return result.data; -}; - -export const env = validateEnv(); diff --git a/apps/live/src/extensions/database.ts b/apps/live/src/extensions/database.ts deleted file mode 100644 index becefc8e1..000000000 --- a/apps/live/src/extensions/database.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Database as HocuspocusDatabase } from "@hocuspocus/extension-database"; -// plane imports -import { - getAllDocumentFormatsFromDocumentEditorBinaryData, - getBinaryDataFromDocumentEditorHTMLString, -} from "@plane/editor"; -import type { TDocumentPayload } from "@plane/types"; -import { logger } from "@plane/logger"; -// lib -import { AppError } from "@/lib/errors"; -// services -import { getPageService } from "@/services/page/handler"; -// type -import type { FetchPayloadWithContext, StorePayloadWithContext } from "@/types"; -import { ForceCloseReason, CloseCode } from "@/types/admin-commands"; -import { broadcastError } from "@/utils/broadcast-error"; -// force close utility -import { forceCloseDocumentAcrossServers } from "./force-close-handler"; - -const fetchDocument = async ({ context, documentName: pageId, instance }: FetchPayloadWithContext) => { - try { - const service = getPageService(context.documentType, context); - // fetch details - const response = (await service.fetchDescriptionBinary(pageId)) as Buffer; - const binaryData = new Uint8Array(response); - // if binary data is empty, convert HTML to binary data - if (binaryData.byteLength === 0) { - const pageDetails = await service.fetchDetails(pageId); - const convertedBinaryData = getBinaryDataFromDocumentEditorHTMLString( - pageDetails.description_html ?? "

", - pageDetails.name - ); - if (convertedBinaryData) { - // save the converted binary data back to the database - try { - const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromDocumentEditorBinaryData( - convertedBinaryData, - true - ); - const payload: TDocumentPayload = { - description_binary: contentBinaryEncoded, - description_html: contentHTML, - description_json: contentJSON, - }; - await service.updateDescriptionBinary(pageId, payload); - } catch (e) { - const error = new AppError(e); - logger.error("Failed to save binary after first conversion from html:", error); - } - return convertedBinaryData; - } - } - // return binary data - return binaryData; - } catch (error) { - const appError = new AppError(error, { context: { pageId } }); - logger.error("Error in fetching document", appError); - - // Broadcast error to frontend for user document types - await broadcastError(instance, pageId, "Unable to load the page. Please try refreshing.", "fetch", context); - - throw appError; - } -}; - -const storeDocument = async ({ - context, - state: pageBinaryData, - documentName: pageId, - instance, -}: StorePayloadWithContext) => { - try { - const service = getPageService(context.documentType, context); - // convert binary data to all formats - const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromDocumentEditorBinaryData( - pageBinaryData, - true - ); - // create payload - const payload: TDocumentPayload = { - description_binary: contentBinaryEncoded, - description_html: contentHTML, - description_json: contentJSON, - }; - await service.updateDescriptionBinary(pageId, payload); - } catch (error) { - const appError = new AppError(error, { context: { pageId } }); - logger.error("Error in updating document:", appError); - - // Check error types - const isContentTooLarge = appError.statusCode === 413; - - // Determine if we should disconnect and unload - const shouldDisconnect = isContentTooLarge; - - // Determine error message and code - let errorMessage: string; - let errorCode: "content_too_large" | "page_locked" | "page_archived" | undefined; - - if (isContentTooLarge) { - errorMessage = "Document is too large to save. Please reduce the content size."; - errorCode = "content_too_large"; - } else { - errorMessage = "Unable to save the page. Please try again."; - } - - // Broadcast error to frontend for user document types - await broadcastError(instance, pageId, errorMessage, "store", context, errorCode, shouldDisconnect); - - // If we should disconnect, close connections and unload document - if (shouldDisconnect) { - // Map error code to ForceCloseReason with proper types - const reason = - errorCode === "content_too_large" ? ForceCloseReason.DOCUMENT_TOO_LARGE : ForceCloseReason.CRITICAL_ERROR; - - const closeCode = errorCode === "content_too_large" ? CloseCode.DOCUMENT_TOO_LARGE : CloseCode.FORCE_CLOSE; - - // force close connections and unload document - await forceCloseDocumentAcrossServers(instance, pageId, reason, closeCode); - - // Don't throw after force close - document is already unloaded - // Throwing would cause hocuspocus's finally block to access the null document - return; - } - - throw appError; - } -}; - -export class Database extends HocuspocusDatabase { - constructor() { - super({ fetch: fetchDocument, store: storeDocument }); - } -} diff --git a/apps/live/src/extensions/force-close-handler.ts b/apps/live/src/extensions/force-close-handler.ts deleted file mode 100644 index b13e08e84..000000000 --- a/apps/live/src/extensions/force-close-handler.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { Connection, Extension, Hocuspocus, onConfigurePayload } from "@hocuspocus/server"; -import { logger } from "@plane/logger"; -import { Redis } from "@/extensions/redis"; -import { AdminCommand, CloseCode, getForceCloseMessage, isForceCloseCommand } from "@/types/admin-commands"; -import type { ForceCloseReason, ClientForceCloseMessage, ForceCloseCommandData } from "@/types/admin-commands"; - -/** - * Extension to handle force close commands from other servers via Redis admin channel - */ -export class ForceCloseHandler implements Extension { - name = "ForceCloseHandler"; - priority = 999; - - async onConfigure({ instance }: onConfigurePayload) { - const redisExt = instance.configuration.extensions.find((ext) => ext instanceof Redis); - - if (!redisExt) { - logger.warn("[FORCE_CLOSE_HANDLER] Redis extension not found"); - return; - } - - // Register handler for force_close admin command - redisExt.onAdminCommand(AdminCommand.FORCE_CLOSE, async (data) => { - // Type guard for safety - if (!isForceCloseCommand(data)) { - logger.error("[FORCE_CLOSE_HANDLER] Received invalid force close command"); - return; - } - - const { docId, reason, code } = data; - - const document = instance.documents.get(docId); - if (!document) { - // Not our document, ignore - return; - } - - const connectionCount = document.getConnectionsCount(); - logger.info(`[FORCE_CLOSE_HANDLER] Sending force close message to ${connectionCount} clients...`); - - // Step 1: Send force close message to ALL clients first - const forceCloseMessage: ClientForceCloseMessage = { - type: "force_close", - reason, - code, - message: getForceCloseMessage(reason), - timestamp: new Date().toISOString(), - }; - - let messageSent = 0; - document.connections.forEach(({ connection }: { connection: Connection }) => { - try { - connection.sendStateless(JSON.stringify(forceCloseMessage)); - messageSent++; - } catch (error) { - logger.error("[FORCE_CLOSE_HANDLER] Failed to send message:", error); - } - }); - - logger.info(`[FORCE_CLOSE_HANDLER] Sent force close message to ${messageSent}/${connectionCount} clients`); - - // Wait a moment for messages to be delivered - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Step 2: Close connections - logger.info(`[FORCE_CLOSE_HANDLER] Closing ${connectionCount} connections...`); - - let closed = 0; - document.connections.forEach(({ connection }: { connection: Connection }) => { - try { - connection.close({ code, reason }); - closed++; - } catch (error) { - logger.error("[FORCE_CLOSE_HANDLER] Failed to close connection:", error); - } - }); - - logger.info(`[FORCE_CLOSE_HANDLER] Closed ${closed}/${connectionCount} connections for ${docId}`); - }); - - logger.info("[FORCE_CLOSE_HANDLER] Registered with Redis extension"); - } -} - -/** - * Force close all connections to a document across all servers and unload it from memory. - * Used for critical errors or admin operations. - * - * @param instance - The Hocuspocus server instance - * @param pageId - The document ID to force close - * @param reason - The reason for force closing - * @param code - Optional WebSocket close code (defaults to FORCE_CLOSE) - * @returns Promise that resolves when document is closed and unloaded - * @throws Error if document not found in memory - */ -export const forceCloseDocumentAcrossServers = async ( - instance: Hocuspocus, - pageId: string, - reason: ForceCloseReason, - code: CloseCode = CloseCode.FORCE_CLOSE -): Promise => { - // STEP 1: VERIFY DOCUMENT EXISTS - const document = instance.documents.get(pageId); - - if (!document) { - logger.info(`[FORCE_CLOSE] Document ${pageId} already unloaded - no action needed`); - return; // Document already cleaned up, nothing to do - } - - const connectionsBefore = document.getConnectionsCount(); - logger.info(`[FORCE_CLOSE] Sending force close message to ${connectionsBefore} local clients...`); - - const forceCloseMessage: ClientForceCloseMessage = { - type: "force_close", - reason, - code, - message: getForceCloseMessage(reason), - timestamp: new Date().toISOString(), - }; - - let messageSentCount = 0; - document.connections.forEach(({ connection }: { connection: Connection }) => { - try { - connection.sendStateless(JSON.stringify(forceCloseMessage)); - messageSentCount++; - } catch (error) { - logger.error("[FORCE_CLOSE] Failed to send message to client:", error); - } - }); - - logger.info(`[FORCE_CLOSE] Sent force close message to ${messageSentCount}/${connectionsBefore} clients`); - - // Wait a moment for messages to be delivered - await new Promise((resolve) => setTimeout(resolve, 50)); - - // STEP 3: CLOSE LOCAL CONNECTIONS - logger.info(`[FORCE_CLOSE] Closing ${connectionsBefore} local connections...`); - - let closedCount = 0; - document.connections.forEach(({ connection }: { connection: Connection }) => { - try { - connection.close({ code, reason }); - closedCount++; - } catch (error) { - logger.error("[FORCE_CLOSE] Failed to close local connection:", error); - } - }); - - logger.info(`[FORCE_CLOSE] Closed ${closedCount}/${connectionsBefore} local connections`); - - // STEP 4: BROADCAST TO OTHER SERVERS - const redisExt = instance.configuration.extensions.find((ext) => ext instanceof Redis); - - if (redisExt) { - const commandData: ForceCloseCommandData = { - command: AdminCommand.FORCE_CLOSE, - docId: pageId, - reason, - code, - originServer: instance.configuration.name || "unknown", - timestamp: new Date().toISOString(), - }; - - const receivers = await redisExt.publishAdminCommand(commandData); - logger.info(`[FORCE_CLOSE] Notified ${receivers} other server(s)`); - } else { - logger.warn("[FORCE_CLOSE] Redis extension not found, cannot notify other servers"); - } - - // STEP 5: WAIT FOR OTHER SERVERS - const waitTime = 800; - logger.info(`[FORCE_CLOSE] Waiting ${waitTime}ms for other servers to close connections...`); - await new Promise((resolve) => setTimeout(resolve, waitTime)); - - // STEP 6: UNLOAD DOCUMENT after closing all the connections - logger.info(`[FORCE_CLOSE] Unloading document from memory...`); - - try { - await instance.unloadDocument(document); - logger.info(`[FORCE_CLOSE] Document unloaded successfully ✅`); - } catch (unloadError: unknown) { - logger.error("[FORCE_CLOSE] UNLOAD FAILED:", unloadError); - logger.error(` Error: ${unloadError instanceof Error ? unloadError.message : "unknown"}`); - } - - // STEP 7: VERIFY UNLOAD - const documentAfterUnload = instance.documents.get(pageId); - - if (documentAfterUnload) { - logger.error( - `❌ [FORCE_CLOSE] Document still in memory!, Document ID: ${pageId}, Connections: ${documentAfterUnload.getConnectionsCount()}` - ); - } else { - logger.info(`✅ [FORCE_CLOSE] COMPLETE, Document: ${pageId}, Status: Successfully closed and unloaded`); - } -}; diff --git a/apps/live/src/extensions/index.ts b/apps/live/src/extensions/index.ts deleted file mode 100644 index d55ca6e8e..000000000 --- a/apps/live/src/extensions/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Database } from "./database"; -import { ForceCloseHandler } from "./force-close-handler"; -import { Logger } from "./logger"; -import { Redis } from "./redis"; -import { TitleSyncExtension } from "./title-sync"; - -export const getExtensions = () => [ - new Logger(), - new Database(), - new Redis(), - new TitleSyncExtension(), - new ForceCloseHandler(), // Must be after Redis to receive broadcasts -]; diff --git a/apps/live/src/extensions/logger.ts b/apps/live/src/extensions/logger.ts deleted file mode 100644 index f670b66c1..000000000 --- a/apps/live/src/extensions/logger.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Logger as HocuspocusLogger } from "@hocuspocus/extension-logger"; -import { logger } from "@plane/logger"; - -export class Logger extends HocuspocusLogger { - constructor() { - super({ - onChange: false, - log: (message) => { - logger.info(message); - }, - }); - } -} diff --git a/apps/live/src/extensions/redis.ts b/apps/live/src/extensions/redis.ts deleted file mode 100644 index 900a00137..000000000 --- a/apps/live/src/extensions/redis.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Redis as HocuspocusRedis } from "@hocuspocus/extension-redis"; -import { OutgoingMessage } from "@hocuspocus/server"; -import type { onConfigurePayload } from "@hocuspocus/server"; -import { logger } from "@plane/logger"; -import { AppError } from "@/lib/errors"; -import { redisManager } from "@/redis"; -import { AdminCommand } from "@/types/admin-commands"; -import type { AdminCommandData, AdminCommandHandler } from "@/types/admin-commands"; - -const getRedisClient = () => { - const redisClient = redisManager.getClient(); - if (!redisClient) { - throw new AppError("Redis client not initialized"); - } - return redisClient; -}; - -export class Redis extends HocuspocusRedis { - private adminHandlers = new Map(); - private readonly ADMIN_CHANNEL = "hocuspocus:admin"; - - constructor() { - super({ redis: getRedisClient() }); - } - - async onConfigure(payload: onConfigurePayload) { - await super.onConfigure(payload); - - // Subscribe to admin channel - await new Promise((resolve, reject) => { - this.sub.subscribe(this.ADMIN_CHANNEL, (error: Error) => { - if (error) { - logger.error(`[Redis] Failed to subscribe to admin channel:`, error); - reject(error); - } else { - logger.info(`[Redis] Subscribed to admin channel: ${this.ADMIN_CHANNEL}`); - resolve(); - } - }); - }); - - // Listen for admin messages - this.sub.on("message", this.handleAdminMessage); - logger.info(`[Redis] Attached admin message listener`); - } - - private handleAdminMessage = async (channel: string, message: string) => { - if (channel !== this.ADMIN_CHANNEL) return; - - try { - const data = JSON.parse(message) as AdminCommandData; - - // Validate command - if (!data.command || !Object.values(AdminCommand).includes(data.command as AdminCommand)) { - logger.warn(`[Redis] Invalid admin command received: ${data.command}`); - return; - } - - const handler = this.adminHandlers.get(data.command); - - if (handler) { - await handler(data); - } else { - logger.warn(`[Redis] No handler registered for admin command: ${data.command}`); - } - } catch (error) { - logger.error("[Redis] Error handling admin message:", error); - } - }; - - /** - * Register handler for an admin command - */ - public onAdminCommand( - command: AdminCommand, - handler: AdminCommandHandler - ) { - this.adminHandlers.set(command, handler as AdminCommandHandler); - logger.info(`[Redis] Registered admin command: ${command}`); - } - - /** - * Publish admin command to global channel - */ - public async publishAdminCommand(data: T): Promise { - // Validate command data - if (!data.command || !Object.values(AdminCommand).includes(data.command)) { - throw new AppError(`Invalid admin command: ${data.command}`); - } - - const message = JSON.stringify(data); - const receivers = await this.pub.publish(this.ADMIN_CHANNEL, message); - - logger.info(`[Redis] Published "${data.command}" command, received by ${receivers} server(s)`); - return receivers; - } - - async onDestroy() { - // Unsubscribe from admin channel - await new Promise((resolve) => { - this.sub.unsubscribe(this.ADMIN_CHANNEL, (error: Error) => { - if (error) { - logger.error(`[Redis] Error unsubscribing from admin channel:`, error); - } - resolve(); - }); - }); - - // Remove the message listener to prevent memory leaks - this.sub.removeListener("message", this.handleAdminMessage); - logger.info(`[Redis] Removed admin message listener`); - - await super.onDestroy(); - } - - /** - * Broadcast a message to a document across all servers via Redis. - * Uses empty identifier so ALL servers process the message. - */ - public async broadcastToDocument(documentName: string, payload: unknown): Promise { - const stringPayload = typeof payload === "string" ? payload : JSON.stringify(payload); - - const message = new OutgoingMessage(documentName).writeBroadcastStateless(stringPayload); - - const emptyPrefix = Buffer.concat([Buffer.from([0])]); - const channel = this["pubKey"](documentName); - const encodedMessage = Buffer.concat([emptyPrefix, Buffer.from(message.toUint8Array())]); - - const result = await this.pub.publishBuffer(channel, encodedMessage); - - logger.info(`REDIS_EXTENSION: Published to ${documentName}, ${result} subscribers`); - - return result; - } -} diff --git a/apps/live/src/extensions/title-sync.ts b/apps/live/src/extensions/title-sync.ts deleted file mode 100644 index c86b74986..000000000 --- a/apps/live/src/extensions/title-sync.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// hocuspocus -import type { Extension, Hocuspocus, Document } from "@hocuspocus/server"; -import { TiptapTransformer } from "@hocuspocus/transformer"; -import type { AnyExtension, JSONContent } from "@tiptap/core"; -import type * as Y from "yjs"; -// editor extensions -import { - TITLE_EDITOR_EXTENSIONS, - createRealtimeEvent, - extractTextFromHTML, - generateTitleProsemirrorJson, -} from "@plane/editor"; -import { logger } from "@plane/logger"; -import { AppError } from "@/lib/errors"; -// helpers -import { getPageService } from "@/services/page/handler"; -import type { HocusPocusServerContext, OnLoadDocumentPayloadWithContext } from "@/types"; -import { broadcastMessageToPage } from "@/utils/broadcast-message"; -import { TitleUpdateManager } from "./title-update/title-update-manager"; - -/** - * Hocuspocus extension for synchronizing document titles - */ -export class TitleSyncExtension implements Extension { - // Maps document names to their observers and update managers - private titleObservers: Map[]) => void> = new Map(); - private titleUpdateManagers: Map = new Map(); - // Store minimal data needed for each document's title observer (prevents closure memory leaks) - private titleObserverData: Map< - string, - { - parentId?: string | null; - userId: string; - workspaceSlug: string | null; - instance: Hocuspocus; - } - > = new Map(); - - /** - * Handle document loading - migrate old titles if needed - */ - async onLoadDocument({ context, document, documentName }: OnLoadDocumentPayloadWithContext) { - try { - // initially for on demand migration of old titles to a new title field - // in the yjs binary - if (document.isEmpty("title")) { - const service = getPageService(context.documentType, context); - const pageDetails = await service.fetchDetails(documentName); - const title = pageDetails.name; - if (title == null) return; - const titleJson = (generateTitleProsemirrorJson as (text: string) => JSONContent)(title); - const titleField = TiptapTransformer.toYdoc(titleJson, "title", TITLE_EDITOR_EXTENSIONS as AnyExtension[]); - document.merge(titleField); - } - } catch (error) { - const appError = new AppError(error, { - context: { operation: "onLoadDocument", documentName }, - }); - logger.error("Error loading document title", appError); - } - } - /** - * Set up title synchronization for a document after it's loaded - */ - async afterLoadDocument({ - document, - documentName, - context, - instance, - }: { - document: Document; - documentName: string; - context: HocusPocusServerContext; - instance: Hocuspocus; - }) { - // Create a title update manager for this document - const updateManager = new TitleUpdateManager(documentName, context); - - // Store the manager - this.titleUpdateManagers.set(documentName, updateManager); - - // Store minimal data needed for the observer (prevents closure memory leak) - this.titleObserverData.set(documentName, { - userId: context.userId, - workspaceSlug: context.workspaceSlug, - instance: instance, - }); - - // Create observer using bound method to avoid closure capturing heavy objects - const titleObserver = this.handleTitleChange.bind(this, documentName); - - // Observe the title field - document.getXmlFragment("title").observeDeep(titleObserver); - this.titleObservers.set(documentName, titleObserver); - } - - /** - * Handle title changes for a document - * This is a separate method to avoid closure memory leaks - */ - private handleTitleChange(documentName: string, events: Y.YEvent[]) { - let title = ""; - events.forEach((event) => { - title = extractTextFromHTML(event.currentTarget.toJSON() as string); - }); - - // Get the manager for this document - const manager = this.titleUpdateManagers.get(documentName); - - // Get the stored data for this document - const data = this.titleObserverData.get(documentName); - - // Broadcast to parent page if it exists - if (data?.parentId && data.workspaceSlug && data.instance) { - const event = createRealtimeEvent({ - user_id: data.userId, - workspace_slug: data.workspaceSlug, - action: "property_updated", - page_id: documentName, - data: { name: title }, - descendants_ids: [], - }); - - // Use the instance from stored data (guaranteed to be set) - broadcastMessageToPage(data.instance, data.parentId, event); - } - - // Schedule the title update - if (manager) { - manager.scheduleUpdate(title); - } - } - - /** - * Force save title before unloading the document - */ - async beforeUnloadDocument({ documentName }: { documentName: string }) { - const updateManager = this.titleUpdateManagers.get(documentName); - if (updateManager) { - // Force immediate save and wait for it to complete - await updateManager.forceSave(); - // Clean up the manager - this.titleUpdateManagers.delete(documentName); - } - } - - /** - * Remove observers after document unload - */ - async afterUnloadDocument({ documentName, document }: { documentName: string; document?: Document }) { - // Clean up observer when document is unloaded - const observer = this.titleObservers.get(documentName); - if (observer) { - // unregister observer from Y.js document to prevent memory leak - if (document) { - try { - document.getXmlFragment("title").unobserveDeep(observer); - } catch (error) { - logger.error("Failed to unobserve title field", new AppError(error, { context: { documentName } })); - } - } - this.titleObservers.delete(documentName); - } - - // Clean up the observer data map to prevent memory leak - this.titleObserverData.delete(documentName); - - // Ensure manager is cleaned up if beforeUnloadDocument somehow didn't run - if (this.titleUpdateManagers.has(documentName)) { - const manager = this.titleUpdateManagers.get(documentName)!; - manager.cancel(); - this.titleUpdateManagers.delete(documentName); - } - } -} diff --git a/apps/live/src/extensions/title-update/debounce.ts b/apps/live/src/extensions/title-update/debounce.ts deleted file mode 100644 index 9de1ba44c..000000000 --- a/apps/live/src/extensions/title-update/debounce.ts +++ /dev/null @@ -1,283 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { logger } from "@plane/logger"; - -/** - * DebounceState - Tracks the state of a debounced function - */ -export interface DebounceState { - lastArgs: any[] | null; - timerId: ReturnType | null; - lastCallTime: number | undefined; - lastExecutionTime: number; - inProgress: boolean; - abortController: AbortController | null; -} - -/** - * Creates a new DebounceState object - */ -export const createDebounceState = (): DebounceState => ({ - lastArgs: null, - timerId: null, - lastCallTime: undefined, - lastExecutionTime: 0, - inProgress: false, - abortController: null, -}); - -/** - * DebounceOptions - Configuration options for debounce - */ -export interface DebounceOptions { - /** The wait time in milliseconds */ - wait: number; - - /** Optional logging prefix for debug messages */ - logPrefix?: string; -} - -/** - * Enhanced debounce manager with abort support - * Manages the state and timing of debounced function calls - */ -export class DebounceManager { - private state: DebounceState; - private wait: number; - private logPrefix: string; - - /** - * Creates a new DebounceManager - * @param options Debounce configuration options - */ - constructor(options: DebounceOptions) { - this.state = createDebounceState(); - this.wait = options.wait; - this.logPrefix = options.logPrefix || ""; - } - - /** - * Schedule a debounced function call - * @param func The function to call - * @param args The arguments to pass to the function - */ - schedule(func: (...args: any[]) => Promise, ...args: any[]): void { - // Always update the last arguments - this.state.lastArgs = args; - - const time = Date.now(); - this.state.lastCallTime = time; - - // If an operation is in progress, just store the new args and start the timer - if (this.state.inProgress) { - // Always restart the timer for the new call, even if an operation is in progress - if (this.state.timerId) { - clearTimeout(this.state.timerId); - } - - this.state.timerId = setTimeout(() => { - this.timerExpired(func); - }, this.wait); - return; - } - - // If already scheduled, update the args and restart the timer - if (this.state.timerId) { - clearTimeout(this.state.timerId); - this.state.timerId = setTimeout(() => { - this.timerExpired(func); - }, this.wait); - return; - } - - // Start the timer for the trailing edge execution - this.state.timerId = setTimeout(() => { - this.timerExpired(func); - }, this.wait); - } - - /** - * Called when the timer expires - */ - private timerExpired(func: (...args: any[]) => Promise): void { - const time = Date.now(); - - // Check if this timer expiration represents the end of the debounce period - if (this.shouldInvoke(time)) { - // Execute the function - this.executeFunction(func, time); - return; - } - - // Otherwise restart the timer - this.state.timerId = setTimeout(() => { - this.timerExpired(func); - }, this.remainingWait(time)); - } - - /** - * Execute the debounced function - */ - private executeFunction(func: (...args: any[]) => Promise, time: number): void { - this.state.timerId = null; - this.state.lastExecutionTime = time; - - // Execute the function asynchronously - this.performFunction(func).catch((error) => { - logger.error(`${this.logPrefix}: Error in execution:`, error); - }); - } - - /** - * Perform the actual function call, handling any in-progress operations - */ - private async performFunction(func: (...args: any[]) => Promise): Promise { - const args = this.state.lastArgs; - if (!args) return; - - // Store the args we're about to use - const currentArgs = [...args]; - - // If another operation is in progress, abort it - await this.abortOngoingOperation(); - - // Mark that we're starting a new operation - this.state.inProgress = true; - this.state.abortController = new AbortController(); - - try { - // Add the abort signal to the arguments if the function can use it - const execArgs = [...currentArgs]; - execArgs.push(this.state.abortController.signal); - - await func(...execArgs); - - // Only clear lastArgs if they haven't been changed during this operation - if (this.state.lastArgs && this.arraysEqual(this.state.lastArgs, currentArgs)) { - this.state.lastArgs = null; - - // Clear any timer as we've successfully processed the latest args - if (this.state.timerId) { - clearTimeout(this.state.timerId); - this.state.timerId = null; - } - } else if (this.state.lastArgs) { - // If lastArgs have changed during this operation, the timer should already be running - // but let's make sure it is - if (!this.state.timerId) { - this.state.timerId = setTimeout(() => { - this.timerExpired(func); - }, this.wait); - } - } - } catch (error) { - if (error instanceof Error && error.name === "AbortError") { - // Nothing to do here, the new operation will be triggered by the timer expiration - } else { - logger.error(`${this.logPrefix}: Error during operation:`, error); - - // On error (not abort), make sure we have a timer running to retry - if (!this.state.timerId && this.state.lastArgs) { - this.state.timerId = setTimeout(() => { - this.timerExpired(func); - }, this.wait); - } - } - } finally { - this.state.inProgress = false; - this.state.abortController = null; - } - } - - /** - * Abort any ongoing operation - */ - private async abortOngoingOperation(): Promise { - if (this.state.inProgress && this.state.abortController) { - this.state.abortController.abort(); - - // Small delay to ensure the abort has had time to propagate - await new Promise((resolve) => setTimeout(resolve, 20)); - - // Double-check that state has been reset, force it if not - if (this.state.inProgress || this.state.abortController) { - this.state.inProgress = false; - this.state.abortController = null; - } - } - } - - /** - * Determine if we should invoke the function now - */ - private shouldInvoke(time: number): boolean { - // Either this is the first call, or we've waited long enough since the last call - return this.state.lastCallTime === undefined || time - this.state.lastCallTime >= this.wait; - } - - /** - * Calculate how much longer we should wait - */ - private remainingWait(time: number): number { - const timeSinceLastCall = time - (this.state.lastCallTime || 0); - return Math.max(0, this.wait - timeSinceLastCall); - } - - /** - * Force immediate execution - */ - async flush(func: (...args: any[]) => Promise): Promise { - // Clear any pending timeout - if (this.state.timerId) { - clearTimeout(this.state.timerId); - this.state.timerId = null; - } - - // Reset timing state - this.state.lastCallTime = undefined; - - // Perform the function immediately - if (this.state.lastArgs) { - await this.performFunction(func); - } - } - - /** - * Cancel any pending operations without executing - */ - cancel(): void { - // Clear any pending timeout - if (this.state.timerId) { - clearTimeout(this.state.timerId); - this.state.timerId = null; - } - - // Reset timing state - this.state.lastCallTime = undefined; - - // Abort any in-progress operation - if (this.state.inProgress && this.state.abortController) { - this.state.abortController.abort(); - this.state.inProgress = false; - this.state.abortController = null; - } - - // Clear args - this.state.lastArgs = null; - } - - /** - * Compare two arrays for equality - */ - private arraysEqual(a: any[], b: any[]): boolean { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false; - } - return true; - } -} diff --git a/apps/live/src/extensions/title-update/title-update-manager.ts b/apps/live/src/extensions/title-update/title-update-manager.ts deleted file mode 100644 index 5521c10ff..000000000 --- a/apps/live/src/extensions/title-update/title-update-manager.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { logger } from "@plane/logger"; -import { AppError } from "@/lib/errors"; -import { getPageService } from "@/services/page/handler"; -import type { HocusPocusServerContext } from "@/types"; -import { DebounceManager } from "./debounce"; - -/** - * Manages title update operations for a single document - * Handles debouncing, aborting, and force saving title updates - */ -export class TitleUpdateManager { - private documentName: string; - private context: HocusPocusServerContext; - private debounceManager: DebounceManager; - private lastTitle: string | null = null; - - /** - * Create a new TitleUpdateManager instance - */ - constructor(documentName: string, context: HocusPocusServerContext, wait: number = 5000) { - this.documentName = documentName; - this.context = context; - - // Set up debounce manager with logging - this.debounceManager = new DebounceManager({ - wait, - logPrefix: `TitleManager[${documentName.substring(0, 8)}]`, - }); - } - - /** - * Schedule a debounced title update - */ - scheduleUpdate(title: string): void { - // Store the latest title - this.lastTitle = title; - - // Schedule the update with the debounce manager - this.debounceManager.schedule(this.updateTitle.bind(this), title); - } - - /** - * Update the title - will be called by the debounce manager - */ - private async updateTitle(title: string, signal?: AbortSignal): Promise { - const service = getPageService(this.context.documentType, this.context); - if (!service.updatePageProperties) { - logger.warn(`No updateTitle method found for document ${this.documentName}`); - return; - } - - try { - await service.updatePageProperties(this.documentName, { - data: { name: title }, - abortSignal: signal, - }); - - // Clear last title only if it matches what we just updated - if (this.lastTitle === title) { - this.lastTitle = null; - } - } catch (error) { - const appError = new AppError(error, { - context: { operation: "updateTitle", documentName: this.documentName }, - }); - logger.error("Error updating title", appError); - } - } - - /** - * Force save the current title immediately - */ - async forceSave(): Promise { - // Ensure we have the current title - if (!this.lastTitle) { - return; - } - - // Use the debounce manager to flush the operation - await this.debounceManager.flush(this.updateTitle.bind(this)); - } - - /** - * Cancel any pending updates - */ - cancel(): void { - this.debounceManager.cancel(); - this.lastTitle = null; - } -} diff --git a/apps/live/src/extensions/title-update/title-utils.ts b/apps/live/src/extensions/title-update/title-utils.ts deleted file mode 100644 index ac2394865..000000000 --- a/apps/live/src/extensions/title-update/title-utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { sanitizeHTML } from "@plane/utils"; - -/** - * Utility function to extract text from HTML content - */ -export const extractTextFromHTML = (html: string): string => { - // Use sanitizeHTML to safely extract text and remove all HTML tags - // This is more secure than regex as it handles edge cases and prevents injection - // Note: sanitizeHTML trims whitespace, which is acceptable for title extraction - return sanitizeHTML(html) || ""; -}; diff --git a/apps/live/src/hocuspocus.ts b/apps/live/src/hocuspocus.ts deleted file mode 100644 index 93ebf725e..000000000 --- a/apps/live/src/hocuspocus.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Hocuspocus } from "@hocuspocus/server"; -import { v4 as uuidv4 } from "uuid"; -// env -import { env } from "@/env"; -// extensions -import { getExtensions } from "@/extensions"; -// lib -import { onAuthenticate } from "@/lib/auth"; -import { onStateless } from "@/lib/stateless"; - -export class HocusPocusServerManager { - private static instance: HocusPocusServerManager | null = null; - private server: Hocuspocus | null = null; - // server options - private serverName = env.HOSTNAME || uuidv4(); - - private constructor() { - // Private constructor to prevent direct instantiation - } - - /** - * Get the singleton instance of HocusPocusServerManager - */ - public static getInstance(): HocusPocusServerManager { - if (!HocusPocusServerManager.instance) { - HocusPocusServerManager.instance = new HocusPocusServerManager(); - } - return HocusPocusServerManager.instance; - } - - /** - * Initialize and configure the HocusPocus server - */ - public async initialize(): Promise { - if (this.server) { - return this.server; - } - - this.server = new Hocuspocus({ - name: this.serverName, - onAuthenticate, - onStateless, - extensions: getExtensions(), - debounce: 10000, - }); - - return this.server; - } - - /** - * Get the configured server instance - */ - public getServer(): Hocuspocus | null { - return this.server; - } - - /** - * Reset the singleton instance (useful for testing) - */ - public static resetInstance(): void { - HocusPocusServerManager.instance = null; - } -} diff --git a/apps/live/src/lib/auth-middleware.ts b/apps/live/src/lib/auth-middleware.ts deleted file mode 100644 index fcf06f82d..000000000 --- a/apps/live/src/lib/auth-middleware.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { Request, Response, NextFunction } from "express"; -import { logger } from "@plane/logger"; -import { env } from "@/env"; - -/** - * Express middleware to verify secret key authentication for protected endpoints - * - * Checks for secret key in headers: - * - x-admin-secret-key (preferred for admin endpoints) - * - live-server-secret-key (for backward compatibility) - * - * @param req - Express request object - * @param res - Express response object - * @param next - Express next function - * - * @example - * ```typescript - * import { Middleware } from "@plane/decorators"; - * import { requireSecretKey } from "@/lib/auth-middleware"; - * - * @Get("/protected") - * @Middleware(requireSecretKey) - * async protectedEndpoint(req: Request, res: Response) { - * // This will only execute if secret key is valid - * } - * ``` - */ -// TODO - Move to hmac -export const requireSecretKey = (req: Request, res: Response, next: NextFunction): void => { - const secretKey = req.headers["live-server-secret-key"]; - - if (!secretKey || secretKey !== env.LIVE_SERVER_SECRET_KEY) { - logger.warn(` - ⚠️ [AUTH] Unauthorized access attempt - Endpoint: ${req.path} - Method: ${req.method} - IP: ${req.ip} - User-Agent: ${req.headers["user-agent"]} - `); - - res.status(401).json({ - error: "Unauthorized", - status: 401, - }); - return; - } - - // Secret key is valid, proceed to the route handler - next(); -}; diff --git a/apps/live/src/lib/auth.ts b/apps/live/src/lib/auth.ts deleted file mode 100644 index 02aa69ca4..000000000 --- a/apps/live/src/lib/auth.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane imports -import type { IncomingHttpHeaders } from "http"; -import type { TUserDetails } from "@plane/editor"; -import { logger } from "@plane/logger"; -import { AppError } from "@/lib/errors"; -// services -import { UserService } from "@/services/user.service"; -// types -import type { HocusPocusServerContext, TDocumentTypes } from "@/types"; - -/** - * Authenticate the user - * @param requestHeaders - The request headers - * @param context - The context - * @param token - The token - * @returns The authenticated user - */ -export const onAuthenticate = async ({ - requestHeaders, - requestParameters, - context, - token, -}: { - requestHeaders: IncomingHttpHeaders; - context: HocusPocusServerContext; - requestParameters: URLSearchParams; - token: string; -}) => { - let cookie: string | undefined = undefined; - let userId: string | undefined = undefined; - - // Extract cookie (fallback to request headers) and userId from token (for scenarios where - // the cookies are not passed in the request headers) - try { - const parsedToken = JSON.parse(token) as TUserDetails; - userId = parsedToken.id; - cookie = parsedToken.cookie; - } catch (error) { - const appError = new AppError(error, { - context: { operation: "onAuthenticate" }, - }); - logger.error("Token parsing failed, using request headers", appError); - } finally { - // If cookie is still not found, fallback to request headers - if (!cookie) { - cookie = requestHeaders.cookie?.toString(); - } - } - - if (!cookie || !userId) { - const appError = new AppError("Credentials not provided", { code: "AUTH_MISSING_CREDENTIALS" }); - logger.error("Credentials not provided", appError); - throw appError; - } - - // set cookie in context, so it can be used throughout the ws connection - context.cookie = cookie ?? requestParameters.get("cookie") ?? ""; - context.documentType = requestParameters.get("documentType")?.toString() as TDocumentTypes; - context.projectId = requestParameters.get("projectId"); - context.userId = userId; - context.workspaceSlug = requestParameters.get("workspaceSlug"); - - return await handleAuthentication({ - cookie: context.cookie, - userId: context.userId, - }); -}; - -export const handleAuthentication = async ({ cookie, userId }: { cookie: string; userId: string }) => { - // fetch current user info - try { - const userService = new UserService(); - const user = await userService.currentUser(cookie); - if (user.id !== userId) { - throw new AppError("Authentication unsuccessful: User ID mismatch", { code: "AUTH_USER_MISMATCH" }); - } - - return { - user: { - id: user.id, - name: user.display_name, - }, - }; - } catch (error) { - const appError = new AppError(error, { - context: { operation: "handleAuthentication" }, - }); - logger.error("Authentication failed", appError); - throw new AppError("Authentication unsuccessful", { code: appError.code }); - } -}; diff --git a/apps/live/src/lib/errors.ts b/apps/live/src/lib/errors.ts deleted file mode 100644 index 4e2bc264c..000000000 --- a/apps/live/src/lib/errors.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { AxiosError } from "axios"; - -/** - * Application error class that sanitizes and standardizes errors across the app. - * Extracts only essential information from AxiosError to prevent massive log bloat - * and sensitive data leaks (cookies, tokens, etc). - * - * Usage: - * new AppError("Simple error message") - * new AppError("Custom error", { code: "MY_CODE", statusCode: 400 }) - * new AppError(axiosError) // Auto-extracts essential info - * new AppError(anyError) // Works with any error type - */ -export class AppError extends Error { - statusCode?: number; - method?: string; - url?: string; - code?: string; - context?: Record; - - constructor(messageOrError: string | unknown, data?: Partial>) { - // Handle error objects - extract essential info - const error = messageOrError; - - // Already AppError - return immediately for performance (no need to re-process) - if (error instanceof AppError) { - return error; - } - - // Handle string message (simple case like regular Error) - if (typeof messageOrError === "string") { - super(messageOrError); - this.name = "AppError"; - if (data) { - Object.assign(this, data); - } - return; - } - - // AxiosError - extract ONLY essential info (no config, no headers, no cookies) - if (error && typeof error === "object" && "isAxiosError" in error) { - const axiosError = error as AxiosError; - const responseData = axiosError.response?.data as any; - super(responseData?.message || axiosError.message); - this.name = "AppError"; - this.statusCode = axiosError.response?.status; - this.method = axiosError.config?.method?.toUpperCase(); - this.url = axiosError.config?.url; - this.code = axiosError.code; - return; - } - - // DOMException (AbortError from cancelled requests) - if (error instanceof DOMException && error.name === "AbortError") { - super(error.message); - this.name = "AppError"; - this.code = "ABORT_ERROR"; - return; - } - - // Standard Error objects - if (error instanceof Error) { - super(error.message); - this.name = "AppError"; - this.code = error.name; - return; - } - - // Unknown error types - safe fallback - super("Unknown error occurred"); - this.name = "AppError"; - } -} diff --git a/apps/live/src/lib/pdf/colors.ts b/apps/live/src/lib/pdf/colors.ts deleted file mode 100644 index 1b220d723..000000000 --- a/apps/live/src/lib/pdf/colors.ts +++ /dev/null @@ -1,231 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -/** - * PDF Export Color Constants - * - * These colors are mapped from the editor CSS variables and tailwind-config tokens - * to ensure PDF exports match the editor's appearance. - * - * Source mappings: - * - Editor colors: packages/editor/src/styles/variables.css - * - Tailwind tokens: packages/tailwind-config/variables.css - */ - -// Editor text colors (from variables.css :root) -export const EDITOR_TEXT_COLORS = { - gray: "#5c5e63", - peach: "#ff5b59", - pink: "#f65385", - orange: "#fd9038", - green: "#0fc27b", - "light-blue": "#17bee9", - "dark-blue": "#266df0", - purple: "#9162f9", -} as const; - -// Editor background colors - Light theme (from variables.css [data-theme*="light"]) -export const EDITOR_BACKGROUND_COLORS_LIGHT = { - gray: "#d6d6d8", - peach: "#ffd5d7", - pink: "#fdd4e3", - orange: "#ffe3cd", - green: "#c3f0de", - "light-blue": "#c5eff9", - "dark-blue": "#c9dafb", - purple: "#e3d8fd", -} as const; - -// Editor background colors - Dark theme (from variables.css [data-theme*="dark"]) -export const EDITOR_BACKGROUND_COLORS_DARK = { - gray: "#404144", - peach: "#593032", - pink: "#562e3d", - orange: "#583e2a", - green: "#1d4a3b", - "light-blue": "#1f495c", - "dark-blue": "#223558", - purple: "#3d325a", -} as const; - -// Use light theme colors by default for PDF exports -export const EDITOR_BACKGROUND_COLORS = EDITOR_BACKGROUND_COLORS_LIGHT; - -// Color key type -export type EditorColorKey = keyof typeof EDITOR_TEXT_COLORS; - -/** - * Maps a color key to its text color hex value - */ -export const getTextColorHex = (colorKey: string): string | null => { - if (colorKey in EDITOR_TEXT_COLORS) { - return EDITOR_TEXT_COLORS[colorKey as EditorColorKey]; - } - return null; -}; - -/** - * Maps a color key to its background color hex value - */ -export const getBackgroundColorHex = (colorKey: string): string | null => { - if (colorKey in EDITOR_BACKGROUND_COLORS) { - return EDITOR_BACKGROUND_COLORS[colorKey as EditorColorKey]; - } - return null; -}; - -/** - * Checks if a value is a CSS variable reference (e.g., "var(--editor-colors-gray-text)") - */ -export const isCssVariable = (value: string): boolean => { - return value.startsWith("var("); -}; - -/** - * Extracts the color key from a CSS variable reference - * e.g., "var(--editor-colors-gray-text)" -> "gray" - * e.g., "var(--editor-colors-light-blue-background)" -> "light-blue" - */ -export const extractColorKeyFromCssVariable = (cssVar: string): string | null => { - // Match patterns like: var(--editor-colors-{color}-text) or var(--editor-colors-{color}-background) - const match = cssVar.match(/var\(--editor-colors-([\w-]+)-(text|background)\)/); - if (match) { - return match[1]; - } - return null; -}; - -/** - * Resolves a color value to a hex color for PDF rendering - * Handles both direct hex values and CSS variable references - */ -export const resolveColorForPdf = (value: string | null | undefined, type: "text" | "background"): string | null => { - if (!value) return null; - - // If it's already a hex color, return it - if (value.startsWith("#")) { - return value; - } - - // If it's a CSS variable, extract the key and get the hex value - if (isCssVariable(value)) { - const colorKey = extractColorKeyFromCssVariable(value); - if (colorKey) { - return type === "text" ? getTextColorHex(colorKey) : getBackgroundColorHex(colorKey); - } - } - - // If it's just a color key (e.g., "gray", "peach"), get the hex value - if (type === "text") { - return getTextColorHex(value); - } - return getBackgroundColorHex(value); -}; - -// Semantic colors from tailwind-config (light theme) -// These are derived from the CSS variables in packages/tailwind-config/variables.css - -// Neutral colors (light theme) -export const NEUTRAL_COLORS = { - white: "#ffffff", - 100: "#fafafa", // oklch(0.9848 0.0003 230.66) ≈ #fafafa - 200: "#f5f5f5", // oklch(0.9696 0.0007 230.67) ≈ #f5f5f5 - 300: "#f0f0f0", // oklch(0.9543 0.001 230.67) ≈ #f0f0f0 - 400: "#ebebeb", // oklch(0.9389 0.0014 230.68) ≈ #ebebeb - 500: "#e5e5e5", // oklch(0.9235 0.001733 230.6853) ≈ #e5e5e5 - 600: "#d9d9d9", // oklch(0.8925 0.0024 230.7) ≈ #d9d9d9 - 700: "#cccccc", // oklch(0.8612 0.0032 230.71) ≈ #cccccc - 800: "#8c8c8c", // oklch(0.6668 0.0079 230.82) ≈ #8c8c8c - 900: "#7a7a7a", // oklch(0.6161 0.009153 230.867) ≈ #7a7a7a - 1000: "#636363", // oklch(0.5288 0.0083 230.88) ≈ #636363 - 1100: "#4d4d4d", // oklch(0.4377 0.0066 230.87) ≈ #4d4d4d - 1200: "#1f1f1f", // oklch(0.2378 0.0029 230.83) ≈ #1f1f1f - black: "#0f0f0f", // oklch(0.1472 0.0034 230.83) ≈ #0f0f0f -} as const; - -// Brand colors (light theme accent) -export const BRAND_COLORS = { - default: "#3f76ff", // oklch(0.4799 0.1158 242.91) - primary accent blue - 100: "#f5f8ff", - 200: "#e8f0ff", - 300: "#d1e1ff", - 400: "#b3d0ff", - 500: "#8ab8ff", - 600: "#5c9aff", - 700: "#3f76ff", - 900: "#2952b3", - 1000: "#1e3d80", - 1100: "#142b5c", - 1200: "#0d1f40", -} as const; - -// Semantic text colors -export const TEXT_COLORS = { - primary: NEUTRAL_COLORS[1200], // --txt-primary - secondary: NEUTRAL_COLORS[1100], // --txt-secondary - tertiary: NEUTRAL_COLORS[1000], // --txt-tertiary - placeholder: NEUTRAL_COLORS[900], // --txt-placeholder - disabled: NEUTRAL_COLORS[800], // --txt-disabled - accentPrimary: BRAND_COLORS.default, // --txt-accent-primary - linkPrimary: BRAND_COLORS.default, // --txt-link-primary -} as const; - -// Semantic background colors -export const BACKGROUND_COLORS = { - canvas: NEUTRAL_COLORS[300], // --bg-canvas - surface1: NEUTRAL_COLORS.white, // --bg-surface-1 - surface2: NEUTRAL_COLORS[100], // --bg-surface-2 - layer1: NEUTRAL_COLORS[200], // --bg-layer-1 - layer2: NEUTRAL_COLORS.white, // --bg-layer-2 - layer3: NEUTRAL_COLORS[300], // --bg-layer-3 - accentSubtle: "#f5f8ff", // --bg-accent-subtle (brand-100) -} as const; - -// Semantic border colors -export const BORDER_COLORS = { - subtle: NEUTRAL_COLORS[400], // --border-subtle - subtle1: NEUTRAL_COLORS[500], // --border-subtle-1 - strong: NEUTRAL_COLORS[600], // --border-strong - strong1: NEUTRAL_COLORS[700], // --border-strong-1 - accentStrong: BRAND_COLORS.default, // --border-accent-strong -} as const; - -// Code/inline code colors -export const CODE_COLORS = { - background: NEUTRAL_COLORS[200], // Similar to bg-layer-1 - text: "#dc2626", // Red for inline code text (matches editor) - blockText: NEUTRAL_COLORS[1200], // Regular text for code blocks -} as const; - -// Link colors -export const LINK_COLORS = { - primary: BRAND_COLORS.default, - hover: BRAND_COLORS[900], -} as const; - -// Mention colors (from pi-chat-editor mention styles: bg-accent-primary/20 text-accent-primary) -export const MENTION_COLORS = { - background: "#e0e9ff", // accent-primary with ~20% opacity on white - text: BRAND_COLORS.default, -} as const; - -// Success/Green colors -export const SUCCESS_COLORS = { - primary: "#10b981", - subtle: "#d1fae5", -} as const; - -// Warning/Amber colors -export const WARNING_COLORS = { - primary: "#f59e0b", - subtle: "#fef3c7", -} as const; - -// Danger/Red colors -export const DANGER_COLORS = { - primary: "#ef4444", - subtle: "#fee2e2", -} as const; diff --git a/apps/live/src/lib/pdf/icons.tsx b/apps/live/src/lib/pdf/icons.tsx deleted file mode 100644 index 92621f3a8..000000000 --- a/apps/live/src/lib/pdf/icons.tsx +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Circle, Path, Rect, Svg } from "@react-pdf/renderer"; - -type IconProps = { - size?: number; - color?: string; -}; - -// Lightbulb icon for callouts (default) -export const LightbulbIcon = ({ size = 16, color = "#ffffff" }: IconProps) => ( - - - -); - -// Document/file icon for page embeds -export const DocumentIcon = ({ size = 12, color = "#1e40af" }: IconProps) => ( - - - - - -); - -// Link icon for page links and external links -export const LinkIcon = ({ size = 12, color = "#2563eb" }: IconProps) => ( - - - - -); - -// Paperclip icon for attachments (default) -export const PaperclipIcon = ({ size = 16, color = "#374151" }: IconProps) => ( - - - -); - -// Image icon for image attachments -export const ImageIcon = ({ size = 16, color = "#374151" }: IconProps) => ( - - - - - -); - -// Video icon for video attachments -export const VideoIcon = ({ size = 16, color = "#374151" }: IconProps) => ( - - - - -); - -// Music/audio icon -export const MusicIcon = ({ size = 16, color = "#374151" }: IconProps) => ( - - - - - -); - -// File-text icon for PDFs and documents -export const FileTextIcon = ({ size = 16, color = "#374151" }: IconProps) => ( - - - - -); - -// Table/spreadsheet icon -export const TableIcon = ({ size = 16, color = "#374151" }: IconProps) => ( - - - - -); - -// Presentation icon -export const PresentationIcon = ({ size = 16, color = "#374151" }: IconProps) => ( - - - - -); - -// Archive/zip icon -export const ArchiveIcon = ({ size = 16, color = "#374151" }: IconProps) => ( - - - - -); - -// Globe icon for external embeds (rich cards) -export const GlobeIcon = ({ size = 12, color = "#374151" }: IconProps) => ( - - - - -); - -// Clipboard icon for whiteboards -export const ClipboardIcon = ({ size = 12, color = "#6b7280" }: IconProps) => ( - - - - -); - -// Ruler/diagram icon for diagrams -export const DiagramIcon = ({ size = 12, color = "#6b7280" }: IconProps) => ( - - - - - -); - -// Work item / task icon -export const TaskIcon = ({ size = 14, color = "#374151" }: IconProps) => ( - - - - -); - -// Checkmark icon for checked task items -export const CheckIcon = ({ size = 10, color = "#ffffff" }: IconProps) => ( - - - -); - -// Helper to get file icon component based on file type -export const getFileIcon = (fileType: string, size = 16, color = "#374151") => { - if (fileType.startsWith("image/")) return ; - if (fileType.startsWith("video/")) return ; - if (fileType.startsWith("audio/")) return ; - if (fileType.includes("pdf")) return ; - if (fileType.includes("spreadsheet") || fileType.includes("excel")) return ; - if (fileType.includes("document") || fileType.includes("word")) return ; - if (fileType.includes("presentation") || fileType.includes("powerpoint")) - return ; - if (fileType.includes("zip") || fileType.includes("archive")) return ; - return ; -}; diff --git a/apps/live/src/lib/pdf/index.ts b/apps/live/src/lib/pdf/index.ts deleted file mode 100644 index f3fe47831..000000000 --- a/apps/live/src/lib/pdf/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export { createPdfDocument, renderPlaneDocToPdfBlob, renderPlaneDocToPdfBuffer } from "./plane-pdf-exporter"; -export { createKeyGenerator, nodeRenderers, renderNode } from "./node-renderers"; -export { markRenderers, applyMarks } from "./mark-renderers"; -export { pdfStyles } from "./styles"; -export type { - KeyGenerator, - MarkRendererRegistry, - NodeRendererRegistry, - PDFExportMetadata, - PDFExportOptions, - PDFMarkRenderer, - PDFNodeRenderer, - PDFRenderContext, - PDFUserMention, - TipTapDocument, - TipTapMark, - TipTapNode, -} from "./types"; diff --git a/apps/live/src/lib/pdf/mark-renderers.ts b/apps/live/src/lib/pdf/mark-renderers.ts deleted file mode 100644 index 1f40c4e35..000000000 --- a/apps/live/src/lib/pdf/mark-renderers.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { Style } from "@react-pdf/types"; -import { - BACKGROUND_COLORS, - CODE_COLORS, - EDITOR_BACKGROUND_COLORS, - EDITOR_TEXT_COLORS, - LINK_COLORS, - resolveColorForPdf, -} from "./colors"; -import type { MarkRendererRegistry, TipTapMark } from "./types"; - -export const markRenderers: MarkRendererRegistry = { - bold: (_mark: TipTapMark, style: Style): Style => ({ - ...style, - fontWeight: "bold", - }), - - italic: (_mark: TipTapMark, style: Style): Style => ({ - ...style, - fontStyle: "italic", - }), - - underline: (_mark: TipTapMark, style: Style): Style => ({ - ...style, - textDecoration: "underline", - }), - - strike: (_mark: TipTapMark, style: Style): Style => ({ - ...style, - textDecoration: "line-through", - }), - - code: (_mark: TipTapMark, style: Style): Style => ({ - ...style, - fontFamily: "Courier", - fontSize: 10, - backgroundColor: BACKGROUND_COLORS.layer1, - color: CODE_COLORS.text, - }), - - link: (_mark: TipTapMark, style: Style): Style => ({ - ...style, - color: LINK_COLORS.primary, - textDecoration: "underline", - }), - - textStyle: (mark: TipTapMark, style: Style): Style => { - const attrs = mark.attrs || {}; - const newStyle: Style = { ...style }; - - if (attrs.color && typeof attrs.color === "string") { - newStyle.color = attrs.color; - } - - if (attrs.backgroundColor && typeof attrs.backgroundColor === "string") { - newStyle.backgroundColor = attrs.backgroundColor; - } - - return newStyle; - }, - - highlight: (mark: TipTapMark, style: Style): Style => { - const attrs = mark.attrs || {}; - return { - ...style, - backgroundColor: (attrs.color as string) || EDITOR_BACKGROUND_COLORS.purple, - }; - }, - - subscript: (_mark: TipTapMark, style: Style): Style => ({ - ...style, - fontSize: 8, - }), - - superscript: (_mark: TipTapMark, style: Style): Style => ({ - ...style, - fontSize: 8, - }), - - /** - * Custom color mark handler - * Handles the customColor extension which stores colors as data-text-color and data-background-color attributes - * The colors can be either: - * 1. Color keys like "gray", "peach", "pink", etc. (from COLORS_LIST) - * 2. Direct hex values for custom colors - * 3. CSS variable references like "var(--editor-colors-gray-text)" - */ - customColor: (mark: TipTapMark, style: Style): Style => { - const attrs = mark.attrs || {}; - const newStyle: Style = { ...style }; - - // Handle text color (stored in 'color' attribute) - const textColor = attrs.color as string | undefined; - if (textColor) { - const resolvedColor = resolveColorForPdf(textColor, "text"); - if (resolvedColor) { - newStyle.color = resolvedColor; - } else if (textColor.startsWith("#") || textColor.startsWith("rgb")) { - // Direct color value - newStyle.color = textColor; - } else if (textColor in EDITOR_TEXT_COLORS) { - // Color key lookup - newStyle.color = EDITOR_TEXT_COLORS[textColor as keyof typeof EDITOR_TEXT_COLORS]; - } - } - - // Handle background color (stored in 'backgroundColor' attribute) - const backgroundColor = attrs.backgroundColor as string | undefined; - if (backgroundColor) { - const resolvedColor = resolveColorForPdf(backgroundColor, "background"); - if (resolvedColor) { - newStyle.backgroundColor = resolvedColor; - } else if (backgroundColor.startsWith("#") || backgroundColor.startsWith("rgb")) { - // Direct color value - newStyle.backgroundColor = backgroundColor; - } else if (backgroundColor in EDITOR_BACKGROUND_COLORS) { - // Color key lookup - newStyle.backgroundColor = EDITOR_BACKGROUND_COLORS[backgroundColor as keyof typeof EDITOR_BACKGROUND_COLORS]; - } - } - - return newStyle; - }, -}; - -export const applyMarks = (marks: TipTapMark[] | undefined, baseStyle: Style = {}): Style => { - if (!marks || marks.length === 0) { - return baseStyle; - } - - return marks.reduce((style, mark) => { - const renderer = markRenderers[mark.type]; - if (renderer) { - return renderer(mark, style); - } - return style; - }, baseStyle); -}; diff --git a/apps/live/src/lib/pdf/node-renderers.tsx b/apps/live/src/lib/pdf/node-renderers.tsx deleted file mode 100644 index 003d21f55..000000000 --- a/apps/live/src/lib/pdf/node-renderers.tsx +++ /dev/null @@ -1,444 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Image, Link, Text, View } from "@react-pdf/renderer"; -import type { Style } from "@react-pdf/types"; -import type { ReactElement } from "react"; -import { CORE_EXTENSIONS } from "@plane/editor"; -import { BACKGROUND_COLORS, EDITOR_BACKGROUND_COLORS, resolveColorForPdf, TEXT_COLORS } from "./colors"; -import { CheckIcon, ClipboardIcon, DocumentIcon, GlobeIcon, LightbulbIcon, LinkIcon } from "./icons"; -import { applyMarks } from "./mark-renderers"; -import { pdfStyles } from "./styles"; -import type { KeyGenerator, NodeRendererRegistry, PDFExportMetadata, PDFRenderContext, TipTapNode } from "./types"; - -const getCalloutIcon = (node: TipTapNode, color: string): ReactElement => { - const logoInUse = node.attrs?.["data-logo-in-use"] as string | undefined; - const iconName = node.attrs?.["data-icon-name"] as string | undefined; - const iconColor = (node.attrs?.["data-icon-color"] as string) || color; - - if (logoInUse === "emoji") { - const emojiUnicode = node.attrs?.["data-emoji-unicode"] as string | undefined; - if (emojiUnicode) { - return {emojiUnicode}; - } - } - - if (iconName) { - switch (iconName) { - case "FileText": - case "File": - return ; - case "Link": - return ; - case "Globe": - return ; - case "Clipboard": - return ; - case "CheckSquare": - case "Check": - return ; - case "Lightbulb": - default: - return ; - } - } - - return ; -}; - -export const createKeyGenerator = (): KeyGenerator => { - let counter = 0; - return () => `node-${counter++}`; -}; - -const renderTextWithMarks = (node: TipTapNode, getKey: KeyGenerator): ReactElement => { - const style = applyMarks(node.marks, {}); - const hasLink = node.marks?.find((m) => m.type === "link"); - - if (hasLink) { - const href = (hasLink.attrs?.href as string) || "#"; - return ( - - {node.text || ""} - - ); - } - - return ( - - {node.text || ""} - - ); -}; - -const getTextAlignStyle = (textAlign: string | null | undefined): Style => { - if (!textAlign) return {}; - return { - textAlign: textAlign as "left" | "right" | "center" | "justify", - }; -}; - -const getFlexAlignStyle = (textAlign: string | null | undefined): Style => { - if (!textAlign) return {}; - if (textAlign === "right") return { alignItems: "flex-end" }; - if (textAlign === "center") return { alignItems: "center" }; - return {}; -}; - -export const nodeRenderers: NodeRendererRegistry = { - doc: (_node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => ( - {children} - ), - - text: (node: TipTapNode, _children: ReactElement[], ctx: PDFRenderContext): ReactElement => - renderTextWithMarks(node, ctx.getKey), - - paragraph: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const textAlign = node.attrs?.textAlign as string | null; - const background = node.attrs?.backgroundColor as string | undefined; - const alignStyle = getTextAlignStyle(textAlign); - const flexStyle = getFlexAlignStyle(textAlign); - const resolvedBgColor = - background && background !== "default" ? resolveColorForPdf(background, "background") : null; - const bgStyle = resolvedBgColor ? { backgroundColor: resolvedBgColor } : {}; - - return ( - - {children} - - ); - }, - - heading: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const level = (node.attrs?.level as number) || 1; - const styleKey = `heading${level}` as keyof typeof pdfStyles; - const style = pdfStyles[styleKey] || pdfStyles.heading1; - const textAlign = node.attrs?.textAlign as string | null; - const alignStyle = getTextAlignStyle(textAlign); - const flexStyle = getFlexAlignStyle(textAlign); - - return ( - - {children} - - ); - }, - - blockquote: (_node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => ( - - {children} - - ), - - codeBlock: (node: TipTapNode, _children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const codeContent = node.content?.map((c) => c.text || "").join("") || ""; - return ( - - {codeContent} - - ); - }, - - bulletList: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const nestingLevel = (node.attrs?._nestingLevel as number) || 0; - const indentStyle = nestingLevel > 0 ? { marginLeft: 18 } : {}; - return ( - - {children} - - ); - }, - - orderedList: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const nestingLevel = (node.attrs?._nestingLevel as number) || 0; - const indentStyle = nestingLevel > 0 ? { marginLeft: 18 } : {}; - return ( - - {children} - - ); - }, - - listItem: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const isOrdered = node.attrs?._parentType === "orderedList"; - const index = (node.attrs?._listItemIndex as number) || 0; - - const bullet = isOrdered ? `${index}.` : "•"; - - const textAlign = node.attrs?._textAlign as string | null; - const flexStyle = getFlexAlignStyle(textAlign); - - return ( - - - {bullet} - - {children} - - ); - }, - - taskList: (_node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => ( - - {children} - - ), - - taskItem: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const checked = node.attrs?.checked === true; - return ( - - - {checked && } - - {children} - - ); - }, - - table: (_node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => ( - - {children} - - ), - - tableRow: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const isHeader = node.attrs?._isHeader === true; - return ( - - {children} - - ); - }, - - tableHeader: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const colwidth = node.attrs?.colwidth as number[] | undefined; - const background = node.attrs?.background as string | undefined; - const width = colwidth?.[0]; - const widthStyle = width ? { width, flex: undefined } : {}; - const resolvedBgColor = background ? resolveColorForPdf(background, "background") : null; - const bgStyle = resolvedBgColor ? { backgroundColor: resolvedBgColor } : {}; - - return ( - - {children} - - ); - }, - - tableCell: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const colwidth = node.attrs?.colwidth as number[] | undefined; - const background = node.attrs?.background as string | undefined; - const width = colwidth?.[0]; - const widthStyle = width ? { width, flex: undefined } : {}; - const resolvedBgColor = background ? resolveColorForPdf(background, "background") : null; - const bgStyle = resolvedBgColor ? { backgroundColor: resolvedBgColor } : {}; - - return ( - - {children} - - ); - }, - - horizontalRule: (_node: TipTapNode, _children: ReactElement[], ctx: PDFRenderContext): ReactElement => ( - - ), - - hardBreak: (_node: TipTapNode, _children: ReactElement[], ctx: PDFRenderContext): ReactElement => ( - {"\n"} - ), - - image: (node: TipTapNode, _children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - if (ctx.metadata?.noAssets) { - return ; - } - - const src = (node.attrs?.src as string) || ""; - const width = node.attrs?.width as number | undefined; - const alignment = (node.attrs?.alignment as string) || "left"; - - if (!src) { - return ; - } - - const alignmentStyle = - alignment === "center" - ? { alignItems: "center" as const } - : alignment === "right" - ? { alignItems: "flex-end" as const } - : { alignItems: "flex-start" as const }; - - return ( - - - - ); - }, - - imageComponent: (node: TipTapNode, _children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - if (ctx.metadata?.noAssets) { - return ; - } - - const assetId = (node.attrs?.src as string) || ""; - const rawWidth = node.attrs?.width; - const width = typeof rawWidth === "string" ? parseInt(rawWidth, 10) : (rawWidth as number | undefined); - const alignment = (node.attrs?.alignment as string) || "left"; - - if (!assetId) { - return ; - } - - let resolvedSrc = assetId; - if (ctx.metadata?.resolvedImageUrls && ctx.metadata.resolvedImageUrls[assetId]) { - resolvedSrc = ctx.metadata.resolvedImageUrls[assetId]; - } - - const alignmentStyle = - alignment === "center" - ? { alignItems: "center" as const } - : alignment === "right" - ? { alignItems: "flex-end" as const } - : { alignItems: "flex-start" as const }; - - if (!resolvedSrc.startsWith("http") && !resolvedSrc.startsWith("data:")) { - return ( - - [Image: {assetId.slice(0, 8)}...] - - ); - } - - const imageStyle = width && !isNaN(width) ? { width, maxHeight: 500 } : { maxWidth: 400, maxHeight: 500 }; - - return ( - - - - ); - }, - - calloutComponent: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const backgroundKey = (node.attrs?.["data-background"] as string) || "gray"; - const backgroundColor = - EDITOR_BACKGROUND_COLORS[backgroundKey as keyof typeof EDITOR_BACKGROUND_COLORS] || BACKGROUND_COLORS.layer3; - - return ( - - {getCalloutIcon(node, TEXT_COLORS.primary)} - {children} - - ); - }, - - mention: (node: TipTapNode, _children: ReactElement[], ctx: PDFRenderContext): ReactElement => { - const id = (node.attrs?.id as string) || ""; - const entityIdentifier = (node.attrs?.entity_identifier as string) || ""; - const entityName = (node.attrs?.entity_name as string) || ""; - - let displayText = entityName || id || entityIdentifier; - - if (ctx.metadata && (entityName === "user_mention" || entityName === "user")) { - const userMention = ctx.metadata.userMentions?.find((u) => u.id === entityIdentifier || u.id === id); - if (userMention) { - displayText = userMention.display_name; - } - } - - return ( - - @{displayText} - - ); - }, -}; - -type InternalRenderContext = { - parentType?: string; - nestingLevel: number; - listItemIndex: number; - textAlign?: string | null; - pdfContext: PDFRenderContext; -}; - -const renderNodeWithContext = (node: TipTapNode, context: InternalRenderContext): ReactElement => { - const { parentType, nestingLevel, listItemIndex, textAlign, pdfContext } = context; - - const isListContainer = node.type === CORE_EXTENSIONS.BULLET_LIST || node.type === CORE_EXTENSIONS.ORDERED_LIST; - - let childTextAlign = textAlign; - if (node.type === CORE_EXTENSIONS.PARAGRAPH && node.attrs?.textAlign) { - childTextAlign = node.attrs.textAlign as string; - } - - const nodeWithContext = { - ...node, - attrs: { - ...node.attrs, - _parentType: parentType, - _nestingLevel: nestingLevel, - _listItemIndex: listItemIndex, - _textAlign: childTextAlign, - _isHeader: node.content?.some((child) => child.type === CORE_EXTENSIONS.TABLE_HEADER), - }, - }; - - let childNestingLevel = nestingLevel; - if (isListContainer && parentType === CORE_EXTENSIONS.LIST_ITEM) { - childNestingLevel = nestingLevel + 1; - } - - let currentListItemIndex = 0; - const children: ReactElement[] = - node.content?.map((child) => { - const childContext: InternalRenderContext = { - parentType: node.type, - nestingLevel: childNestingLevel, - listItemIndex: 0, - textAlign: childTextAlign, - pdfContext, - }; - - if (isListContainer && child.type === CORE_EXTENSIONS.LIST_ITEM) { - currentListItemIndex++; - childContext.listItemIndex = currentListItemIndex; - } - - return renderNodeWithContext(child, childContext); - }) || []; - - const renderer = nodeRenderers[node.type]; - if (renderer) { - return renderer(nodeWithContext, children, pdfContext); - } - - if (children.length > 0) { - return {children}; - } - - return ; -}; - -export const renderNode = ( - node: TipTapNode, - parentType?: string, - _index?: number, - metadata?: PDFExportMetadata, - getKey?: KeyGenerator -): ReactElement => { - const keyGen = getKey ?? createKeyGenerator(); - - return renderNodeWithContext(node, { - parentType, - nestingLevel: 0, - listItemIndex: 0, - pdfContext: { getKey: keyGen, metadata }, - }); -}; diff --git a/apps/live/src/lib/pdf/plane-pdf-exporter.tsx b/apps/live/src/lib/pdf/plane-pdf-exporter.tsx deleted file mode 100644 index f6c6b599c..000000000 --- a/apps/live/src/lib/pdf/plane-pdf-exporter.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { createRequire } from "module"; -import path from "path"; -import { Document, Font, Page, pdf, Text } from "@react-pdf/renderer"; -import { createKeyGenerator, renderNode } from "./node-renderers"; -import { pdfStyles } from "./styles"; -import type { PDFExportOptions, TipTapDocument } from "./types"; - -// Use createRequire for ESM compatibility to resolve font file paths -const require = createRequire(import.meta.url); - -// Resolve local font file paths from @fontsource/inter package -const interFontDir = path.dirname(require.resolve("@fontsource/inter/package.json")); - -Font.register({ - family: "Inter", - fonts: [ - { - src: path.join(interFontDir, "files/inter-latin-400-normal.woff"), - fontWeight: 400, - }, - { - src: path.join(interFontDir, "files/inter-latin-400-italic.woff"), - fontWeight: 400, - fontStyle: "italic", - }, - { - src: path.join(interFontDir, "files/inter-latin-600-normal.woff"), - fontWeight: 600, - }, - { - src: path.join(interFontDir, "files/inter-latin-600-italic.woff"), - fontWeight: 600, - fontStyle: "italic", - }, - { - src: path.join(interFontDir, "files/inter-latin-700-normal.woff"), - fontWeight: 700, - }, - { - src: path.join(interFontDir, "files/inter-latin-700-italic.woff"), - fontWeight: 700, - fontStyle: "italic", - }, - ], -}); - -export const createPdfDocument = (doc: TipTapDocument, options: PDFExportOptions = {}) => { - const { title, author, subject, pageSize = "A4", pageOrientation = "portrait", metadata, noAssets } = options; - - // Merge noAssets into metadata for use in node renderers - const mergedMetadata = { ...metadata, noAssets }; - - const content = doc.content || []; - const getKey = createKeyGenerator(); - const renderedContent = content.map((node, index) => renderNode(node, "doc", index, mergedMetadata, getKey)); - - return ( - - - {title && {title}} - {renderedContent} - - - ); -}; - -export const renderPlaneDocToPdfBuffer = async ( - doc: TipTapDocument, - options: PDFExportOptions = {} -): Promise => { - const pdfDocument = createPdfDocument(doc, options); - const pdfInstance = pdf(pdfDocument); - const blob = await pdfInstance.toBlob(); - const arrayBuffer = await blob.arrayBuffer(); - return Buffer.from(arrayBuffer); -}; - -export const renderPlaneDocToPdfBlob = async (doc: TipTapDocument, options: PDFExportOptions = {}): Promise => { - const pdfDocument = createPdfDocument(doc, options); - const pdfInstance = pdf(pdfDocument); - return await pdfInstance.toBlob(); -}; diff --git a/apps/live/src/lib/pdf/styles.ts b/apps/live/src/lib/pdf/styles.ts deleted file mode 100644 index b55a156d0..000000000 --- a/apps/live/src/lib/pdf/styles.ts +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { StyleSheet } from "@react-pdf/renderer"; -import { - BACKGROUND_COLORS, - BORDER_COLORS, - BRAND_COLORS, - CODE_COLORS, - LINK_COLORS, - MENTION_COLORS, - TEXT_COLORS, -} from "./colors"; - -export const pdfStyles = StyleSheet.create({ - page: { - padding: 40, - fontFamily: "Inter", - fontSize: 11, - lineHeight: 1.6, - color: TEXT_COLORS.primary, - }, - title: { - fontSize: 24, - fontWeight: 600, - marginBottom: 20, - color: TEXT_COLORS.primary, - }, - heading1: { - fontSize: 20, - fontWeight: 600, - marginTop: 16, - marginBottom: 8, - color: TEXT_COLORS.primary, - }, - heading2: { - fontSize: 16, - fontWeight: 600, - marginTop: 14, - marginBottom: 6, - color: TEXT_COLORS.primary, - }, - heading3: { - fontSize: 14, - fontWeight: 600, - marginTop: 12, - marginBottom: 4, - color: TEXT_COLORS.primary, - }, - heading4: { - fontSize: 12, - fontWeight: 600, - marginTop: 10, - marginBottom: 4, - color: TEXT_COLORS.secondary, - }, - heading5: { - fontSize: 11, - fontWeight: 600, - marginTop: 8, - marginBottom: 4, - color: TEXT_COLORS.secondary, - }, - heading6: { - fontSize: 10, - fontWeight: 600, - marginTop: 6, - marginBottom: 4, - color: TEXT_COLORS.tertiary, - }, - paragraph: { - marginBottom: 0, - }, - paragraphWrapper: { - marginBottom: 8, - }, - blockquote: { - borderLeftWidth: 3, - borderLeftColor: BORDER_COLORS.strong, // Matches .ProseMirror blockquote border-strong - paddingLeft: 12, - marginLeft: 0, - marginVertical: 8, - fontStyle: "normal", // Matches editor: font-style: normal - fontWeight: 400, // Matches editor: font-weight: 400 - color: TEXT_COLORS.primary, - breakInside: "avoid", - }, - codeBlock: { - backgroundColor: BACKGROUND_COLORS.layer1, // bg-layer-1 equivalent - padding: 12, - borderRadius: 4, - fontFamily: "Courier", - fontSize: 10, - marginVertical: 8, - color: TEXT_COLORS.primary, - breakInside: "avoid", - }, - codeInline: { - backgroundColor: BACKGROUND_COLORS.layer1, - padding: 2, - paddingHorizontal: 4, - borderRadius: 2, - fontFamily: "Courier", - fontSize: 10, - color: CODE_COLORS.text, // Red for inline code - }, - bulletList: { - marginVertical: 8, - paddingLeft: 0, - }, - orderedList: { - marginVertical: 8, - paddingLeft: 0, - }, - listItem: { - display: "flex", - flexDirection: "row", - gap: 6, - marginBottom: 4, - paddingRight: 10, - breakInside: "avoid", - }, - listItemBullet: {}, - listItemContent: { - flex: 1, - }, - taskList: { - marginVertical: 8, - }, - taskItem: { - display: "flex", - flexDirection: "row", - gap: 6, - marginBottom: 4, - alignItems: "flex-start", - paddingRight: 10, - breakInside: "avoid", - }, - taskCheckbox: { - width: 12, - height: 12, - borderWidth: 1, - borderColor: BORDER_COLORS.strong, // Matches editor: border-strong - borderRadius: 2, - marginTop: 2, - alignItems: "center", - justifyContent: "center", - }, - taskCheckboxChecked: { - backgroundColor: BRAND_COLORS.default, // --background-color-accent-primary - borderColor: BRAND_COLORS.default, // --border-color-accent-strong - }, - table: { - marginVertical: 8, - borderWidth: 1, - borderColor: BORDER_COLORS.subtle1, // border-subtle-1 - }, - tableRow: { - flexDirection: "row", - borderBottomWidth: 1, - borderBottomColor: BORDER_COLORS.subtle1, - breakInside: "avoid", - }, - tableHeaderRow: { - backgroundColor: BACKGROUND_COLORS.surface2, // Slightly different from white - flexDirection: "row", - borderBottomWidth: 1, - borderBottomColor: BORDER_COLORS.subtle1, - }, - tableCell: { - padding: 8, - borderRightWidth: 1, - borderRightColor: BORDER_COLORS.subtle1, - flex: 1, - }, - tableHeaderCell: { - padding: 8, - borderRightWidth: 1, - borderRightColor: BORDER_COLORS.subtle1, - flex: 1, - fontWeight: "bold", - }, - horizontalRule: { - borderBottomWidth: 1, - borderBottomColor: BORDER_COLORS.subtle1, // Matches div[data-type="horizontalRule"] border-subtle-1 - marginVertical: 16, - }, - image: { - maxWidth: "100%", - marginVertical: 8, - }, - imagePlaceholder: { - backgroundColor: BACKGROUND_COLORS.layer1, - padding: 16, - borderRadius: 4, - marginVertical: 8, - alignItems: "center", - justifyContent: "center", - borderWidth: 1, - borderColor: BORDER_COLORS.subtle, - borderStyle: "dashed", - }, - imagePlaceholderText: { - color: TEXT_COLORS.tertiary, - fontSize: 10, - }, - callout: { - backgroundColor: BACKGROUND_COLORS.layer3, // bg-layer-3 (default callout background) - padding: 12, - borderRadius: 6, - marginVertical: 8, - flexDirection: "row", - alignItems: "flex-start", - breakInside: "avoid", - }, - calloutIconContainer: { - marginRight: 10, - marginTop: 2, - }, - calloutContent: { - flex: 1, - color: TEXT_COLORS.primary, // text-primary - }, - mention: { - backgroundColor: MENTION_COLORS.background, // bg-accent-primary/20 equivalent - color: MENTION_COLORS.text, // text-accent-primary - padding: 2, - paddingHorizontal: 4, - borderRadius: 2, - }, - link: { - color: LINK_COLORS.primary, // --txt-link-primary - textDecoration: "underline", - }, - bold: { - fontWeight: "bold", - }, - italic: { - fontStyle: "italic", - }, - underline: { - textDecoration: "underline", - }, - strike: { - textDecoration: "line-through", - }, -}); diff --git a/apps/live/src/lib/pdf/types.ts b/apps/live/src/lib/pdf/types.ts deleted file mode 100644 index 0578a49c5..000000000 --- a/apps/live/src/lib/pdf/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { Style } from "@react-pdf/types"; - -export type TipTapMark = { - type: string; - attrs?: Record; -}; - -export type TipTapNode = { - type: string; - attrs?: Record; - content?: TipTapNode[]; - text?: string; - marks?: TipTapMark[]; -}; - -export type TipTapDocument = { - type: "doc"; - content?: TipTapNode[]; -}; - -export type KeyGenerator = () => string; - -export type PDFRenderContext = { - getKey: KeyGenerator; - metadata?: PDFExportMetadata; -}; - -export type PDFNodeRenderer = ( - node: TipTapNode, - children: React.ReactElement[], - context: PDFRenderContext -) => React.ReactElement; - -export type PDFMarkRenderer = (mark: TipTapMark, currentStyle: Style) => Style; - -export type NodeRendererRegistry = Record; - -export type MarkRendererRegistry = Record; - -export type PDFExportOptions = { - title?: string; - author?: string; - subject?: string; - pageSize?: "A4" | "A3" | "A2" | "LETTER" | "LEGAL" | "TABLOID"; - pageOrientation?: "portrait" | "landscape"; - metadata?: PDFExportMetadata; - /** When true, images and other assets are excluded from the PDF */ - noAssets?: boolean; -}; - -/** - * Metadata for resolving entity references in PDF export - */ -export type PDFExportMetadata = { - /** User mentions (user_mention in mention node) */ - userMentions?: PDFUserMention[]; - /** Resolved image URLs: Map of asset ID to presigned URL */ - resolvedImageUrls?: Record; - /** When true, images and other assets are excluded from the PDF */ - noAssets?: boolean; -}; - -export type PDFUserMention = { - id: string; - display_name: string; - avatar_url?: string; -}; diff --git a/apps/live/src/lib/stateless.ts b/apps/live/src/lib/stateless.ts deleted file mode 100644 index d59f8fe99..000000000 --- a/apps/live/src/lib/stateless.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { onStatelessPayload } from "@hocuspocus/server"; -import { DocumentCollaborativeEvents } from "@plane/editor/lib"; -import type { TDocumentEventsServer } from "@plane/editor/lib"; - -/** - * Broadcast the client event to all the clients so that they can update their state - * @param param0 - */ -export const onStateless = async ({ payload, document }: onStatelessPayload) => { - const response = DocumentCollaborativeEvents[payload as TDocumentEventsServer]?.client; - if (response) { - document.broadcastStateless(response); - } -}; diff --git a/apps/live/src/redis.ts b/apps/live/src/redis.ts deleted file mode 100644 index f23374a79..000000000 --- a/apps/live/src/redis.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import Redis from "ioredis"; -import { logger } from "@plane/logger"; -import { env } from "./env"; - -export class RedisManager { - private static instance: RedisManager; - private redisClient: Redis | null = null; - private isConnected: boolean = false; - private connectionPromise: Promise | null = null; - - private constructor() {} - - public static getInstance(): RedisManager { - if (!RedisManager.instance) { - RedisManager.instance = new RedisManager(); - } - return RedisManager.instance; - } - - public async initialize(): Promise { - if (this.redisClient && this.isConnected) { - logger.info("REDIS_MANAGER: client already initialized and connected"); - return; - } - - if (this.connectionPromise) { - logger.info("REDIS_MANAGER: Redis connection already in progress, waiting..."); - await this.connectionPromise; - return; - } - - this.connectionPromise = this.connect(); - await this.connectionPromise; - } - - private getRedisUrl(): string { - const redisUrl = env.REDIS_URL; - const redisHost = env.REDIS_HOST; - const redisPort = env.REDIS_PORT; - - if (redisUrl) { - return redisUrl; - } - - if (redisHost && redisPort && !Number.isNaN(Number(redisPort))) { - return `redis://${redisHost}:${redisPort}`; - } - - return ""; - } - - private async connect(): Promise { - try { - const redisUrl = this.getRedisUrl(); - - if (!redisUrl) { - logger.warn("REDIS_MANAGER: No Redis URL provided, Redis functionality will be disabled"); - this.isConnected = false; - return; - } - - // Configuration optimized for BOTH regular operations AND pub/sub - // HocuspocusRedis uses .duplicate() which inherits these settings - this.redisClient = new Redis(redisUrl, { - lazyConnect: false, // Connect immediately for reliability (duplicates inherit this) - keepAlive: 30000, - connectTimeout: 10000, - maxRetriesPerRequest: 3, - enableOfflineQueue: true, // Keep commands queued during reconnection - retryStrategy: (times: number) => { - // Exponential backoff with max 2 seconds - const delay = Math.min(times * 50, 2000); - logger.info(`REDIS_MANAGER: Reconnection attempt ${times}, delay: ${delay}ms`); - return delay; - }, - }); - - // Set up event listeners - this.redisClient.on("connect", () => { - logger.info("REDIS_MANAGER: Redis client connected"); - this.isConnected = true; - }); - - this.redisClient.on("ready", () => { - logger.info("REDIS_MANAGER: Redis client ready"); - this.isConnected = true; - }); - - this.redisClient.on("error", (error) => { - logger.error("REDIS_MANAGER: Redis client error:", error); - this.isConnected = false; - }); - - this.redisClient.on("close", () => { - logger.warn("REDIS_MANAGER: Redis client connection closed"); - this.isConnected = false; - }); - - this.redisClient.on("reconnecting", () => { - logger.info("REDIS_MANAGER: Redis client reconnecting..."); - this.isConnected = false; - }); - - await this.redisClient.ping(); - logger.info("REDIS_MANAGER: Redis connection test successful"); - } catch (error) { - logger.error("REDIS_MANAGER: Failed to initialize Redis client:", error); - this.isConnected = false; - throw error; - } finally { - this.connectionPromise = null; - } - } - - public getClient(): Redis | null { - if (!this.redisClient || !this.isConnected) { - logger.warn("REDIS_MANAGER: Redis client not available or not connected"); - return null; - } - return this.redisClient; - } - - public isClientConnected(): boolean { - return this.isConnected && this.redisClient !== null; - } - - public async disconnect(): Promise { - if (this.redisClient) { - try { - await this.redisClient.quit(); - logger.info("REDIS_MANAGER: Redis client disconnected gracefully"); - } catch (error) { - logger.error("REDIS_MANAGER: Error disconnecting Redis client:", error); - // Force disconnect if quit fails - this.redisClient.disconnect(); - } finally { - this.redisClient = null; - this.isConnected = false; - } - } - } - - // Convenience methods for common Redis operations - public async set(key: string, value: string, ttl?: number): Promise { - const client = this.getClient(); - if (!client) return false; - - try { - if (ttl) { - await client.setex(key, ttl, value); - } else { - await client.set(key, value); - } - return true; - } catch (error) { - logger.error(`REDIS_MANAGER: Error setting Redis key ${key}:`, error); - return false; - } - } - - public async get(key: string): Promise { - const client = this.getClient(); - if (!client) return null; - - try { - return await client.get(key); - } catch (error) { - logger.error(`REDIS_MANAGER: Error getting Redis key ${key}:`, error); - return null; - } - } - - public async del(key: string): Promise { - const client = this.getClient(); - if (!client) return false; - - try { - await client.del(key); - return true; - } catch (error) { - logger.error(`REDIS_MANAGER: Error deleting Redis key ${key}:`, error); - return false; - } - } - - public async exists(key: string): Promise { - const client = this.getClient(); - if (!client) return false; - - try { - const result = await client.exists(key); - return result === 1; - } catch (error) { - logger.error(`REDIS_MANAGER: Error checking Redis key ${key}:`, error); - return false; - } - } - - public async expire(key: string, ttl: number): Promise { - const client = this.getClient(); - if (!client) return false; - - try { - const result = await client.expire(key, ttl); - return result === 1; - } catch (error) { - logger.error(`REDIS_MANAGER: Error setting expiry for Redis key ${key}:`, error); - return false; - } - } -} - -// Export a default instance for convenience -export const redisManager = RedisManager.getInstance(); diff --git a/apps/live/src/schema/pdf-export.ts b/apps/live/src/schema/pdf-export.ts deleted file mode 100644 index e3085eefa..000000000 --- a/apps/live/src/schema/pdf-export.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Schema } from "effect"; - -export const PdfExportRequestBody = Schema.Struct({ - pageId: Schema.NonEmptyTrimmedString, - workspaceSlug: Schema.NonEmptyTrimmedString, - projectId: Schema.optional(Schema.NonEmptyTrimmedString), - title: Schema.optional(Schema.String), - author: Schema.optional(Schema.String), - subject: Schema.optional(Schema.String), - pageSize: Schema.optional(Schema.Literal("A4", "A3", "A2", "LETTER", "LEGAL", "TABLOID")), - pageOrientation: Schema.optional(Schema.Literal("portrait", "landscape")), - fileName: Schema.optional(Schema.String), - noAssets: Schema.optional(Schema.Boolean), -}); - -export type TPdfExportRequestBody = Schema.Schema.Type; - -export class PdfValidationError extends Schema.TaggedError()("PdfValidationError", { - message: Schema.NonEmptyTrimmedString, - cause: Schema.optional(Schema.Unknown), -}) {} - -export class PdfAuthenticationError extends Schema.TaggedError()("PdfAuthenticationError", { - message: Schema.NonEmptyTrimmedString, -}) {} - -export class PdfContentFetchError extends Schema.TaggedError()("PdfContentFetchError", { - message: Schema.NonEmptyTrimmedString, - cause: Schema.optional(Schema.Unknown), -}) {} - -export class PdfMetadataFetchError extends Schema.TaggedError()("PdfMetadataFetchError", { - message: Schema.NonEmptyTrimmedString, - source: Schema.Literal("user-mentions"), - cause: Schema.optional(Schema.Unknown), -}) {} - -export class PdfImageProcessingError extends Schema.TaggedError()("PdfImageProcessingError", { - message: Schema.NonEmptyTrimmedString, - assetId: Schema.NonEmptyTrimmedString, - cause: Schema.optional(Schema.Unknown), -}) {} - -export class PdfGenerationError extends Schema.TaggedError()("PdfGenerationError", { - message: Schema.NonEmptyTrimmedString, - cause: Schema.optional(Schema.Unknown), -}) {} - -export class PdfTimeoutError extends Schema.TaggedError()("PdfTimeoutError", { - message: Schema.NonEmptyTrimmedString, - operation: Schema.NonEmptyTrimmedString, -}) {} - -export type PdfExportError = - | PdfValidationError - | PdfAuthenticationError - | PdfContentFetchError - | PdfMetadataFetchError - | PdfImageProcessingError - | PdfGenerationError - | PdfTimeoutError; diff --git a/apps/live/src/server.ts b/apps/live/src/server.ts deleted file mode 100644 index 9a3906bf7..000000000 --- a/apps/live/src/server.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { Server as HttpServer } from "http"; -import type { Hocuspocus } from "@hocuspocus/server"; -import compression from "compression"; -import cors from "cors"; -import type { Express, Request, Response, Router } from "express"; -import express from "express"; -import expressWs from "express-ws"; -import helmet from "helmet"; -// plane imports -import { registerController } from "@plane/decorators"; -import { logger, loggerMiddleware } from "@plane/logger"; -// controllers -import { CONTROLLERS } from "@/controllers"; -// env -import { env } from "@/env"; -// hocuspocus server -import { HocusPocusServerManager } from "@/hocuspocus"; -// redis -import { redisManager } from "@/redis"; - -export class Server { - private app: Express; - private router: Router; - private hocuspocusServer: Hocuspocus | undefined; - private httpServer: HttpServer | undefined; - - constructor() { - this.app = express(); - expressWs(this.app); - this.setupMiddleware(); - this.router = express.Router(); - this.app.set("port", env.PORT || 3000); - this.app.use(env.LIVE_BASE_PATH, this.router); - } - - public async initialize(): Promise { - try { - await redisManager.initialize(); - logger.info("SERVER: Redis setup completed"); - const manager = HocusPocusServerManager.getInstance(); - this.hocuspocusServer = await manager.initialize(); - logger.info("SERVER: HocusPocus setup completed"); - this.setupRoutes(this.hocuspocusServer); - this.setupNotFoundHandler(); - } catch (error) { - logger.error("SERVER: Failed to initialize live server dependencies:", error); - throw error; - } - } - - private setupMiddleware() { - // Security middleware - this.app.use(helmet()); - // Middleware for response compression - this.app.use(compression({ level: env.COMPRESSION_LEVEL, threshold: env.COMPRESSION_THRESHOLD })); - // Logging middleware - this.app.use(loggerMiddleware); - // Body parsing middleware - this.app.use(express.json()); - this.app.use(express.urlencoded({ extended: true })); - // cors middleware - this.setupCors(); - } - - private setupCors() { - const allowedOrigins = env.CORS_ALLOWED_ORIGINS.split(",").map((s) => s.trim()); - this.app.use( - cors({ - origin: allowedOrigins.length > 0 ? allowedOrigins : false, - credentials: true, - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allowedHeaders: ["Content-Type", "Authorization", "x-api-key"], - }) - ); - } - - private setupNotFoundHandler() { - this.app.use((_req: Request, res: Response) => { - res.status(404).json({ - message: "Not Found", - }); - }); - } - - private setupRoutes(hocuspocusServer: Hocuspocus) { - CONTROLLERS.forEach((controller) => registerController(this.router, controller, [hocuspocusServer])); - } - - public listen() { - this.httpServer = this.app - .listen(this.app.get("port"), () => { - logger.info(`SERVER: Express server has started at port ${this.app.get("port")}`); - }) - .on("error", (err) => { - logger.error("SERVER: Failed to start server:", err); - throw err; - }); - } - - public async destroy() { - if (this.hocuspocusServer) { - this.hocuspocusServer.closeConnections(); - logger.info("SERVER: HocusPocus connections closed gracefully."); - } - - await redisManager.disconnect(); - logger.info("SERVER: Redis connection closed gracefully."); - - if (this.httpServer) { - await new Promise((resolve, reject) => { - this.httpServer!.close((err) => { - if (err) { - reject(err); - } else { - logger.info("SERVER: Express server closed gracefully."); - resolve(); - } - }); - }); - } - } -} diff --git a/apps/live/src/services/api.service.ts b/apps/live/src/services/api.service.ts deleted file mode 100644 index b04655a0b..000000000 --- a/apps/live/src/services/api.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { AxiosInstance } from "axios"; -import { create } from "axios"; -import { env } from "@/env"; -import { AppError } from "@/lib/errors"; - -export abstract class APIService { - protected baseURL: string; - private axiosInstance: AxiosInstance; - private header: Record = {}; - - constructor(baseURL?: string) { - this.baseURL = baseURL || env.API_BASE_URL; - this.axiosInstance = create({ - baseURL: this.baseURL, - withCredentials: true, - timeout: 20000, - }); - this.setupInterceptors(); - } - - private setupInterceptors() { - this.axiosInstance.interceptors.response.use( - (response) => response, - (error) => { - return Promise.reject(new AppError(error)); - } - ); - } - - setHeader(key: string, value: string) { - this.header[key] = value; - } - - getHeader() { - return this.header; - } - - get(url: string, params = {}, config = {}) { - return this.axiosInstance.get(url, { - ...params, - ...config, - }); - } - - post(url: string, data = {}, config = {}) { - return this.axiosInstance.post(url, data, config); - } - - put(url: string, data = {}, config = {}) { - return this.axiosInstance.put(url, data, config); - } - - patch(url: string, data = {}, config = {}) { - return this.axiosInstance.patch(url, data, config); - } - - delete(url: string, data?: Record | null | string, config = {}) { - return this.axiosInstance.delete(url, { data, ...config }); - } - - request(config = {}) { - return this.axiosInstance(config); - } -} diff --git a/apps/live/src/services/page/core.service.ts b/apps/live/src/services/page/core.service.ts deleted file mode 100644 index 3b66accfd..000000000 --- a/apps/live/src/services/page/core.service.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { logger } from "@plane/logger"; -import type { TDocumentPayload, TPage } from "@plane/types"; -// services -import { AppError } from "@/lib/errors"; -import { APIService } from "../api.service"; - -export type TUserMention = { - id: string; - display_name: string; - avatar_url?: string; -}; - -export abstract class PageCoreService extends APIService { - protected abstract basePath: string; - - - - async fetchDetails(pageId: string): Promise { - try { - const response = await this.get(`${this.basePath}/pages/${pageId}/`, { - headers: this.getHeader(), - }); - return response?.data as TPage; - } catch (error) { - const appError = new AppError(error, { - context: { operation: "fetchDetails", pageId }, - }); - logger.error("Failed to fetch page details", appError); - throw appError; - } - } - - async fetchDescriptionBinary(pageId: string): Promise { - try { - const response = await this.get(`${this.basePath}/pages/${pageId}/description/`, { - headers: { - ...this.getHeader(), - "Content-Type": "application/octet-stream", - }, - responseType: "arraybuffer", - }); - const data = response?.data; - if (!Buffer.isBuffer(data)) { - throw new Error("Expected response to be a Buffer"); - } - return data; - } catch (error) { - const appError = new AppError(error, { - context: { operation: "fetchDescriptionBinary", pageId }, - }); - logger.error("Failed to fetch page description binary", appError); - throw appError; - } - } - - /** - * Updates the title of a page - */ - async updatePageProperties( - pageId: string, - params: { data: Partial; abortSignal?: AbortSignal } - ): Promise { - const { data, abortSignal } = params; - - // Early abort check - if (abortSignal?.aborted) { - throw new AppError(new DOMException("Aborted", "AbortError")); - } - - // Create an abort listener that will reject the pending promise - let abortListener: (() => void) | undefined; - const abortPromise = new Promise((_, reject) => { - if (abortSignal) { - abortListener = () => { - reject(new AppError(new DOMException("Aborted", "AbortError"))); - }; - abortSignal.addEventListener("abort", abortListener); - } - }); - - try { - return await Promise.race([ - this.patch(`${this.basePath}/pages/${pageId}/`, data, { - headers: this.getHeader(), - signal: abortSignal, - }) - .then((response) => response?.data) - .catch((error) => { - const appError = new AppError(error, { - context: { operation: "updatePageProperties", pageId }, - }); - - if (appError.code === "ABORT_ERROR") { - throw appError; - } - - logger.error("Failed to update page properties", appError); - throw appError; - }), - abortPromise, - ]); - } finally { - // Clean up abort listener - if (abortSignal && abortListener) { - abortSignal.removeEventListener("abort", abortListener); - } - } - } - - async updateDescriptionBinary(pageId: string, data: TDocumentPayload): Promise { - try { - const response = await this.patch(`${this.basePath}/pages/${pageId}/description/`, data, { - headers: this.getHeader(), - }); - return response?.data as unknown; - } catch (error) { - const appError = new AppError(error, { - context: { operation: "updateDescriptionBinary", pageId }, - }); - logger.error("Failed to update page description binary", appError); - throw appError; - } - } - - /** - * Fetches user mentions for a page - * @param pageId - The page ID - * @returns Array of user mentions - */ - async fetchUserMentions(pageId: string): Promise { - try { - const response = await this.get(`${this.basePath}/pages/${pageId}/mentions/`, { - headers: this.getHeader(), - params: { - mention_type: "user_mention", - }, - }); - return (response?.data as TUserMention[]) ?? []; - } catch (error) { - const appError = new AppError(error, { - context: { operation: "fetchUserMentions", pageId }, - }); - logger.error("Failed to fetch user mentions", appError); - throw appError; - } - } - - /** - * Resolves an image asset ID to its actual URL by following the 302 redirect - * @param workspaceSlug - The workspace slug - * @param assetId - The asset UUID - * @param projectId - Optional project ID for project-specific assets - * @returns The resolved image URL (presigned S3 URL) - */ - async resolveImageAssetUrl( - workspaceSlug: string, - assetId: string, - projectId?: string | null - ): Promise { - const path = projectId - ? `/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${assetId}/?disposition=inline` - : `/api/assets/v2/workspaces/${workspaceSlug}/${assetId}/?disposition=inline`; - - try { - const response = await this.get(path, { - headers: this.getHeader(), - maxRedirects: 0, - validateStatus: (status: number) => status >= 200 && status < 400, - }); - // If we get a 302, the Location header contains the presigned URL - if (response.status === 302 || response.status === 301) { - return response.headers?.location || null; - } - return null; - } catch (error) { - // Axios throws on 3xx when maxRedirects is 0, so we need to handle the redirect from the error - if ((error as any).response?.status === 302 || (error as any).response?.status === 301) { - return (error as any).response.headers?.location || null; - } - logger.error("Failed to resolve image asset URL", { - assetId, - workspaceSlug, - error: (error as any).message, - }); - return null; - } - } - - /** - * Resolves multiple image asset IDs to their actual URLs - * @param workspaceSlug - The workspace slug - * @param assetIds - Array of asset UUIDs - * @param projectId - Optional project ID for project-specific assets - * @returns Map of assetId to resolved URL - */ - async resolveImageAssetUrls( - workspaceSlug: string, - assetIds: string[], - projectId?: string | null - ): Promise> { - const urlMap = new Map(); - - // Resolve all asset URLs in parallel - const results = await Promise.allSettled( - assetIds.map(async (assetId) => { - const url = await this.resolveImageAssetUrl(workspaceSlug, assetId, projectId); - return { assetId, url }; - }) - ); - - for (const result of results) { - if (result.status === "fulfilled" && result.value.url) { - urlMap.set(result.value.assetId, result.value.url); - } - } - - return urlMap; - } -} diff --git a/apps/live/src/services/page/extended.service.ts b/apps/live/src/services/page/extended.service.ts deleted file mode 100644 index 01bb78a46..000000000 --- a/apps/live/src/services/page/extended.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { PageCoreService } from "./core.service"; - -/** - * This is the extended service for the page service. - * It extends the core service and adds additional functionality. - * Implementation for this is found in the enterprise repository. - */ -export abstract class PageService extends PageCoreService { - -} diff --git a/apps/live/src/services/page/handler.ts b/apps/live/src/services/page/handler.ts deleted file mode 100644 index 2bfd0b1dd..000000000 --- a/apps/live/src/services/page/handler.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { AppError } from "@/lib/errors"; -import type { HocusPocusServerContext, TDocumentTypes } from "@/types"; -// services -import { ProjectPageService } from "./project-page.service"; - -export const getPageService = (documentType: TDocumentTypes, context: HocusPocusServerContext) => { - if (documentType === "project_page") { - return new ProjectPageService({ - workspaceSlug: context.workspaceSlug, - projectId: context.projectId, - cookie: context.cookie, - }); - } - - throw new AppError(`Invalid document type ${documentType} provided.`); -}; diff --git a/apps/live/src/services/page/project-page.service.ts b/apps/live/src/services/page/project-page.service.ts deleted file mode 100644 index d89ab0aa7..000000000 --- a/apps/live/src/services/page/project-page.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { AppError } from "@/lib/errors"; -import { PageService } from "./extended.service"; - -interface ProjectPageServiceParams { - workspaceSlug: string | null; - projectId: string | null; - cookie: string | null; - [key: string]: unknown; -} - -export class ProjectPageService extends PageService { - protected basePath: string; - - constructor(params: ProjectPageServiceParams) { - super(); - const { workspaceSlug, projectId } = params; - if (!workspaceSlug || !projectId) throw new AppError("Missing required fields."); - // validate cookie - if (!params.cookie) throw new AppError("Cookie is required."); - // set cookie - this.setHeader("Cookie", params.cookie); - // set base path - this.basePath = `/api/workspaces/${workspaceSlug}/projects/${projectId}`; - } -} diff --git a/apps/live/src/services/pdf-export/effect-utils.ts b/apps/live/src/services/pdf-export/effect-utils.ts deleted file mode 100644 index 18f40b089..000000000 --- a/apps/live/src/services/pdf-export/effect-utils.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Effect, Duration, Schedule, pipe } from "effect"; -import { PdfTimeoutError } from "@/schema/pdf-export"; - -/** - * Wraps an effect with timeout and exponential backoff retry logic. - * Preserves the environment type R for proper dependency injection. - */ -export const withTimeoutAndRetry = - (operation: string, { timeoutMs = 5000, maxRetries = 2 }: { timeoutMs?: number; maxRetries?: number } = {}) => - (effect: Effect.Effect): Effect.Effect => - effect.pipe( - Effect.timeoutFail({ - duration: Duration.millis(timeoutMs), - onTimeout: () => - new PdfTimeoutError({ - message: `Operation "${operation}" timed out after ${timeoutMs}ms`, - operation, - }), - }), - Effect.retry( - pipe( - Schedule.exponential(Duration.millis(200)), - Schedule.compose(Schedule.recurs(maxRetries)), - Schedule.tapInput((error: E | PdfTimeoutError) => - Effect.logWarning("PDF_EXPORT: Retrying operation", { operation, error }) - ) - ) - ) - ); - -/** - * Recovers from any error with a default fallback value. - * Logs the error before recovering. - */ -export const recoverWithDefault = - (fallback: A) => - (effect: Effect.Effect): Effect.Effect => - effect.pipe( - Effect.tapError((error) => Effect.logWarning("PDF_EXPORT: Operation failed, using fallback", { error })), - Effect.catchAll(() => Effect.succeed(fallback)) - ); - -/** - * Wraps a promise-returning function with proper Effect error handling - */ -export const tryAsync = (fn: () => Promise, onError: (cause: unknown) => E): Effect.Effect => - Effect.tryPromise({ - try: fn, - catch: onError, - }); diff --git a/apps/live/src/services/pdf-export/index.ts b/apps/live/src/services/pdf-export/index.ts deleted file mode 100644 index fa2a7c68d..000000000 --- a/apps/live/src/services/pdf-export/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export { PdfExportService, exportToPdf } from "./pdf-export.service"; -export * from "./effect-utils"; -export * from "./types"; diff --git a/apps/live/src/services/pdf-export/pdf-export.service.ts b/apps/live/src/services/pdf-export/pdf-export.service.ts deleted file mode 100644 index e9c67fc36..000000000 --- a/apps/live/src/services/pdf-export/pdf-export.service.ts +++ /dev/null @@ -1,379 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Effect } from "effect"; -import sharp from "sharp"; -import { getAllDocumentFormatsFromDocumentEditorBinaryData } from "@plane/editor/lib"; -import type { PDFExportMetadata, TipTapDocument } from "@/lib/pdf"; -import { renderPlaneDocToPdfBuffer } from "@/lib/pdf"; -import { getPageService } from "@/services/page/handler"; -import type { TDocumentTypes } from "@/types"; -import { - PdfContentFetchError, - PdfGenerationError, - PdfImageProcessingError, - PdfTimeoutError, -} from "@/schema/pdf-export"; -import { withTimeoutAndRetry, recoverWithDefault, tryAsync } from "./effect-utils"; -import type { PdfExportInput, PdfExportResult, PageContent, MetadataResult } from "./types"; - -const IMAGE_CONCURRENCY = 4; -const IMAGE_TIMEOUT_MS = 8000; -const CONTENT_FETCH_TIMEOUT_MS = 7000; -const PDF_RENDER_TIMEOUT_MS = 15000; -const IMAGE_MAX_DIMENSION = 1200; - -type TipTapNode = { - type: string; - attrs?: Record; - content?: TipTapNode[]; -}; - -/** - * PDF Export Service - */ -export class PdfExportService extends Effect.Service()("PdfExportService", { - sync: () => ({ - /** - * Determines document type - */ - getDocumentType: (_input: PdfExportInput): TDocumentTypes => { - return "project_page"; - }, - - /** - * Extracts image asset IDs from document content - */ - extractImageAssetIds: (doc: TipTapNode): string[] => { - const assetIds: string[] = []; - - const traverse = (node: TipTapNode) => { - if ((node.type === "imageComponent" || node.type === "image") && node.attrs?.src) { - const src = node.attrs.src as string; - if (src && !src.startsWith("http") && !src.startsWith("data:")) { - assetIds.push(src); - } - } - if (node.content) { - for (const child of node.content) { - traverse(child); - } - } - }; - - traverse(doc); - return [...new Set(assetIds)]; - }, - - /** - * Fetches page content (description binary) and parses it - */ - fetchPageContent: ( - pageService: ReturnType, - pageId: string, - requestId: string - ): Effect.Effect => - Effect.gen(function* () { - yield* Effect.logDebug("PDF_EXPORT: Fetching page content", { requestId, pageId }); - - const descriptionBinary = yield* tryAsync( - () => pageService.fetchDescriptionBinary(pageId), - (cause) => - new PdfContentFetchError({ - message: "Failed to fetch page content", - cause, - }) - ).pipe( - withTimeoutAndRetry("fetch page content", { - timeoutMs: CONTENT_FETCH_TIMEOUT_MS, - maxRetries: 3, - }) - ); - - if (!descriptionBinary) { - return yield* Effect.fail( - new PdfContentFetchError({ - message: "Page content not found", - }) - ); - } - - const binaryData = new Uint8Array(descriptionBinary); - const { contentJSON, titleHTML } = getAllDocumentFormatsFromDocumentEditorBinaryData(binaryData, true); - - return { - contentJSON: contentJSON as TipTapDocument, - titleHTML: titleHTML || null, - descriptionBinary, - }; - }), - - /** - * Fetches user mentions for the page - */ - fetchUserMentions: ( - pageService: ReturnType, - pageId: string, - requestId: string - ): Effect.Effect => - Effect.gen(function* () { - yield* Effect.logDebug("PDF_EXPORT: Fetching user mentions", { requestId }); - - const userMentionsRaw = yield* tryAsync( - async () => { - if (pageService.fetchUserMentions) { - return await pageService.fetchUserMentions(pageId); - } - return []; - }, - () => [] - ).pipe(recoverWithDefault([] as Array<{ id: string; display_name: string; avatar_url?: string }>)); - - return { - userMentions: userMentionsRaw.map((u) => ({ - id: u.id, - display_name: u.display_name, - avatar_url: u.avatar_url, - })), - }; - }), - - /** - * Resolves and processes images for PDF embedding - */ - processImages: ( - pageService: ReturnType, - workspaceSlug: string, - projectId: string | undefined, - assetIds: string[], - requestId: string - ): Effect.Effect> => - Effect.gen(function* () { - if (assetIds.length === 0) { - return {}; - } - - yield* Effect.logDebug("PDF_EXPORT: Processing images", { - requestId, - count: assetIds.length, - }); - - // Resolve URLs first - const resolvedUrlMap = yield* tryAsync( - async () => { - const urlMap = new Map(); - for (const assetId of assetIds) { - const url = await pageService.resolveImageAssetUrl?.(workspaceSlug, assetId, projectId); - if (url) urlMap.set(assetId, url); - } - return urlMap; - }, - () => new Map() - ).pipe(recoverWithDefault(new Map())); - - if (resolvedUrlMap.size === 0) { - return {}; - } - - // Process each image - const processSingleImage = ([assetId, url]: [string, string]) => - Effect.gen(function* () { - const response = yield* tryAsync( - () => fetch(url), - (cause) => - new PdfImageProcessingError({ - message: "Failed to fetch image", - assetId, - cause, - }) - ); - - if (!response.ok) { - return yield* Effect.fail( - new PdfImageProcessingError({ - message: `Image fetch returned ${response.status}`, - assetId, - }) - ); - } - - const arrayBuffer = yield* tryAsync( - () => response.arrayBuffer(), - (cause) => - new PdfImageProcessingError({ - message: "Failed to read image body", - assetId, - cause, - }) - ); - - const processedBuffer = yield* tryAsync( - () => - sharp(Buffer.from(arrayBuffer)) - .rotate() - .flatten({ background: { r: 255, g: 255, b: 255 } }) - .resize(IMAGE_MAX_DIMENSION, IMAGE_MAX_DIMENSION, { fit: "inside", withoutEnlargement: true }) - .jpeg({ quality: 85 }) - .toBuffer(), - (cause) => - new PdfImageProcessingError({ - message: "Failed to process image", - assetId, - cause, - }) - ); - - const base64 = processedBuffer.toString("base64"); - return [assetId, `data:image/jpeg;base64,${base64}`] as const; - }).pipe( - withTimeoutAndRetry(`process image ${assetId}`, { - timeoutMs: IMAGE_TIMEOUT_MS, - maxRetries: 1, - }), - Effect.tapError((error) => - Effect.logWarning("PDF_EXPORT: Image processing failed", { - requestId, - assetId, - error, - }) - ), - Effect.catchAll(() => Effect.succeed(null as readonly [string, string] | null)) - ); - - const entries = Array.from(resolvedUrlMap.entries()); - const pairs = yield* Effect.forEach(entries, processSingleImage, { - concurrency: IMAGE_CONCURRENCY, - }); - - const filtered = pairs.filter((p): p is readonly [string, string] => p !== null); - return Object.fromEntries(filtered); - }), - - /** - * Renders document to PDF buffer - */ - renderPdf: ( - contentJSON: TipTapDocument, - metadata: PDFExportMetadata, - options: { - title?: string; - author?: string; - subject?: string; - pageSize?: "A4" | "A3" | "A2" | "LETTER" | "LEGAL" | "TABLOID"; - pageOrientation?: "portrait" | "landscape"; - noAssets?: boolean; - }, - requestId: string - ): Effect.Effect => - Effect.gen(function* () { - yield* Effect.logDebug("PDF_EXPORT: Rendering PDF", { requestId }); - - const pdfBuffer = yield* tryAsync( - () => - renderPlaneDocToPdfBuffer(contentJSON, { - title: options.title, - author: options.author, - subject: options.subject, - pageSize: options.pageSize, - pageOrientation: options.pageOrientation, - metadata, - noAssets: options.noAssets, - }), - (cause) => - new PdfGenerationError({ - message: "Failed to render PDF", - cause, - }) - ).pipe(withTimeoutAndRetry("render PDF", { timeoutMs: PDF_RENDER_TIMEOUT_MS, maxRetries: 0 })); - - yield* Effect.logInfo("PDF_EXPORT: PDF rendered successfully", { - requestId, - size: pdfBuffer.length, - }); - - return pdfBuffer; - }), - }), -}) {} - -/** - * Main export pipeline - orchestrates the entire PDF export process - * Separate function to avoid circular dependency in service definition - */ -export const exportToPdf = ( - input: PdfExportInput -): Effect.Effect => - Effect.gen(function* () { - const service = yield* PdfExportService; - const { requestId, pageId, workspaceSlug, projectId, noAssets } = input; - - yield* Effect.logInfo("PDF_EXPORT: Starting export", { requestId, pageId, workspaceSlug }); - - // Create page service - const documentType = service.getDocumentType(input); - const pageService = getPageService(documentType, { - workspaceSlug, - projectId: projectId || null, - cookie: input.cookie, - documentType, - userId: "", - }); - - // Fetch content - const content = yield* service.fetchPageContent(pageService, pageId, requestId); - - // Extract image asset IDs - const imageAssetIds = service.extractImageAssetIds(content.contentJSON as TipTapNode); - - // Fetch user mentions - let metadata = yield* service.fetchUserMentions(pageService, pageId, requestId); - - // Process images if needed - if (!noAssets && imageAssetIds.length > 0) { - const resolvedImages = yield* service.processImages( - pageService, - workspaceSlug, - projectId, - imageAssetIds, - requestId - ); - metadata = { ...metadata, resolvedImageUrls: resolvedImages }; - } - - yield* Effect.logDebug("PDF_EXPORT: Metadata prepared", { - requestId, - userMentions: metadata.userMentions?.length ?? 0, - resolvedImages: Object.keys(metadata.resolvedImageUrls ?? {}).length, - }); - - // Render PDF - const documentTitle = input.title || content.titleHTML || undefined; - const pdfBuffer = yield* service.renderPdf( - content.contentJSON, - metadata, - { - title: documentTitle, - author: input.author, - subject: input.subject, - pageSize: input.pageSize, - pageOrientation: input.pageOrientation, - noAssets, - }, - requestId - ); - - yield* Effect.logInfo("PDF_EXPORT: Export complete", { - requestId, - pageId, - size: pdfBuffer.length, - }); - - return { - pdfBuffer, - outputFileName: input.fileName || `page-${pageId}.pdf`, - pageId, - }; - }); diff --git a/apps/live/src/services/pdf-export/types.ts b/apps/live/src/services/pdf-export/types.ts deleted file mode 100644 index 1a95b0ece..000000000 --- a/apps/live/src/services/pdf-export/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { TipTapDocument, PDFUserMention } from "@/lib/pdf"; - -export interface PdfExportInput { - readonly pageId: string; - readonly workspaceSlug: string; - readonly projectId?: string; - readonly title?: string; - readonly author?: string; - readonly subject?: string; - readonly pageSize?: "A4" | "A3" | "A2" | "LETTER" | "LEGAL" | "TABLOID"; - readonly pageOrientation?: "portrait" | "landscape"; - readonly fileName?: string; - readonly noAssets?: boolean; - readonly cookie: string; - readonly requestId: string; -} - -export interface PdfExportResult { - readonly pdfBuffer: Buffer; - readonly outputFileName: string; - readonly pageId: string; -} - -export interface PageContent { - readonly contentJSON: TipTapDocument; - readonly titleHTML: string | null; - readonly descriptionBinary: Buffer; -} - -/** - * Metadata - includes user mentions - */ -export interface MetadataResult { - readonly userMentions: PDFUserMention[]; - readonly resolvedImageUrls?: Record; -} diff --git a/apps/live/src/services/user.service.ts b/apps/live/src/services/user.service.ts deleted file mode 100644 index 9e25af467..000000000 --- a/apps/live/src/services/user.service.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// types -import { logger } from "@plane/logger"; -import type { IUser } from "@plane/types"; -// services -import { AppError } from "@/lib/errors"; -import { APIService } from "@/services/api.service"; - -export class UserService extends APIService { - - - currentUserConfig() { - return { - url: `${this.baseURL}/api/users/me/`, - }; - } - - async currentUser(cookie: string): Promise { - return this.get("/api/users/me/", { - headers: { - Cookie: cookie, - }, - }) - .then((response) => response?.data) - .catch((error) => { - const appError = new AppError(error, { - context: { operation: "currentUser" }, - }); - logger.error("Failed to fetch current user", appError); - throw appError; - }); - } -} diff --git a/apps/live/src/start.ts b/apps/live/src/start.ts deleted file mode 100644 index 705eb12f3..000000000 --- a/apps/live/src/start.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { logger } from "@plane/logger"; -import { AppError } from "@/lib/errors"; -import { Server } from "./server"; - -let server: Server; - -async function startServer() { - server = new Server(); - try { - await server.initialize(); - server.listen(); - } catch (error) { - logger.error("Failed to start server:", error); - process.exit(1); - } -} - -startServer(); - -// Handle process signals -process.on("SIGTERM", async () => { - logger.info("Received SIGTERM signal. Initiating graceful shutdown..."); - try { - if (server) { - await server.destroy(); - } - logger.info("Server shut down gracefully"); - } catch (error) { - logger.error("Error during graceful shutdown:", error); - process.exit(1); - } - process.exit(0); -}); - -process.on("SIGINT", async () => { - logger.info("Received SIGINT signal. Killing node process..."); - try { - if (server) { - await server.destroy(); - } - logger.info("Server shut down gracefully"); - } catch (error) { - logger.error("Error during graceful shutdown:", error); - process.exit(1); - } - process.exit(1); -}); - -process.on("unhandledRejection", (err: Error) => { - const error = new AppError(err); - logger.error(`[UNHANDLED_REJECTION]`, error); -}); - -process.on("uncaughtException", (err: Error) => { - const error = new AppError(err); - logger.error(`[UNCAUGHT_EXCEPTION]`, error); -}); diff --git a/apps/live/src/types/admin-commands.ts b/apps/live/src/types/admin-commands.ts deleted file mode 100644 index 1cbe7a537..000000000 --- a/apps/live/src/types/admin-commands.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -/** - * Type-safe admin commands for server-to-server communication - */ - -/** - * Force close error codes - reasons why a document is being force closed - */ -export enum ForceCloseReason { - CRITICAL_ERROR = "critical_error", - MEMORY_LEAK = "memory_leak", - DOCUMENT_TOO_LARGE = "document_too_large", - ADMIN_REQUEST = "admin_request", - SERVER_SHUTDOWN = "server_shutdown", - SECURITY_VIOLATION = "security_violation", - CORRUPTION_DETECTED = "corruption_detected", -} - -/** - * WebSocket close codes - * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code - */ -export enum CloseCode { - /** Normal closure; the connection successfully completed */ - NORMAL = 1000, - /** The endpoint is going away (server shutdown or browser navigating away) */ - GOING_AWAY = 1001, - /** Protocol error */ - PROTOCOL_ERROR = 1002, - /** Unsupported data */ - UNSUPPORTED_DATA = 1003, - /** Reserved (no status code was present) */ - NO_STATUS = 1005, - /** Abnormal closure */ - ABNORMAL = 1006, - /** Invalid frame payload data */ - INVALID_DATA = 1007, - /** Policy violation */ - POLICY_VIOLATION = 1008, - /** Message too big */ - MESSAGE_TOO_BIG = 1009, - /** Client expected extension not negotiated */ - MANDATORY_EXTENSION = 1010, - /** Server encountered unexpected condition */ - INTERNAL_ERROR = 1011, - /** Custom: Force close requested */ - FORCE_CLOSE = 4000, - /** Custom: Document too large */ - DOCUMENT_TOO_LARGE = 4001, - /** Custom: Memory pressure */ - MEMORY_PRESSURE = 4002, - /** Custom: Security violation */ - SECURITY_VIOLATION = 4003, -} - -/** - * Admin command types - */ -export enum AdminCommand { - FORCE_CLOSE = "force_close", - HEALTH_CHECK = "health_check", - RESTART_DOCUMENT = "restart_document", -} - -/** - * Force close command data structure - */ -export interface ForceCloseCommandData { - command: AdminCommand.FORCE_CLOSE; - docId: string; - reason: ForceCloseReason; - code: CloseCode; - originServer: string; - timestamp?: string; -} - -/** - * Health check command data structure - */ -export interface HealthCheckCommandData { - command: AdminCommand.HEALTH_CHECK; - originServer: string; - timestamp: string; -} - -/** - * Union type for all admin commands - */ -export type AdminCommandData = ForceCloseCommandData | HealthCheckCommandData; - -/** - * Client force close message structure (sent to clients via sendStateless) - */ -export interface ClientForceCloseMessage { - type: "force_close"; - reason: ForceCloseReason; - code: CloseCode; - message?: string; - timestamp?: string; -} - -/** - * Admin command handler function type - */ -export type AdminCommandHandler = (data: T) => Promise | void; - -/** - * Type guard to check if data is a ForceCloseCommandData - */ -export function isForceCloseCommand(data: AdminCommandData): data is ForceCloseCommandData { - return data.command === AdminCommand.FORCE_CLOSE; -} - -/** - * Type guard to check if data is a HealthCheckCommandData - */ -export function isHealthCheckCommand(data: AdminCommandData): data is HealthCheckCommandData { - return data.command === AdminCommand.HEALTH_CHECK; -} - -/** - * Validate force close reason - */ -export function isValidForceCloseReason(reason: string): reason is ForceCloseReason { - return Object.values(ForceCloseReason).includes(reason as ForceCloseReason); -} - -/** - * Get human-readable message for force close reason - */ -export function getForceCloseMessage(reason: ForceCloseReason): string { - const messages: Record = { - [ForceCloseReason.CRITICAL_ERROR]: "A critical error occurred. Please refresh the page.", - [ForceCloseReason.MEMORY_LEAK]: "Memory limit exceeded. Please refresh the page.", - [ForceCloseReason.DOCUMENT_TOO_LARGE]: - "Content limit reached and live sync is off. Create a new page or use nested pages to continue syncing.", - [ForceCloseReason.ADMIN_REQUEST]: "Connection closed by administrator. Please try again later.", - [ForceCloseReason.SERVER_SHUTDOWN]: "Server is shutting down. Please reconnect in a moment.", - [ForceCloseReason.SECURITY_VIOLATION]: "Security violation detected. Connection terminated.", - [ForceCloseReason.CORRUPTION_DETECTED]: "Data corruption detected. Please refresh the page.", - }; - - return messages[reason] || "Connection closed. Please refresh the page."; -} diff --git a/apps/live/src/types/index.ts b/apps/live/src/types/index.ts deleted file mode 100644 index 39c941d09..000000000 --- a/apps/live/src/types/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { fetchPayload, onLoadDocumentPayload, storePayload } from "@hocuspocus/server"; - -export type TConvertDocumentRequestBody = { - description_html: string; - variant: "rich" | "document"; -}; - -export interface OnLoadDocumentPayloadWithContext extends onLoadDocumentPayload { - context: HocusPocusServerContext; -} - -export interface FetchPayloadWithContext extends fetchPayload { - context: HocusPocusServerContext; -} - -export interface StorePayloadWithContext extends storePayload { - context: HocusPocusServerContext; -} - -export type TDocumentTypes = "project_page"; - -// Additional Hocuspocus types that are not exported from the main package -export type HocusPocusServerContext = { - projectId: string | null; - cookie: string; - documentType: TDocumentTypes; - workspaceSlug: string | null; - userId: string; -}; diff --git a/apps/live/src/utils/broadcast-error.ts b/apps/live/src/utils/broadcast-error.ts deleted file mode 100644 index 4d1077c9f..000000000 --- a/apps/live/src/utils/broadcast-error.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { Hocuspocus } from "@hocuspocus/server"; -import { createRealtimeEvent } from "@plane/editor"; -import { logger } from "@plane/logger"; -import type { HocusPocusServerContext } from "@/types"; -import { broadcastMessageToPage } from "./broadcast-message"; - -// Helper to broadcast error to frontend -export const broadcastError = async ( - hocuspocusServerInstance: Hocuspocus, - pageId: string, - errorMessage: string, - errorType: "fetch" | "store", - context: HocusPocusServerContext, - errorCode?: "content_too_large" | "page_locked" | "page_archived", - shouldDisconnect?: boolean -) => { - try { - const errorEvent = createRealtimeEvent({ - action: "error", - page_id: pageId, - parent_id: undefined, - descendants_ids: [], - data: { - error_message: errorMessage, - error_type: errorType, - error_code: errorCode, - should_disconnect: shouldDisconnect, - user_id: context.userId || "", - }, - workspace_slug: context.workspaceSlug || "", - user_id: context.userId || "", - }); - - await broadcastMessageToPage(hocuspocusServerInstance, pageId, errorEvent); - } catch (broadcastError) { - logger.error("Error broadcasting error message to frontend:", broadcastError); - } -}; diff --git a/apps/live/src/utils/broadcast-message.ts b/apps/live/src/utils/broadcast-message.ts deleted file mode 100644 index 473a3c731..000000000 --- a/apps/live/src/utils/broadcast-message.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { Hocuspocus } from "@hocuspocus/server"; -import type { BroadcastedEvent } from "@plane/editor"; -import { logger } from "@plane/logger"; -import { Redis } from "@/extensions/redis"; -import { AppError } from "@/lib/errors"; - -export const broadcastMessageToPage = async ( - hocuspocusServerInstance: Hocuspocus, - documentName: string, - eventData: BroadcastedEvent -): Promise => { - if (!hocuspocusServerInstance || !hocuspocusServerInstance.documents) { - const appError = new AppError("HocusPocus server not available or initialized", { - context: { operation: "broadcastMessageToPage", documentName }, - }); - logger.error("Error while broadcasting message:", appError); - return false; - } - - const redisExtension = hocuspocusServerInstance.configuration.extensions.find((ext) => ext instanceof Redis); - - if (!redisExtension) { - logger.error("BROADCAST_MESSAGE_TO_PAGE: Redis extension not found"); - return false; - } - - try { - await redisExtension.broadcastToDocument(documentName, eventData); - return true; - } catch (error) { - logger.error(`BROADCAST_MESSAGE_TO_PAGE: Error broadcasting to ${documentName}:`, error); - return false; - } -}; diff --git a/apps/live/tests/lib/pdf/pdf-rendering.test.ts b/apps/live/tests/lib/pdf/pdf-rendering.test.ts deleted file mode 100644 index 507c6f900..000000000 --- a/apps/live/tests/lib/pdf/pdf-rendering.test.ts +++ /dev/null @@ -1,732 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { describe, it, expect } from "vitest"; -import { PDFParse } from "pdf-parse"; -import { renderPlaneDocToPdfBuffer } from "@/lib/pdf"; -import type { TipTapDocument, PDFExportMetadata } from "@/lib/pdf"; - -const PDF_HEADER = "%PDF-"; - -/** - * Helper to extract text content from a PDF buffer - */ -async function extractPdfText(buffer: Buffer): Promise { - const uint8 = new Uint8Array(buffer); - const parser = new PDFParse(uint8); - const result = await parser.getText(); - return result.pages.map((p) => p.text).join("\n"); -} - -describe("PDF Rendering Integration", () => { - describe("renderPlaneDocToPdfBuffer", () => { - it("should render empty document to valid PDF", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - - expect(buffer).toBeInstanceOf(Buffer); - expect(buffer.length).toBeGreaterThan(0); - expect(buffer.toString("ascii", 0, 5)).toBe(PDF_HEADER); - }); - - it("should render document with title and verify content", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Hello World" }], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc, { - title: "Test Document", - }); - - expect(buffer).toBeInstanceOf(Buffer); - expect(buffer.toString("ascii", 0, 5)).toBe(PDF_HEADER); - - const text = await extractPdfText(buffer); - expect(text).toContain("Hello World"); - // Title is rendered in PDF content when provided - expect(text).toContain("Test Document"); - }); - - it("should render heading nodes and verify text", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "heading", - attrs: { level: 1 }, - content: [{ type: "text", text: "Main Heading" }], - }, - { - type: "heading", - attrs: { level: 2 }, - content: [{ type: "text", text: "Subheading" }], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("Main Heading"); - expect(text).toContain("Subheading"); - }); - - it("should render paragraph with text and verify content", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "This is a test paragraph with some content." }], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("This is a test paragraph with some content."); - }); - - it("should render bullet list with all items", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "bulletList", - content: [ - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "First item" }], - }, - ], - }, - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Second item" }], - }, - ], - }, - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Third item" }], - }, - ], - }, - ], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("First item"); - expect(text).toContain("Second item"); - expect(text).toContain("Third item"); - // Bullet points should be present - expect(text).toContain("•"); - }); - - it("should render ordered list with numbers", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "orderedList", - content: [ - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Step one" }], - }, - ], - }, - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Step two" }], - }, - ], - }, - ], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("Step one"); - expect(text).toContain("Step two"); - // Numbers should be present - expect(text).toMatch(/1\./); - expect(text).toMatch(/2\./); - }); - - it("should render task list with task text", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "taskList", - content: [ - { - type: "taskItem", - attrs: { checked: true }, - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Completed task" }], - }, - ], - }, - { - type: "taskItem", - attrs: { checked: false }, - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Pending task" }], - }, - ], - }, - ], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("Completed task"); - expect(text).toContain("Pending task"); - }); - - it("should render code block with code content", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "codeBlock", - content: [ - { type: "text", text: "const greeting = 'Hello';\n" }, - { type: "text", text: "console.log(greeting);" }, - ], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("const greeting"); - expect(text).toContain("console.log"); - }); - - it("should render blockquote with quoted text", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "blockquote", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "This is a quoted text." }], - }, - ], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("This is a quoted text."); - }); - - it("should render table with all cell content", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "table", - content: [ - { - type: "tableRow", - content: [ - { - type: "tableHeader", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Header 1" }], - }, - ], - }, - { - type: "tableHeader", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Header 2" }], - }, - ], - }, - ], - }, - { - type: "tableRow", - content: [ - { - type: "tableCell", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Cell 1" }], - }, - ], - }, - { - type: "tableCell", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Cell 2" }], - }, - ], - }, - ], - }, - ], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("Header 1"); - expect(text).toContain("Header 2"); - expect(text).toContain("Cell 1"); - expect(text).toContain("Cell 2"); - }); - - it("should render horizontal rule with surrounding text", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Before rule" }], - }, - { type: "horizontalRule" }, - { - type: "paragraph", - content: [{ type: "text", text: "After rule" }], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("Before rule"); - expect(text).toContain("After rule"); - }); - - it("should render text with marks (bold, italic) preserving content", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "paragraph", - content: [ - { type: "text", text: "Normal " }, - { - type: "text", - text: "bold", - marks: [{ type: "bold" }], - }, - { type: "text", text: " and " }, - { - type: "text", - text: "italic", - marks: [{ type: "italic" }], - }, - { type: "text", text: " text." }, - ], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("Normal"); - expect(text).toContain("bold"); - expect(text).toContain("italic"); - expect(text).toContain("text."); - }); - - it("should render link marks with link text", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "paragraph", - content: [ - { type: "text", text: "Click " }, - { - type: "text", - text: "here", - marks: [{ type: "link", attrs: { href: "https://example.com" } }], - }, - { type: "text", text: " to visit." }, - ], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("Click"); - expect(text).toContain("here"); - expect(text).toContain("to visit"); - }); - }); - - describe("page options", () => { - it("should support different page sizes and verify content renders", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Page size test content" }], - }, - ], - }; - - const a4Buffer = await renderPlaneDocToPdfBuffer(doc, { pageSize: "A4" }); - const letterBuffer = await renderPlaneDocToPdfBuffer(doc, { pageSize: "LETTER" }); - - const a4Text = await extractPdfText(a4Buffer); - const letterText = await extractPdfText(letterBuffer); - - expect(a4Text).toContain("Page size test content"); - expect(letterText).toContain("Page size test content"); - // Different page sizes should produce different PDF sizes - expect(a4Buffer.length).not.toBe(letterBuffer.length); - }); - - it("should support landscape orientation and verify content", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Landscape content here" }], - }, - ], - }; - - const portraitBuffer = await renderPlaneDocToPdfBuffer(doc, { pageOrientation: "portrait" }); - const landscapeBuffer = await renderPlaneDocToPdfBuffer(doc, { pageOrientation: "landscape" }); - - const portraitText = await extractPdfText(portraitBuffer); - const landscapeText = await extractPdfText(landscapeBuffer); - - expect(portraitText).toContain("Landscape content here"); - expect(landscapeText).toContain("Landscape content here"); - expect(portraitBuffer.length).not.toBe(landscapeBuffer.length); - }); - - it("should include author metadata in PDF", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Document content" }], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc, { - author: "Test Author", - }); - - // Verify PDF is valid and contains content - expect(buffer).toBeInstanceOf(Buffer); - expect(buffer.toString("ascii", 0, 5)).toBe(PDF_HEADER); - // Author metadata is embedded in PDF info dict (checked via raw bytes) - const pdfString = buffer.toString("latin1"); - expect(pdfString).toContain("/Author"); - }); - - it("should include subject metadata in PDF", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Document content" }], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc, { - subject: "Technical Documentation", - }); - - // Verify PDF is valid - expect(buffer).toBeInstanceOf(Buffer); - expect(buffer.toString("ascii", 0, 5)).toBe(PDF_HEADER); - // Subject metadata is embedded in PDF info dict - const pdfString = buffer.toString("latin1"); - expect(pdfString).toContain("/Subject"); - }); - }); - - describe("metadata rendering", () => { - it("should render user mentions with resolved display name", async () => { - const metadata: PDFExportMetadata = { - userMentions: [{ id: "user-123", display_name: "John Doe" }], - }; - - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "paragraph", - content: [ - { type: "text", text: "Hello " }, - { - type: "mention", - attrs: { - entity_name: "user_mention", - entity_identifier: "user-123", - }, - }, - ], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc, { metadata }); - const text = await extractPdfText(buffer); - - expect(text).toContain("Hello"); - expect(text).toContain("John Doe"); - }); - }); - - describe("complex documents", () => { - it("should render a full document with mixed content and verify all sections", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "heading", - attrs: { level: 1 }, - content: [{ type: "text", text: "Project Overview" }], - }, - { - type: "paragraph", - content: [ - { type: "text", text: "This document describes the " }, - { type: "text", text: "key features", marks: [{ type: "bold" }] }, - { type: "text", text: " of the project." }, - ], - }, - { - type: "heading", - attrs: { level: 2 }, - content: [{ type: "text", text: "Features" }], - }, - { - type: "bulletList", - content: [ - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Feature A - Core functionality" }], - }, - ], - }, - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Feature B - Advanced options" }], - }, - ], - }, - ], - }, - { - type: "heading", - attrs: { level: 2 }, - content: [{ type: "text", text: "Code Example" }], - }, - { - type: "codeBlock", - content: [{ type: "text", text: "function hello() {\n return 'world';\n}" }], - }, - { - type: "blockquote", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Important: Review before deployment." }], - }, - ], - }, - { type: "horizontalRule" }, - { - type: "paragraph", - content: [{ type: "text", text: "End of document." }], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc, { - title: "Project Overview", - author: "Development Team", - subject: "Technical Documentation", - }); - - const text = await extractPdfText(buffer); - - // Verify metadata is embedded in PDF - const pdfString = buffer.toString("latin1"); - expect(pdfString).toContain("/Title"); - expect(pdfString).toContain("/Author"); - expect(pdfString).toContain("/Subject"); - - // Verify all content sections are present - expect(text).toContain("Project Overview"); - expect(text).toContain("This document describes the"); - expect(text).toContain("key features"); - expect(text).toContain("Features"); - expect(text).toContain("Feature A - Core functionality"); - expect(text).toContain("Feature B - Advanced options"); - expect(text).toContain("Code Example"); - expect(text).toContain("function hello"); - expect(text).toContain("Important: Review before deployment"); - expect(text).toContain("End of document"); - }); - - it("should render deeply nested lists with all levels", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "bulletList", - content: [ - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Level 1" }], - }, - { - type: "bulletList", - content: [ - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Level 2" }], - }, - { - type: "bulletList", - content: [ - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Level 3" }], - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc); - const text = await extractPdfText(buffer); - - expect(text).toContain("Level 1"); - expect(text).toContain("Level 2"); - expect(text).toContain("Level 3"); - }); - }); - - describe("noAssets option", () => { - it("should render text but skip images when noAssets is true", async () => { - const doc: TipTapDocument = { - type: "doc", - content: [ - { - type: "image", - attrs: { src: "https://example.com/image.png" }, - }, - { - type: "paragraph", - content: [{ type: "text", text: "Text after image" }], - }, - ], - }; - - const buffer = await renderPlaneDocToPdfBuffer(doc, { noAssets: true }); - const text = await extractPdfText(buffer); - - expect(text).toContain("Text after image"); - }); - }); -}); diff --git a/apps/live/tests/services/pdf-export/effect-utils.test.ts b/apps/live/tests/services/pdf-export/effect-utils.test.ts deleted file mode 100644 index 44ff35e67..000000000 --- a/apps/live/tests/services/pdf-export/effect-utils.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { describe, it, expect, assert } from "vitest"; -import { Effect, Duration, Either } from "effect"; -import { withTimeoutAndRetry, recoverWithDefault, tryAsync } from "@/services/pdf-export/effect-utils"; -import { PdfTimeoutError } from "@/schema/pdf-export"; - -describe("effect-utils", () => { - describe("withTimeoutAndRetry", () => { - it("should succeed when effect completes within timeout", async () => { - const effect = Effect.succeed("success"); - const wrapped = withTimeoutAndRetry("test-operation")(effect); - - const result = await Effect.runPromise(wrapped); - expect(result).toBe("success"); - }); - - it("should fail with PdfTimeoutError when effect exceeds timeout", async () => { - const slowEffect = Effect.gen(function* () { - yield* Effect.sleep(Duration.millis(500)); - return "success"; - }); - - const wrapped = withTimeoutAndRetry("test-operation", { - timeoutMs: 50, - maxRetries: 0, - })(slowEffect); - - const result = await Effect.runPromise(Effect.either(wrapped)); - - assert(Either.isLeft(result), "Expected Left but got Right"); - expect(result.left).toBeInstanceOf(PdfTimeoutError); - expect((result.left as PdfTimeoutError).operation).toBe("test-operation"); - }); - - it("should retry on failure up to maxRetries times", async () => { - const attemptCounter = { count: 0 }; - - const flakyEffect = Effect.gen(function* () { - attemptCounter.count++; - if (attemptCounter.count < 3) { - return yield* Effect.fail(new Error("transient failure")); - } - return "success"; - }); - - const wrapped = withTimeoutAndRetry("test-operation", { - timeoutMs: 5000, - maxRetries: 3, - })(flakyEffect); - - const result = await Effect.runPromise(wrapped); - - expect(result).toBe("success"); - expect(attemptCounter.count).toBe(3); - }); - - it("should fail after exhausting retries", async () => { - const effect = Effect.fail(new Error("permanent failure")); - - const wrapped = withTimeoutAndRetry("test-operation", { - timeoutMs: 5000, - maxRetries: 2, - })(effect); - - const result = await Effect.runPromise(Effect.either(wrapped)); - - expect(result._tag).toBe("Left"); - }); - }); - - describe("recoverWithDefault", () => { - it("should return success value when effect succeeds", async () => { - const effect = Effect.succeed("success"); - const wrapped = recoverWithDefault("fallback")(effect); - - const result = await Effect.runPromise(wrapped); - expect(result).toBe("success"); - }); - - it("should return fallback value when effect fails", async () => { - const effect = Effect.fail(new Error("failure")); - const wrapped = recoverWithDefault("fallback")(effect); - - const result = await Effect.runPromise(wrapped); - expect(result).toBe("fallback"); - }); - - it("should log warning when recovering from error", async () => { - const logs: string[] = []; - - const effect = Effect.fail(new Error("test error")).pipe( - recoverWithDefault("fallback"), - Effect.tap(() => Effect.sync(() => logs.push("after recovery"))) - ); - - const result = await Effect.runPromise(effect); - - expect(result).toBe("fallback"); - expect(logs).toContain("after recovery"); - }); - - it("should work with complex fallback objects", async () => { - const fallback = { items: [], count: 0, metadata: { version: 1 } }; - - const effect = Effect.fail(new Error("failure")); - const wrapped = recoverWithDefault(fallback)(effect); - - const result = await Effect.runPromise(wrapped); - expect(result).toEqual(fallback); - }); - }); - - describe("tryAsync", () => { - it("should wrap successful promise", async () => { - const effect = tryAsync( - () => Promise.resolve("success"), - (err) => new Error(`wrapped: ${err}`) - ); - - const result = await Effect.runPromise(effect); - expect(result).toBe("success"); - }); - - it("should wrap rejected promise with custom error", async () => { - const effect = tryAsync( - () => Promise.reject(new Error("original")), - (err) => new Error(`wrapped: ${(err as Error).message}`) - ); - - const result = await Effect.runPromise(Effect.either(effect)); - - assert(Either.isLeft(result), "Expected Left but got Right"); - expect(result.left.message).toBe("wrapped: original"); - }); - - it("should handle synchronous throws", async () => { - const effect = tryAsync( - () => { - throw new Error("sync error"); - }, - (err) => new Error(`caught: ${(err as Error).message}`) - ); - - const result = await Effect.runPromise(Effect.either(effect)); - - assert(Either.isLeft(result), "Expected Left but got Right"); - expect(result.left.message).toBe("caught: sync error"); - }); - }); -}); diff --git a/apps/live/tsconfig.json b/apps/live/tsconfig.json deleted file mode 100644 index 9d49437c0..000000000 --- a/apps/live/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "@plane/typescript-config/base.json", - "compilerOptions": { - "exactOptionalPropertyTypes": false, - "noUnusedParameters": false, - "noImplicitOverride": false, - "noImplicitReturns": false, - "noUnusedLocals": false, - "jsx": "react-jsx", - - "paths": { - "@/*": ["./src/*"], - "@/plane-live/*": ["./src/ce/*"] - }, - "experimentalDecorators": true, - "emitDecoratorMetadata": true - }, - "include": ["src", "tests"], - "exclude": ["node_modules", "dist"] -} diff --git a/apps/live/tsdown.config.ts b/apps/live/tsdown.config.ts deleted file mode 100644 index e94a584ba..000000000 --- a/apps/live/tsdown.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from "tsdown"; - -export default defineConfig({ - entry: ["src/start.ts"], - outDir: "dist", - format: ["esm"], - dts: false, - clean: true, - sourcemap: false, - exports: true, -}); diff --git a/apps/live/vitest.config.ts b/apps/live/vitest.config.ts deleted file mode 100644 index d9a1624cd..000000000 --- a/apps/live/vitest.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineConfig } from "vitest/config"; -import path from "path"; - -export default defineConfig({ - test: { - environment: "node", - globals: true, - include: ["tests/**/*.test.ts", "tests/**/*.spec.ts"], - coverage: { - provider: "v8", - reporter: ["text", "json", "html"], - include: ["src/**/*.ts"], - exclude: ["src/**/*.d.ts", "src/**/types.ts"], - }, - }, - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, -}); diff --git a/apps/proxy/.prettierignore b/apps/proxy/.prettierignore deleted file mode 100644 index b0b8bc622..000000000 --- a/apps/proxy/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -.next/ -.react-router/ -.turbo/ -.vite/ -build/ -dist/ -node_modules/ -out/ -pnpm-lock.yaml -storybook-static/ diff --git a/apps/proxy/Caddyfile.aio.ce b/apps/proxy/Caddyfile.aio.ce deleted file mode 100644 index 9cf6d8dd8..000000000 --- a/apps/proxy/Caddyfile.aio.ce +++ /dev/null @@ -1,46 +0,0 @@ -(plane_proxy) { - request_body { - max_size {$FILE_SIZE_LIMIT} - } - - handle /spaces/* { - reverse_proxy localhost:3002 - } - - handle /live/* { - reverse_proxy localhost:3005 - } - handle /api/* { - reverse_proxy localhost:3004 - } - - handle /auth/* { - reverse_proxy localhost:3004 - } - - handle_path /god-mode* { - root * /app/admin - try_files {path} {path}/ /index.html - file_server - } - handle_path /* { - root * /app/web - try_files {path} {path}/ /index.html - file_server - } -} - -{ - {$CERT_EMAIL} - acme_ca {$CERT_ACME_CA:https://acme-v02.api.letsencrypt.org/directory} - {$CERT_ACME_DNS} - servers { - max_header_size 25MB - client_ip_headers X-Forwarded-For X-Real-IP - trusted_proxies static {$TRUSTED_PROXIES:0.0.0.0/0} - } -} - -{$SITE_ADDRESS} { - import plane_proxy -} \ No newline at end of file diff --git a/apps/proxy/Caddyfile.ce b/apps/proxy/Caddyfile.ce deleted file mode 100644 index 14559f272..000000000 --- a/apps/proxy/Caddyfile.ce +++ /dev/null @@ -1,39 +0,0 @@ -(plane_proxy) { - request_body { - max_size {$FILE_SIZE_LIMIT} - } - - redir /spaces /spaces/ permanent - reverse_proxy /spaces/* space:3000 - - redir /god-mode /god-mode/ permanent - reverse_proxy /god-mode/* admin:3000 - - reverse_proxy /live/* live:3000 - - reverse_proxy /api/* api:8000 - - reverse_proxy /auth/* api:8000 - - reverse_proxy /static/* api:8000 - - reverse_proxy /{$BUCKET_NAME}/* plane-minio:9000 - reverse_proxy /{$BUCKET_NAME} plane-minio:9000 - - reverse_proxy /* web:3000 -} - -{ - {$CERT_EMAIL} - acme_ca {$CERT_ACME_CA:https://acme-v02.api.letsencrypt.org/directory} - {$CERT_ACME_DNS} - servers { - max_header_size 25MB - client_ip_headers X-Forwarded-For X-Real-IP - trusted_proxies static {$TRUSTED_PROXIES:0.0.0.0/0} - } -} - -{$SITE_ADDRESS} { - import plane_proxy -} \ No newline at end of file diff --git a/apps/proxy/Dockerfile.ce b/apps/proxy/Dockerfile.ce deleted file mode 100644 index 87436e7a5..000000000 --- a/apps/proxy/Dockerfile.ce +++ /dev/null @@ -1,45 +0,0 @@ -FROM caddy:2.11.4-builder-alpine AS caddy-builder - -# grpc and otel move together: grpc >= v1.83.0 requires otel v1.44.0, and xcaddy -# resolves these --with pins in one `go get`, so a stale otel pin makes go walk -# grpc backwards until it finds one that fits (silently, with a zero exit) -- -# it lands on the v1.83.0-dev pre-release and the build still succeeds. Bump the -# otel pair in lockstep with grpc, and confirm with `go version -m` afterwards. -RUN XCADDY_SKIP_CLEANUP=1 xcaddy build \ - --with github.com/caddy-dns/cloudflare@v0.2.1 \ - --with github.com/caddy-dns/digitalocean@04bde2867106aa1b44c2f9da41a285fa02e629c5 \ - --with github.com/mholt/caddy-l4@6faae83b167fda94e62b686be5cbeb9b3f8fe002 \ - --with github.com/go-jose/go-jose/v3@v3.0.5 \ - --with github.com/go-jose/go-jose/v4@v4.1.4 \ - --with google.golang.org/grpc@v1.83.1 \ - --with go.opentelemetry.io/otel@v1.44.0 \ - --with go.opentelemetry.io/otel/sdk@v1.44.0 - -# Caddy's own go.mod pins x/crypto, x/net and x/text at versions with open CVEs; -# xcaddy preserved its build dir above, so upgrade them there and rebuild. -# x/crypto v0.55.0 requires x/net v0.57.0 and x/text v0.41.0, hence those floors. -RUN cd "$(find /tmp -maxdepth 1 -name 'buildenv_*' -type d | head -1)" && \ - go get golang.org/x/crypto@v0.55.0 && \ - go get golang.org/x/net@v0.57.0 && \ - go get golang.org/x/text@v0.41.0 && \ - go mod tidy && \ - go build -o /usr/bin/caddy -ldflags "-w -s" -trimpath -tags nobadger,nomysql,nopgx . - -# xcaddy resolves every --with pin in a single `go get`, which walks a module -# backwards to an older release rather than failing when the pins conflict. -# Assert the binary really carries the pinned grpc, so a silent downgrade fails -# the build instead of shipping a proxy that only looks patched. -RUN go version -m /usr/bin/caddy | grep -qE 'google\.golang\.org/grpc[[:space:]]v1\.83\.1[[:space:]]' \ - || { echo "caddy was not built against the pinned grpc -- check the --with pins" >&2; exit 1; } - -FROM caddy:2.11.4-alpine - -# OS security updates. --available forces reinstall from the current repos; -# bump APK_SECURITY_PATCH to bust buildx's cached layer (keyed on this command -# string), otherwise a rebuild silently re-ships the old packages. -ARG APK_SECURITY_PATCH=2026-09-07 -RUN echo "apk-security-patch ${APK_SECURITY_PATCH}" && apk update && apk upgrade --no-cache --available && apk add --no-cache nss-tools bash curl - -COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy - -COPY Caddyfile.ce /etc/caddy/Caddyfile \ No newline at end of file diff --git a/apps/space/.env.example b/apps/space/.env.example deleted file mode 100644 index 59663aa67..000000000 --- a/apps/space/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -VITE_API_BASE_URL="http://localhost:8000" - -VITE_WEB_BASE_URL="http://localhost:3000" - -VITE_ADMIN_BASE_URL="http://localhost:3001" -VITE_ADMIN_BASE_PATH="/god-mode" - -VITE_SPACE_BASE_URL="http://localhost:3002" -VITE_SPACE_BASE_PATH="/spaces" - -VITE_LIVE_BASE_URL="http://localhost:3100" -VITE_LIVE_BASE_PATH="/live" diff --git a/apps/space/.gitignore b/apps/space/.gitignore deleted file mode 100644 index 5449b293a..000000000 --- a/apps/space/.gitignore +++ /dev/null @@ -1,43 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# react-router -/.react-router/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -.pnpm-debug.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# env -.env - -# Sentry Config File -.env.sentry-build-plugin diff --git a/apps/space/.prettierignore b/apps/space/.prettierignore deleted file mode 100644 index b0b8bc622..000000000 --- a/apps/space/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -.next/ -.react-router/ -.turbo/ -.vite/ -build/ -dist/ -node_modules/ -out/ -pnpm-lock.yaml -storybook-static/ diff --git a/apps/space/Dockerfile.dev b/apps/space/Dockerfile.dev deleted file mode 100644 index 20ac137da..000000000 --- a/apps/space/Dockerfile.dev +++ /dev/null @@ -1,19 +0,0 @@ -FROM node:22-alpine - -RUN apk add --no-cache libc6-compat - -# Set working directory -WORKDIR /app - -COPY . . - -RUN corepack enable pnpm && pnpm add -g turbo -RUN pnpm install - -EXPOSE 3002 - -ENV VITE_SPACE_BASE_PATH="/spaces" - -VOLUME [ "/app/node_modules", "/app/apps/space/node_modules"] - -CMD ["pnpm", "dev", "--filter=space"] diff --git a/apps/space/Dockerfile.space b/apps/space/Dockerfile.space deleted file mode 100644 index ee5e3c300..000000000 --- a/apps/space/Dockerfile.space +++ /dev/null @@ -1,114 +0,0 @@ -FROM node:22-alpine AS base - -WORKDIR /app - -ENV TURBO_TELEMETRY_DISABLED=1 -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PNPM_HOME/bin:$PATH" -ENV CI=1 - -RUN corepack enable pnpm - -# =========================================================================== # - -FROM base AS builder - -RUN pnpm add -g turbo@2.9.18 - -COPY . . - -# Create a pruned workspace for just the space app -RUN turbo prune --scope=space --docker - -# =========================================================================== # - -FROM base AS installer - -# Build in production mode; we still install dev deps explicitly below -ENV NODE_ENV=production - -# Public envs required at build time (pick up via process.env) -ARG VITE_API_BASE_URL="" -ENV VITE_API_BASE_URL=$VITE_API_BASE_URL -ARG VITE_API_BASE_PATH="/api" -ENV VITE_API_BASE_PATH=$VITE_API_BASE_PATH - -ARG VITE_ADMIN_BASE_URL="" -ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL -ARG VITE_ADMIN_BASE_PATH="/god-mode" -ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH - -ARG VITE_SPACE_BASE_URL="" -ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL -ARG VITE_SPACE_BASE_PATH="/spaces" -ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH - -ARG VITE_LIVE_BASE_URL="" -ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL -ARG VITE_LIVE_BASE_PATH="/live" -ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH - -ARG VITE_WEB_BASE_URL="" -ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL -ARG VITE_WEB_BASE_PATH="" -ENV VITE_WEB_BASE_PATH=$VITE_WEB_BASE_PATH - -ARG VITE_WEBSITE_URL="https://plane.so" -ENV VITE_WEBSITE_URL=$VITE_WEBSITE_URL - -ARG VITE_SUPPORT_EMAIL="support@plane.so" -ENV VITE_SUPPORT_EMAIL=$VITE_SUPPORT_EMAIL - -COPY .gitignore .gitignore -COPY --from=builder /app/out/json/ . -COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml - -# Copy full directory structure before fetch to ensure all package.json files are available -COPY --from=builder /app/out/full/ . -COPY turbo.json turbo.json - -# Fetch dependencies to cache store, then install offline with dev deps -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false - -# Build only the space package -RUN pnpm turbo run build --filter=space - -# =========================================================================== # - -FROM base AS runner - -ENV NODE_ENV=production - -# OS security updates. --available forces reinstall from the current repos; -# bump APK_SECURITY_PATCH to bust buildx's cached layer (keyed on this command -# string), otherwise a rebuild silently re-ships the old packages. -ARG APK_SECURITY_PATCH=2026-09-07 -RUN echo "apk-security-patch ${APK_SECURITY_PATCH}" && apk update && apk upgrade --no-cache --available - -# Remove go from Alpine APK database; not needed at runtime and carries stdlib CVEs -RUN apk del go 2>/dev/null || true - -# npm is not used at runtime (the CMD execs the react-router-serve bin shim -# directly rather than going through npx); its bundled dependencies carry known -# vulnerabilities (tar, brace-expansion, pacote, sigstore, ip-address, fast-uri, -# browserslist), so remove the whole CLI rather than picking off one package at -# a time. -RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx - -COPY --from=installer /app/apps/space/build ./apps/space/build -COPY --from=installer /app/apps/space/node_modules ./apps/space/node_modules -COPY --from=installer /app/node_modules ./node_modules - -# esbuild and tsgolint are build-only Go binaries; remove from runtime image to eliminate stdlib CVEs -RUN find /app/node_modules \( -name 'esbuild' -o -name 'tsgolint' \) -type f -delete 2>/dev/null || true - -WORKDIR /app/apps/space - -EXPOSE 3000 - -RUN apk add --no-cache curl -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD curl -fsS http://127.0.0.1:3000/spaces/ >/dev/null || exit 1 - -CMD ["./node_modules/.bin/react-router-serve", "./build/server/index.js"] diff --git a/apps/space/README.md b/apps/space/README.md deleted file mode 100644 index fc2298101..000000000 --- a/apps/space/README.md +++ /dev/null @@ -1,10 +0,0 @@ -

- -

- - Plane Logo - -

- -

Plane Space

-

Open-source, self-hosted project planning tool

diff --git a/apps/space/additional.d.ts b/apps/space/additional.d.ts deleted file mode 100644 index f400344c6..000000000 --- a/apps/space/additional.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// additional.d.ts -/// diff --git a/apps/space/app/[workspaceSlug]/[projectId]/page.tsx b/apps/space/app/[workspaceSlug]/[projectId]/page.tsx deleted file mode 100644 index b14ec17c0..000000000 --- a/apps/space/app/[workspaceSlug]/[projectId]/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; -// plane imports -import { SitesProjectPublishService } from "@plane/services"; -import type { TProjectPublishSettings } from "@plane/types"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import type { Route } from "./+types/page"; - -const publishService = new SitesProjectPublishService(); - -export const clientLoader = async ({ params, request }: Route.ClientLoaderArgs) => { - const { workspaceSlug, projectId } = params; - - // Validate required params - if (!workspaceSlug || !projectId) { - throw redirect("/404"); - } - - // Extract query params from the request URL - const url = new URL(request.url); - const board = url.searchParams.get("board"); - const peekId = url.searchParams.get("peekId"); - - let response: TProjectPublishSettings | undefined = undefined; - - try { - response = await publishService.retrieveSettingsByProjectId(workspaceSlug, projectId); - } catch { - throw redirect("/404"); - } - - if (response?.entity_name === "project") { - let redirectUrl = `/issues/${response?.anchor}`; - const urlParams = new URLSearchParams(); - if (board) urlParams.append("board", String(board)); - if (peekId) urlParams.append("peekId", String(peekId)); - if (urlParams.toString()) redirectUrl += `?${urlParams.toString()}`; - - throw redirect(redirectUrl); - } else { - throw redirect("/404"); - } -}; - -export default function IssuesPage() { - return ( -
- -
- ); -} diff --git a/apps/space/app/assets/404.svg b/apps/space/app/assets/404.svg deleted file mode 100644 index 4c298417d..000000000 --- a/apps/space/app/assets/404.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/apps/space/app/assets/auth/background-pattern-dark.svg b/apps/space/app/assets/auth/background-pattern-dark.svg deleted file mode 100644 index c258cbabf..000000000 --- a/apps/space/app/assets/auth/background-pattern-dark.svg +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/space/app/assets/auth/background-pattern.svg b/apps/space/app/assets/auth/background-pattern.svg deleted file mode 100644 index 5fcbeec27..000000000 --- a/apps/space/app/assets/auth/background-pattern.svg +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/space/app/assets/favicon/apple-touch-icon.png b/apps/space/app/assets/favicon/apple-touch-icon.png deleted file mode 100644 index a63126783..000000000 Binary files a/apps/space/app/assets/favicon/apple-touch-icon.png and /dev/null differ diff --git a/apps/space/app/assets/favicon/favicon-16x16.png b/apps/space/app/assets/favicon/favicon-16x16.png deleted file mode 100644 index af59ef010..000000000 Binary files a/apps/space/app/assets/favicon/favicon-16x16.png and /dev/null differ diff --git a/apps/space/app/assets/favicon/favicon-32x32.png b/apps/space/app/assets/favicon/favicon-32x32.png deleted file mode 100644 index 16a1271ad..000000000 Binary files a/apps/space/app/assets/favicon/favicon-32x32.png and /dev/null differ diff --git a/apps/space/app/assets/favicon/favicon.ico b/apps/space/app/assets/favicon/favicon.ico deleted file mode 100644 index 613b1a313..000000000 Binary files a/apps/space/app/assets/favicon/favicon.ico and /dev/null differ diff --git a/apps/space/app/assets/favicon/site.webmanifest b/apps/space/app/assets/favicon/site.webmanifest deleted file mode 100644 index 1d4105781..000000000 --- a/apps/space/app/assets/favicon/site.webmanifest +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/apps/space/app/assets/images/logo-spinner-dark.gif b/apps/space/app/assets/images/logo-spinner-dark.gif deleted file mode 100644 index 8bd083255..000000000 Binary files a/apps/space/app/assets/images/logo-spinner-dark.gif and /dev/null differ diff --git a/apps/space/app/assets/images/logo-spinner-light.gif b/apps/space/app/assets/images/logo-spinner-light.gif deleted file mode 100644 index 8b5710314..000000000 Binary files a/apps/space/app/assets/images/logo-spinner-light.gif and /dev/null differ diff --git a/apps/space/app/assets/instance/instance-failure-dark.svg b/apps/space/app/assets/instance/instance-failure-dark.svg deleted file mode 100644 index 58d691705..000000000 --- a/apps/space/app/assets/instance/instance-failure-dark.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/space/app/assets/instance/instance-failure.svg b/apps/space/app/assets/instance/instance-failure.svg deleted file mode 100644 index a59862283..000000000 --- a/apps/space/app/assets/instance/instance-failure.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/space/app/assets/instance/intake-sent-dark.png b/apps/space/app/assets/instance/intake-sent-dark.png deleted file mode 100644 index 70a62730e..000000000 Binary files a/apps/space/app/assets/instance/intake-sent-dark.png and /dev/null differ diff --git a/apps/space/app/assets/instance/intake-sent-light.png b/apps/space/app/assets/instance/intake-sent-light.png deleted file mode 100644 index 9425c0723..000000000 Binary files a/apps/space/app/assets/instance/intake-sent-light.png and /dev/null differ diff --git a/apps/space/app/assets/instance/plane-instance-not-ready.webp b/apps/space/app/assets/instance/plane-instance-not-ready.webp deleted file mode 100644 index a0efca52c..000000000 Binary files a/apps/space/app/assets/instance/plane-instance-not-ready.webp and /dev/null differ diff --git a/apps/space/app/assets/instance/plane-takeoff.png b/apps/space/app/assets/instance/plane-takeoff.png deleted file mode 100644 index 417ff8299..000000000 Binary files a/apps/space/app/assets/instance/plane-takeoff.png and /dev/null differ diff --git a/apps/space/app/assets/logos/gitea-logo.svg b/apps/space/app/assets/logos/gitea-logo.svg deleted file mode 100644 index 43291345d..000000000 --- a/apps/space/app/assets/logos/gitea-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/space/app/assets/logos/github-black.png b/apps/space/app/assets/logos/github-black.png deleted file mode 100644 index 7a7a82474..000000000 Binary files a/apps/space/app/assets/logos/github-black.png and /dev/null differ diff --git a/apps/space/app/assets/logos/github-dark.svg b/apps/space/app/assets/logos/github-dark.svg deleted file mode 100644 index a0cb35c3a..000000000 --- a/apps/space/app/assets/logos/github-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/space/app/assets/logos/github-square.svg b/apps/space/app/assets/logos/github-square.svg deleted file mode 100644 index a7836db8f..000000000 --- a/apps/space/app/assets/logos/github-square.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - diff --git a/apps/space/app/assets/logos/github-white.svg b/apps/space/app/assets/logos/github-white.svg deleted file mode 100644 index 90fe34d8b..000000000 --- a/apps/space/app/assets/logos/github-white.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/apps/space/app/assets/logos/gitlab-logo.svg b/apps/space/app/assets/logos/gitlab-logo.svg deleted file mode 100644 index dab4d8b74..000000000 --- a/apps/space/app/assets/logos/gitlab-logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/space/app/assets/logos/google-logo.svg b/apps/space/app/assets/logos/google-logo.svg deleted file mode 100644 index 088288fa3..000000000 --- a/apps/space/app/assets/logos/google-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/space/app/assets/plane-logo.svg b/apps/space/app/assets/plane-logo.svg deleted file mode 100644 index 11179417c..000000000 --- a/apps/space/app/assets/plane-logo.svg +++ /dev/null @@ -1,94 +0,0 @@ - - - - diff --git a/apps/space/app/assets/plane-logos/black-horizontal-with-blue-logo.png b/apps/space/app/assets/plane-logos/black-horizontal-with-blue-logo.png deleted file mode 100644 index c14505a6f..000000000 Binary files a/apps/space/app/assets/plane-logos/black-horizontal-with-blue-logo.png and /dev/null differ diff --git a/apps/space/app/assets/plane-logos/blue-without-text-new.png b/apps/space/app/assets/plane-logos/blue-without-text-new.png deleted file mode 100644 index ea94aec79..000000000 Binary files a/apps/space/app/assets/plane-logos/blue-without-text-new.png and /dev/null differ diff --git a/apps/space/app/assets/plane-logos/blue-without-text.png b/apps/space/app/assets/plane-logos/blue-without-text.png deleted file mode 100644 index ea94aec79..000000000 Binary files a/apps/space/app/assets/plane-logos/blue-without-text.png and /dev/null differ diff --git a/apps/space/app/assets/plane-logos/white-horizontal-with-blue-logo.png b/apps/space/app/assets/plane-logos/white-horizontal-with-blue-logo.png deleted file mode 100644 index 97560fb9f..000000000 Binary files a/apps/space/app/assets/plane-logos/white-horizontal-with-blue-logo.png and /dev/null differ diff --git a/apps/space/app/assets/plane-logos/white-horizontal.svg b/apps/space/app/assets/plane-logos/white-horizontal.svg deleted file mode 100644 index 13e2dbb9f..000000000 --- a/apps/space/app/assets/plane-logos/white-horizontal.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/apps/space/app/assets/project-not-published.svg b/apps/space/app/assets/project-not-published.svg deleted file mode 100644 index db4b404df..000000000 --- a/apps/space/app/assets/project-not-published.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/space/app/assets/robots.txt b/apps/space/app/assets/robots.txt deleted file mode 100644 index 77470cb39..000000000 --- a/apps/space/app/assets/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: / \ No newline at end of file diff --git a/apps/space/app/assets/something-went-wrong.svg b/apps/space/app/assets/something-went-wrong.svg deleted file mode 100644 index bd51f7f49..000000000 --- a/apps/space/app/assets/something-went-wrong.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/space/app/assets/user-logged-in.svg b/apps/space/app/assets/user-logged-in.svg deleted file mode 100644 index e20b49e82..000000000 --- a/apps/space/app/assets/user-logged-in.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/space/app/compat/next/helper.ts b/apps/space/app/compat/next/helper.ts deleted file mode 100644 index c4edf3d54..000000000 --- a/apps/space/app/compat/next/helper.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -/** - * Ensures that a URL has a trailing slash while preserving query parameters and fragments - * @param url - The URL to process - * @returns The URL with a trailing slash added to the pathname (if not already present) - */ -export function ensureTrailingSlash(url: string): string { - try { - const fallbackBaseUrl = - typeof window !== "undefined" && window.location.origin ? window.location.origin : "http://dummy.com"; - // Handle relative URLs by creating a URL object with a fallback base URL - const urlObj = new URL(url, fallbackBaseUrl); - - // Don't modify root path - if (urlObj.pathname === "/") { - return url; - } - - // Add trailing slash if it doesn't exist - if (!urlObj.pathname.endsWith("/")) { - urlObj.pathname += "/"; - } - - // For relative URLs, return just the path + search + hash - if (url.startsWith("/")) { - return urlObj.pathname + urlObj.search + urlObj.hash; - } - - // For absolute URLs, return the full URL - return urlObj.toString(); - } catch (error) { - // If URL parsing fails, return the original URL - console.warn("Failed to parse URL for trailing slash enforcement:", url, error); - return url; - } -} diff --git a/apps/space/app/compat/next/navigation.ts b/apps/space/app/compat/next/navigation.ts deleted file mode 100644 index 0aa9254bc..000000000 --- a/apps/space/app/compat/next/navigation.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useMemo } from "react"; -import { useLocation, useNavigate, useParams as useParamsRR, useSearchParams as useSearchParamsRR } from "react-router"; -import { ensureTrailingSlash } from "./helper"; - -export function useRouter() { - const navigate = useNavigate(); - return useMemo( - () => ({ - push: (to: string) => navigate(ensureTrailingSlash(to)), - replace: (to: string) => navigate(ensureTrailingSlash(to), { replace: true }), - back: () => navigate(-1), - forward: () => navigate(1), - refresh: () => { - location.reload(); - }, - prefetch: async (_to: string) => { - // no-op in this shim - }, - }), - [navigate] - ); -} - -export function usePathname(): string { - const { pathname } = useLocation(); - return pathname; -} - -export function useSearchParams(): URLSearchParams { - const [searchParams] = useSearchParamsRR(); - return searchParams; -} - -export function useParams() { - return useParamsRR(); -} diff --git a/apps/space/app/entry.client.tsx b/apps/space/app/entry.client.tsx deleted file mode 100644 index 36c91ca80..000000000 --- a/apps/space/app/entry.client.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { initPromise } from "@plane/i18n"; -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; -import { HydratedRouter } from "react-router/dom"; - -// Initialize i18n before hydrating (the remix-i18next pattern for React -// Router: await init, then hydrateRoot). The server renders with an -// initialized instance; hydrating before the client instance is ready would -// make the first client render diverge from the server HTML, and React 19 -// leaves server DOM it could not adopt in place instead of clearing it. -void initPromise - .catch(() => {}) - .then(() => { - startTransition(() => { - hydrateRoot( - document, - - - - ); - }); - }); diff --git a/apps/space/app/error.tsx b/apps/space/app/error.tsx deleted file mode 100644 index 87aa8dc19..000000000 --- a/apps/space/app/error.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// ui -import { Button } from "@plane/propel/button"; - -function ErrorPage() { - const handleRetry = () => { - window.location.reload(); - }; - - return ( -
-
-
-

Yikes! That doesn{"'"}t look good.

-

- That crashed Plane, pun intended. No worries, though. Our engineers have been notified. If you have more - details, please write to{" "} - - support@plane.so - {" "} - or on our{" "} - - Forum - - . -

-
-
- - {/* */} -
-
-
- ); -} - -export default ErrorPage; diff --git a/apps/space/app/issues/[anchor]/layout.tsx b/apps/space/app/issues/[anchor]/layout.tsx deleted file mode 100644 index 6dbc2e711..000000000 --- a/apps/space/app/issues/[anchor]/layout.tsx +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { Outlet } from "react-router"; -import type { ShouldRevalidateFunctionArgs } from "react-router"; -import useSWR from "swr"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { PoweredBy } from "@/components/common/powered-by"; -import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error"; -import { IssuesNavbarRoot } from "@/components/issues/navbar"; -// hooks -import { PageNotFound } from "@/components/ui/not-found"; -import { usePublish, usePublishList } from "@/hooks/store/publish"; -import { useIssueFilter } from "@/hooks/store/use-issue-filter"; -import type { Route } from "./+types/layout"; - -const DEFAULT_TITLE = "Plane"; -const DEFAULT_DESCRIPTION = "Made with Plane, an AI-powered work management platform with publishing capabilities."; - -interface IssueMetadata { - name?: string; - description?: string; - cover_image?: string; -} - -// Loader function runs on the server and fetches metadata -export async function loader({ params }: Route.LoaderArgs) { - const { anchor } = params; - - // Validate anchor before using in request (only allow alphanumeric, -, _) - const ANCHOR_REGEX = /^[a-zA-Z0-9_-]+$/; - if (!ANCHOR_REGEX.test(anchor)) { - return { metadata: null }; - } - - try { - const response = await fetch(`${process.env.VITE_API_BASE_URL}/api/public/anchor/${anchor}/meta/`); - - if (!response.ok) { - return { metadata: null }; - } - - const metadata: IssueMetadata = await response.json(); - return { metadata }; - } catch (error) { - console.error("Error fetching issue metadata:", error); - return { metadata: null }; - } -} - -// Meta function uses the loader data to generate metadata -export function meta({ loaderData }: Route.MetaArgs) { - const metadata = loaderData?.metadata; - - const title = metadata?.name || DEFAULT_TITLE; - const description = metadata?.description || DEFAULT_DESCRIPTION; - const coverImage = metadata?.cover_image; - - const metaTags = [ - { title }, - { name: "description", content: description }, - // OpenGraph metadata - { property: "og:title", content: title }, - { property: "og:description", content: description }, - { property: "og:type", content: "website" }, - // Twitter metadata - { name: "twitter:card", content: "summary_large_image" }, - { name: "twitter:title", content: title }, - { name: "twitter:description", content: description }, - ]; - - // Add images if cover image exists - if (coverImage) { - metaTags.push( - { property: "og:image", content: coverImage }, - { property: "og:image:width", content: "800" }, - { property: "og:image:height", content: "600" }, - { property: "og:image:alt", content: title }, - { name: "twitter:image", content: coverImage } - ); - } - - return metaTags; -} - -// Prevent loader from re-running on anchor param changes -export function shouldRevalidate({ currentParams, nextParams }: ShouldRevalidateFunctionArgs) { - return currentParams.anchor !== nextParams.anchor; -} - -function IssuesLayout(props: Route.ComponentProps) { - const { anchor } = props.params; - // store hooks - const { fetchPublishSettings } = usePublishList(); - const publishSettings = usePublish(anchor); - const { updateLayoutOptions } = useIssueFilter(); - // fetch publish settings - const { error } = useSWR( - anchor ? `PUBLISH_SETTINGS_${anchor}` : null, - anchor - ? async () => { - const response = await fetchPublishSettings(anchor); - if (response.view_props) { - updateLayoutOptions({ - list: !!response.view_props.list, - kanban: !!response.view_props.kanban, - calendar: !!response.view_props.calendar, - gantt: !!response.view_props.gantt, - spreadsheet: !!response.view_props.spreadsheet, - }); - } - } - : null - ); - - if (!publishSettings && !error) { - return ( -
- -
- ); - } - - if (error?.status === 404) return ; - - if (error) return ; - - return ( - <> -
-
- -
-
- -
-
- - - ); -} - -export default observer(IssuesLayout); diff --git a/apps/space/app/issues/[anchor]/page.tsx b/apps/space/app/issues/[anchor]/page.tsx deleted file mode 100644 index cd19bda4b..000000000 --- a/apps/space/app/issues/[anchor]/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams, useSearchParams } from "next/navigation"; -import useSWR from "swr"; -// components -import { IssuesLayoutsRoot } from "@/components/issues/issue-layouts"; -// hooks -import { usePublish } from "@/hooks/store/publish"; -import { useLabel } from "@/hooks/store/use-label"; -import { useStates } from "@/hooks/store/use-state"; - -const IssuesPage = observer(function IssuesPage() { - // params - const params = useParams<{ anchor: string }>(); - const { anchor } = params; - const searchParams = useSearchParams(); - const peekId = searchParams.get("peekId") || undefined; - // store - const { fetchStates } = useStates(); - const { fetchLabels } = useLabel(); - - useSWR(anchor ? `PUBLIC_STATES_${anchor}` : null, anchor ? () => fetchStates(anchor) : null); - useSWR(anchor ? `PUBLIC_LABELS_${anchor}` : null, anchor ? () => fetchLabels(anchor) : null); - - const publishSettings = usePublish(anchor); - - if (!publishSettings) return null; - - return ; -}); - -export default IssuesPage; diff --git a/apps/space/app/not-found.tsx b/apps/space/app/not-found.tsx deleted file mode 100644 index e137fdd87..000000000 --- a/apps/space/app/not-found.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// assets -import SomethingWentWrongImage from "@/app/assets/something-went-wrong.svg?url"; - -function NotFound() { - return ( -
-
-
-
- Something went wrong -
-
-

That didn{"'"}t work

-

- Check the URL you are entering in the browser{"'"}s address bar and try again. -

-
-
- ); -} - -export default NotFound; diff --git a/apps/space/app/page.tsx b/apps/space/app/page.tsx deleted file mode 100644 index 02a9a5649..000000000 --- a/apps/space/app/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import { useSearchParams, useRouter } from "next/navigation"; -// plane imports -import { isValidNextPath } from "@plane/utils"; -// components -import { UserLoggedIn } from "@/components/account/user-logged-in"; -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { AuthView } from "@/components/views"; -// hooks -import { useUser } from "@/hooks/store/use-user"; -import type { Route } from "./+types/page"; - -export const headers: Route.HeadersFunction = () => ({ - "X-Frame-Options": "SAMEORIGIN", -}); - -const HomePage = observer(function HomePage() { - const { data: currentUser, isAuthenticated, isInitializing } = useUser(); - const searchParams = useSearchParams(); - const router = useRouter(); - const nextPath = searchParams.get("next_path"); - - useEffect(() => { - if (currentUser && isAuthenticated && nextPath && isValidNextPath(nextPath)) { - router.replace(nextPath); - } - }, [currentUser, isAuthenticated, nextPath, router]); - - if (isInitializing) - return ( -
- -
- ); - - if (currentUser && isAuthenticated) { - if (nextPath && isValidNextPath(nextPath)) { - return ( -
- -
- ); - } - return ; - } - - return ; -}); - -export default HomePage; diff --git a/apps/space/app/providers.tsx b/apps/space/app/providers.tsx deleted file mode 100644 index 463770ec5..000000000 --- a/apps/space/app/providers.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { ThemeProvider } from "next-themes"; -// components -import { TranslationProvider } from "@plane/i18n"; -import { AppProgressBar } from "@/lib/b-progress"; -import { InstanceProvider } from "@/lib/instance-provider"; -import { StoreProvider } from "@/lib/store-provider"; -import { ToastProvider } from "@/lib/toast-provider"; - -export function AppProviders({ children }: { children: React.ReactNode }) { - return ( - - - - - - {children} - - - - - ); -} diff --git a/apps/space/app/root.tsx b/apps/space/app/root.tsx deleted file mode 100644 index fe504b1ed..000000000 --- a/apps/space/app/root.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Links, Meta, Outlet, Scripts } from "react-router"; -// assets -import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url"; -import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url"; -import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url"; -import faviconIco from "@/app/assets/favicon/favicon.ico?url"; -import siteWebmanifest from "@/app/assets/favicon/site.webmanifest?url"; -import { LogoSpinner } from "@/components/common/logo-spinner"; -import globalStyles from "@/styles/globals.css?url"; -// types -import type { Route } from "./+types/root"; -// local imports -import ErrorPage from "./error"; -import { AppProviders } from "./providers"; -// fonts -import "@fontsource-variable/inter"; -import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url"; -import "@fontsource/material-symbols-rounded"; -import "@fontsource/ibm-plex-mono"; - -const APP_TITLE = "Plane Publish | Make your Plane boards public with one-click"; -const APP_DESCRIPTION = "Plane Publish is a customer feedback management tool built on top of plane.so"; - -export const links: Route.LinksFunction = () => [ - { rel: "apple-touch-icon", sizes: "180x180", href: appleTouchIcon }, - { rel: "icon", type: "image/png", sizes: "32x32", href: favicon32 }, - { rel: "icon", type: "image/png", sizes: "16x16", href: favicon16 }, - { rel: "shortcut icon", href: faviconIco }, - { rel: "manifest", href: siteWebmanifest }, - { rel: "stylesheet", href: globalStyles }, - { - rel: "preload", - href: interVariableWoff2, - as: "font", - type: "font/woff2", - crossOrigin: "anonymous", - }, -]; - -export const headers: Route.HeadersFunction = () => ({ - "Referrer-Policy": "origin-when-cross-origin", - "X-Content-Type-Options": "nosniff", - "X-DNS-Prefetch-Control": "on", - "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", -}); - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - -
- {children} - - - - ); -} - -export const meta: Route.MetaFunction = () => [ - { title: APP_TITLE }, - { name: "description", content: APP_DESCRIPTION }, - { property: "og:title", content: APP_TITLE }, - { property: "og:description", content: APP_DESCRIPTION }, - { property: "og:url", content: "https://sites.plane.so/" }, - { - name: "keywords", - content: - "software development, customer feedback, software, accelerate, code management, release management, project management, work item tracking, agile, scrum, kanban, collaboration", - }, - { name: "twitter:site", content: "@planepowers" }, -]; - -export default function Root() { - return ; -} - -export function HydrateFallback() { - return ( -
- -
- ); -} - -export function ErrorBoundary({ error: _error }: Route.ErrorBoundaryProps) { - return ; -} diff --git a/apps/space/app/routes.ts b/apps/space/app/routes.ts deleted file mode 100644 index 1a94ca0f7..000000000 --- a/apps/space/app/routes.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { RouteConfig } from "@react-router/dev/routes"; -import { index, layout, route } from "@react-router/dev/routes"; - -export default [ - index("./page.tsx"), - route(":workspaceSlug/:projectId", "./[workspaceSlug]/[projectId]/page.tsx"), - layout("./issues/[anchor]/layout.tsx", [route("issues/:anchor", "./issues/[anchor]/page.tsx")]), - // Catch-all route for 404 handling - route("*", "./not-found.tsx"), -] satisfies RouteConfig; diff --git a/apps/space/app/types/next-link.d.ts b/apps/space/app/types/next-link.d.ts deleted file mode 100644 index e39cc5205..000000000 --- a/apps/space/app/types/next-link.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module "next/link" { - import type { FC, ComponentProps } from "react"; - - type NextLinkProps = ComponentProps<"a"> & { - href: string; - replace?: boolean; - prefetch?: boolean; - scroll?: boolean; - shallow?: boolean; - }; - - const Link: FC; - export default Link; -} diff --git a/apps/space/app/types/next-navigation.d.ts b/apps/space/app/types/next-navigation.d.ts deleted file mode 100644 index 67a80c4fa..000000000 --- a/apps/space/app/types/next-navigation.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module "next/navigation" { - export function useRouter(): { - push: (url: string) => void; - replace: (url: string) => void; - back: () => void; - forward: () => void; - refresh: () => void; - prefetch: (url: string) => Promise; - }; - - export function usePathname(): string; - export function useSearchParams(): URLSearchParams; - export function useParams>(): T; -} diff --git a/apps/space/app/types/react-router-virtual.d.ts b/apps/space/app/types/react-router-virtual.d.ts deleted file mode 100644 index 68caf20ae..000000000 --- a/apps/space/app/types/react-router-virtual.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module "virtual:react-router/server-build" { - import type { ServerBuild } from "react-router"; - const serverBuild: ServerBuild; - export default serverBuild; -} diff --git a/apps/space/components/account/auth-forms/auth-banner.tsx b/apps/space/components/account/auth-forms/auth-banner.tsx deleted file mode 100644 index e69e8ae27..000000000 --- a/apps/space/components/account/auth-forms/auth-banner.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { CloseOutline, InfoOutline } from "@makeplane/propel/icons"; -// helpers -import type { TAuthErrorInfo } from "@/helpers/authentication.helper"; - -type TAuthBanner = { - bannerData: TAuthErrorInfo | undefined; - handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void; -}; - -export function AuthBanner(props: TAuthBanner) { - const { bannerData, handleBannerData } = props; - - if (!bannerData) return <>; - return ( -
-
- -
-
{bannerData?.message}
-
handleBannerData && handleBannerData(undefined)} - > - -
-
- ); -} diff --git a/apps/space/components/account/auth-forms/auth-header.tsx b/apps/space/components/account/auth-forms/auth-header.tsx deleted file mode 100644 index a2063fe73..000000000 --- a/apps/space/components/account/auth-forms/auth-header.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// helpers -import { EAuthModes } from "@/types/auth"; - -type TAuthHeader = { - authMode: EAuthModes; -}; - -type TAuthHeaderContent = { - header: string; - subHeader: string; -}; - -type TAuthHeaderDetails = { - [mode in EAuthModes]: TAuthHeaderContent; -}; - -const Titles: TAuthHeaderDetails = { - [EAuthModes.SIGN_IN]: { - header: "Sign in to upvote or comment", - subHeader: "Contribute in nudging the features you want to get built.", - }, - [EAuthModes.SIGN_UP]: { - header: "View, comment, and do more", - subHeader: "Sign up or log in to work with Plane work items and Pages.", - }, -}; - -export function AuthHeader(props: TAuthHeader) { - const { authMode } = props; - - const getHeaderSubHeader = (mode: EAuthModes | null): TAuthHeaderContent => { - if (mode) { - return Titles[mode]; - } - - return { - header: "Comment or react to work items", - subHeader: "Use plane to add your valuable inputs to features.", - }; - }; - - const { header, subHeader } = getHeaderSubHeader(authMode); - - return ( - <> -
- {header} - {subHeader} -
- - ); -} diff --git a/apps/space/components/account/auth-forms/auth-root.tsx b/apps/space/components/account/auth-forms/auth-root.tsx deleted file mode 100644 index d31649f78..000000000 --- a/apps/space/components/account/auth-forms/auth-root.tsx +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -// plane imports -import { SitesAuthService } from "@plane/services"; -import type { IEmailCheckData } from "@plane/types"; -import { OAuthOptions } from "@plane/ui"; -// helpers -import type { TAuthErrorInfo } from "@/helpers/authentication.helper"; -import { EErrorAlertType, authErrorHandler, EAuthenticationErrorCodes } from "@/helpers/authentication.helper"; -// hooks -import { useOAuthConfig } from "@/hooks/oauth"; -import { useInstance } from "@/hooks/store/use-instance"; -// types -import { EAuthModes, EAuthSteps } from "@/types/auth"; -// local imports -import { TermsAndConditions } from "../terms-and-conditions"; -import { AuthBanner } from "./auth-banner"; -import { AuthHeader } from "./auth-header"; -import { AuthEmailForm } from "./email"; -import { AuthPasswordForm } from "./password"; -import { AuthUniqueCodeForm } from "./unique-code"; - -const authService = new SitesAuthService(); - -export const AuthRoot = observer(function AuthRoot() { - // router params - const searchParams = useSearchParams(); - const emailParam = searchParams.get("email") || undefined; - const error_code = searchParams.get("error_code") || undefined; - const nextPath = searchParams.get("next_path") || undefined; - // states - const [authMode, setAuthMode] = useState(EAuthModes.SIGN_UP); - const [authStep, setAuthStep] = useState(EAuthSteps.EMAIL); - const [email, setEmail] = useState(emailParam ? emailParam.toString() : ""); - const [errorInfo, setErrorInfo] = useState(undefined); - const [isPasswordAutoset, setIsPasswordAutoset] = useState(true); - // hooks - const { config } = useInstance(); - - useEffect(() => { - if (error_code) { - const errorhandler = authErrorHandler(error_code?.toString() as EAuthenticationErrorCodes); - if (errorhandler) { - if (errorhandler.code === EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN) { - setAuthMode(EAuthModes.SIGN_IN); - setAuthStep(EAuthSteps.PASSWORD); - } - if (errorhandler.code === EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP) { - setAuthMode(EAuthModes.SIGN_UP); - setAuthStep(EAuthSteps.PASSWORD); - } - if ( - [ - EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_IN, - EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN, - EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN, - ].includes(errorhandler.code) - ) { - setAuthMode(EAuthModes.SIGN_IN); - setAuthStep(EAuthSteps.UNIQUE_CODE); - } - if ( - [ - EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_UP, - EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP, - EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP, - ].includes(errorhandler.code) - ) { - setAuthMode(EAuthModes.SIGN_UP); - setAuthStep(EAuthSteps.UNIQUE_CODE); - } - setErrorInfo(errorhandler); - } - } - }, [error_code]); - - const isSMTPConfigured = config?.is_smtp_configured || false; - const isMagicLoginEnabled = config?.is_magic_login_enabled || false; - const isEmailPasswordEnabled = config?.is_email_password_enabled || false; - const oAuthActionText = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in"; - const { isOAuthEnabled, oAuthOptions } = useOAuthConfig(oAuthActionText); - - // submit handler- email verification - const handleEmailVerification = async (data: IEmailCheckData) => { - setEmail(data.email); - - await authService - .emailCheck(data) - .then(async (response) => { - let currentAuthMode: EAuthModes = response.existing ? EAuthModes.SIGN_IN : EAuthModes.SIGN_UP; - if (response.existing) { - currentAuthMode = EAuthModes.SIGN_IN; - setAuthMode(() => EAuthModes.SIGN_IN); - } else { - currentAuthMode = EAuthModes.SIGN_UP; - setAuthMode(() => EAuthModes.SIGN_UP); - } - - if (currentAuthMode === EAuthModes.SIGN_IN) { - if (isSMTPConfigured && isMagicLoginEnabled && response.status === "MAGIC_CODE") { - setAuthStep(EAuthSteps.UNIQUE_CODE); - generateEmailUniqueCode(data.email); - } else if (isEmailPasswordEnabled) { - setIsPasswordAutoset(false); - setAuthStep(EAuthSteps.PASSWORD); - } else { - const errorhandler = authErrorHandler("5005" as EAuthenticationErrorCodes); - setErrorInfo(errorhandler); - } - } else { - if (isSMTPConfigured && isMagicLoginEnabled && response.status === "MAGIC_CODE") { - setAuthStep(EAuthSteps.UNIQUE_CODE); - generateEmailUniqueCode(data.email); - } else if (isEmailPasswordEnabled) { - setAuthStep(EAuthSteps.PASSWORD); - } else { - const errorhandler = authErrorHandler("5006" as EAuthenticationErrorCodes); - setErrorInfo(errorhandler); - } - } - return; - }) - .catch((error) => { - const errorhandler = authErrorHandler(error?.error_code?.toString(), data?.email || undefined); - if (errorhandler?.type) setErrorInfo(errorhandler); - }); - }; - - // generating the unique code - const generateEmailUniqueCode = async (email: string): Promise<{ code: string } | undefined> => { - const payload = { email: email }; - return await authService - .generateUniqueCode(payload) - .then(() => ({ code: "" })) - .catch((error) => { - const errorhandler = authErrorHandler(error?.error_code.toString()); - if (errorhandler?.type) setErrorInfo(errorhandler); - throw error; - }); - }; - - return ( -
-
- {errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && ( - setErrorInfo(value)} /> - )} - - {isOAuthEnabled && } - - {authStep === EAuthSteps.EMAIL && } - {authStep === EAuthSteps.UNIQUE_CODE && ( - { - setEmail(""); - setAuthStep(EAuthSteps.EMAIL); - }} - generateEmailUniqueCode={generateEmailUniqueCode} - /> - )} - {authStep === EAuthSteps.PASSWORD && ( - { - setEmail(""); - setAuthStep(EAuthSteps.EMAIL); - }} - handleAuthStep={(step: EAuthSteps) => { - if (step === EAuthSteps.UNIQUE_CODE) generateEmailUniqueCode(email); - setAuthStep(step); - }} - /> - )} - -
-
- ); -}); diff --git a/apps/space/components/account/auth-forms/email.tsx b/apps/space/components/account/auth-forms/email.tsx deleted file mode 100644 index 997c3df7d..000000000 --- a/apps/space/components/account/auth-forms/email.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { FormEvent } from "react"; -import { useMemo, useRef, useState } from "react"; -import { observer } from "mobx-react"; -// icons -import { CloseCircleOutline, WarningCircleOutline } from "@makeplane/propel/icons"; -// types -import { Button } from "@plane/propel/button"; -import type { IEmailCheckData } from "@plane/types"; -// ui -import { Input, Spinner } from "@plane/ui"; -// helpers -import { cn } from "@plane/utils"; -import { checkEmailValidity } from "@/helpers/string.helper"; - -type TAuthEmailForm = { - defaultEmail: string; - onSubmit: (data: IEmailCheckData) => Promise; -}; - -export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailForm) { - const { onSubmit, defaultEmail } = props; - // states - const [isSubmitting, setIsSubmitting] = useState(false); - const [email, setEmail] = useState(defaultEmail); - - const emailError = useMemo( - () => (email && !checkEmailValidity(email) ? { email: "Email is invalid" } : undefined), - [email] - ); - - const handleFormSubmit = async (event: FormEvent) => { - event.preventDefault(); - setIsSubmitting(true); - const payload: IEmailCheckData = { - email: email, - }; - await onSubmit(payload); - setIsSubmitting(false); - }; - - const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting; - - const [isFocused, setIsFocused] = useState(true); - const inputRef = useRef(null); - - return ( -
-
- -
{ - setIsFocused(true); - }} - onBlur={() => { - setIsFocused(false); - }} - > - setEmail(e.target.value)} - placeholder="name@company.com" - className={`h-10 w-full border-0 disable-autofill-style placeholder:text-placeholder autofill:bg-danger-subtle focus:bg-none active:bg-transparent`} - autoComplete="off" - autoFocus - ref={inputRef} - /> - {email.length > 0 && ( - - )} -
- {emailError?.email && !isFocused && ( -

- - {emailError.email} -

- )} -
- -
- ); -}); diff --git a/apps/space/components/account/auth-forms/index.ts b/apps/space/components/account/auth-forms/index.ts deleted file mode 100644 index 125f6699c..000000000 --- a/apps/space/components/account/auth-forms/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./auth-root"; diff --git a/apps/space/components/account/auth-forms/password.tsx b/apps/space/components/account/auth-forms/password.tsx deleted file mode 100644 index 3b17cdf18..000000000 --- a/apps/space/components/account/auth-forms/password.tsx +++ /dev/null @@ -1,268 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useEffect, useMemo, useRef, useState } from "react"; -import { observer } from "mobx-react"; -import { CloseCircleOutline, HideOutline, ShowOutline } from "@makeplane/propel/icons"; -// plane imports -import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants"; -import { Button } from "@plane/propel/button"; -import { AuthService } from "@plane/services"; -import { Input, Spinner, PasswordStrengthIndicator } from "@plane/ui"; -import { getPasswordStrength } from "@plane/utils"; -// types -import { EAuthModes, EAuthSteps } from "@/types/auth"; - -type Props = { - email: string; - isPasswordAutoset: boolean; - isSMTPConfigured: boolean; - mode: EAuthModes; - nextPath: string | undefined; - handleEmailClear: () => void; - handleAuthStep: (step: EAuthSteps) => void; -}; - -type TPasswordFormValues = { - email: string; - password: string; - confirm_password?: string; -}; - -const defaultValues: TPasswordFormValues = { - email: "", - password: "", -}; - -const authService = new AuthService(); - -export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props) { - const { email, nextPath, isSMTPConfigured, handleAuthStep, handleEmailClear, mode } = props; - // ref - const formRef = useRef(null); - // states - const [csrfPromise, setCsrfPromise] = useState | undefined>(undefined); - const [passwordFormData, setPasswordFormData] = useState({ ...defaultValues, email }); - const [showPassword, setShowPassword] = useState({ - password: false, - retypePassword: false, - }); - const [isSubmitting, setIsSubmitting] = useState(false); - const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); - const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false); - - const handleShowPassword = (key: keyof typeof showPassword) => - setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); - - const handleFormChange = (key: keyof TPasswordFormValues, value: string) => - setPasswordFormData((prev) => ({ ...prev, [key]: value })); - - useEffect(() => { - if (csrfPromise === undefined) { - const promise = authService.requestCSRFToken(); - setCsrfPromise(promise); - } - }, [csrfPromise]); - - const redirectToUniqueCodeSignIn = async () => { - handleAuthStep(EAuthSteps.UNIQUE_CODE); - }; - - const passwordSupport = passwordFormData.password.length > 0 && - mode === EAuthModes.SIGN_UP && - getPasswordStrength(passwordFormData.password) != E_PASSWORD_STRENGTH.STRENGTH_VALID && ( - - ); - - const isButtonDisabled = useMemo( - () => - !isSubmitting && - !!passwordFormData.password && - (mode === EAuthModes.SIGN_UP - ? getPasswordStrength(passwordFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID && - passwordFormData.password === passwordFormData.confirm_password - : true) - ? false - : true, - [isSubmitting, mode, passwordFormData.confirm_password, passwordFormData.password] - ); - - const password = passwordFormData.password ?? ""; - const confirmPassword = passwordFormData.confirm_password ?? ""; - const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length; - - const handleCSRFToken = async () => { - if (!formRef || !formRef.current) return; - const token = await csrfPromise; - if (!token?.csrf_token) return; - const csrfElement = formRef.current.querySelector("input[name=csrfmiddlewaretoken]"); - csrfElement?.setAttribute("value", token?.csrf_token); - }; - - return ( -
{ - event.preventDefault(); - await handleCSRFToken(); - if (formRef.current) { - formRef.current.submit(); - } - setIsSubmitting(true); - }} - onError={() => setIsSubmitting(false)} - > - - - -
- -
- handleFormChange("email", e.target.value)} - placeholder="name@company.com" - className={`h-10 w-full border-0 disable-autofill-style placeholder:text-placeholder`} - disabled - /> - {passwordFormData.email.length > 0 && ( - - )} -
-
- -
- -
- handleFormChange("password", e.target.value)} - placeholder="Enter password" - className="h-10 w-full border border-subtle !bg-surface-1 pr-12 disable-autofill-style placeholder:text-placeholder" - onFocus={() => setIsPasswordInputFocused(true)} - onBlur={() => setIsPasswordInputFocused(false)} - autoComplete="off" - autoFocus - /> - {showPassword?.password ? ( - - ) : ( - - )} -
- {passwordSupport} -
- - {mode === EAuthModes.SIGN_UP && ( -
- -
- handleFormChange("confirm_password", e.target.value)} - placeholder="Confirm password" - className="h-10 w-full border border-subtle !bg-surface-1 pr-12 disable-autofill-style placeholder:text-placeholder" - onFocus={() => setIsRetryPasswordInputFocused(true)} - onBlur={() => setIsRetryPasswordInputFocused(false)} - autoComplete="off" - /> - {showPassword?.retypePassword ? ( - - ) : ( - - )} -
- {!!passwordFormData.confirm_password && - passwordFormData.password !== passwordFormData.confirm_password && - renderPasswordMatchError && Passwords don{"'"}t match} -
- )} - -
- {mode === EAuthModes.SIGN_IN ? ( - <> - - {isSMTPConfigured && ( - - )} - - ) : ( - - )} -
-
- ); -}); diff --git a/apps/space/components/account/auth-forms/unique-code.tsx b/apps/space/components/account/auth-forms/unique-code.tsx deleted file mode 100644 index 61f058726..000000000 --- a/apps/space/components/account/auth-forms/unique-code.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useEffect, useState } from "react"; -import { CloseCircleOutline, TickCircleOutline } from "@makeplane/propel/icons"; -// plane imports -import { API_BASE_URL } from "@plane/constants"; -import { Button } from "@plane/propel/button"; -import { AuthService } from "@plane/services"; -import { Input, Spinner } from "@plane/ui"; -// hooks -import useTimer from "@/hooks/use-timer"; -// types -import { EAuthModes } from "@/types/auth"; - -// services -const authService = new AuthService(); - -type TAuthUniqueCodeForm = { - mode: EAuthModes; - email: string; - nextPath: string | undefined; - handleEmailClear: () => void; - generateEmailUniqueCode: (email: string) => Promise<{ code: string } | undefined>; -}; - -type TUniqueCodeFormValues = { - email: string; - code: string; -}; - -const defaultValues: TUniqueCodeFormValues = { - email: "", - code: "", -}; - -export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) { - const { mode, email, nextPath, handleEmailClear, generateEmailUniqueCode } = props; - // derived values - const defaultResetTimerValue = 5; - // states - const [uniqueCodeFormData, setUniqueCodeFormData] = useState({ ...defaultValues, email }); - const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); - const [csrfToken, setCsrfToken] = useState(undefined); - const [isSubmitting, setIsSubmitting] = useState(false); - // timer - const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0); - - const handleFormChange = (key: keyof TUniqueCodeFormValues, value: string) => - setUniqueCodeFormData((prev) => ({ ...prev, [key]: value })); - - const generateNewCode = async (email: string) => { - try { - setIsRequestingNewCode(true); - const uniqueCode = await generateEmailUniqueCode(email); - setResendCodeTimer(defaultResetTimerValue); - handleFormChange("code", uniqueCode?.code || ""); - setIsRequestingNewCode(false); - } catch { - setResendCodeTimer(0); - console.error("Error while requesting new code"); - setIsRequestingNewCode(false); - } - }; - - useEffect(() => { - if (csrfToken === undefined) - authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); - }, [csrfToken]); - - const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0; - const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting; - - return ( -
setIsSubmitting(true)} - onError={() => setIsSubmitting(false)} - > - - - -
- -
- handleFormChange("email", e.target.value)} - placeholder="name@company.com" - className={`h-10 w-full border-0 disable-autofill-style placeholder:text-placeholder`} - autoComplete="off" - disabled - /> - {uniqueCodeFormData.email.length > 0 && ( - - )} -
-
- -
- - handleFormChange("code", e.target.value)} - placeholder="123456" - className="h-10 w-full border border-subtle !bg-surface-1 pr-12 disable-autofill-style placeholder:text-placeholder" - autoComplete="off" - autoFocus - /> -
-

- - Paste the code sent to your email -

- -
-
- -
- -
-
- ); -} diff --git a/apps/space/components/account/terms-and-conditions.tsx b/apps/space/components/account/terms-and-conditions.tsx deleted file mode 100644 index 1e9a71a6e..000000000 --- a/apps/space/components/account/terms-and-conditions.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -type Props = { - isSignUp?: boolean; -}; - -export function TermsAndConditions(props: Props) { - const { isSignUp = false } = props; - return ( - -

- {isSignUp ? "By creating an account" : "By signing in"}, you agree to our{" \n"} - - Terms of Service - {" "} - and{" "} - - Privacy Policy - - {"."} -

-
- ); -} diff --git a/apps/space/components/account/user-logged-in.tsx b/apps/space/components/account/user-logged-in.tsx deleted file mode 100644 index 42815b013..000000000 --- a/apps/space/components/account/user-logged-in.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { PlaneLockup } from "@plane/propel/icons"; -// assets -import UserLoggedInImage from "@/app/assets/user-logged-in.svg?url"; -// components -import { PoweredBy } from "@/components/common/powered-by"; -import { UserAvatar } from "@/components/issues/navbar/user-avatar"; -// hooks -import { useUser } from "@/hooks/store/use-user"; - -export const UserLoggedIn = observer(function UserLoggedIn() { - // store hooks - const { data: user } = useUser(); - - if (!user) return null; - - return ( -
-
- - -
- -
-
-
-
- User already logged in -
-
-

Nice! Just one more step.

-

- Enter the public-share URL or link of the view or Page you are trying to see in the browser{"'"}s address - bar. -

-
-
- -
- ); -}); diff --git a/apps/space/components/common/logo-spinner.tsx b/apps/space/components/common/logo-spinner.tsx deleted file mode 100644 index 3f0f17938..000000000 --- a/apps/space/components/common/logo-spinner.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useTheme } from "next-themes"; -// assets -import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url"; -import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url"; - -export function LogoSpinner() { - const { resolvedTheme } = useTheme(); - - const logoSrc = resolvedTheme === "dark" ? LogoSpinnerLight : LogoSpinnerDark; - - return ( -
- logo -
- ); -} diff --git a/apps/space/components/common/powered-by.tsx b/apps/space/components/common/powered-by.tsx deleted file mode 100644 index 05da51210..000000000 --- a/apps/space/components/common/powered-by.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { WEBSITE_URL } from "@plane/constants"; -// assets -import { PlaneLogo } from "@plane/propel/icons"; - -type TPoweredBy = { - disabled?: boolean; -}; - -export function PoweredBy(props: TPoweredBy) { - // props - const { disabled = false } = props; - - if (disabled || !WEBSITE_URL) return null; - - return ( - - -
- Powered by Plane Publish -
-
- ); -} diff --git a/apps/space/components/common/project-logo.tsx b/apps/space/components/common/project-logo.tsx deleted file mode 100644 index b9540c0a0..000000000 --- a/apps/space/components/common/project-logo.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// types -import type { TLogoProps } from "@plane/types"; -// helpers -import { cn } from "@plane/utils"; - -type Props = { - className?: string; - logo: TLogoProps; -}; - -export function ProjectLogo(props: Props) { - const { className, logo } = props; - - if (logo.in_use === "icon" && logo.icon) - return ( - - {logo.icon.name} - - ); - - if (logo.in_use === "emoji" && logo.emoji) - return ( - - {logo.emoji.value?.split("-").map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))} - - ); - - return ; -} diff --git a/apps/space/components/editor/embeds/mentions/index.ts b/apps/space/components/editor/embeds/mentions/index.ts deleted file mode 100644 index d98033459..000000000 --- a/apps/space/components/editor/embeds/mentions/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./root"; diff --git a/apps/space/components/editor/embeds/mentions/root.tsx b/apps/space/components/editor/embeds/mentions/root.tsx deleted file mode 100644 index 2c6914a29..000000000 --- a/apps/space/components/editor/embeds/mentions/root.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { TCallbackMentionComponentProps } from "@plane/editor"; -// local components -import { EditorUserMention } from "./user"; - -export function EditorMentionsRoot(props: TCallbackMentionComponentProps) { - const { entity_identifier, entity_name } = props; - - switch (entity_name) { - case "user_mention": - return ; - default: - return null; - } -} diff --git a/apps/space/components/editor/embeds/mentions/user.tsx b/apps/space/components/editor/embeds/mentions/user.tsx deleted file mode 100644 index 24c817df4..000000000 --- a/apps/space/components/editor/embeds/mentions/user.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// helpers -import { cn } from "@plane/utils"; -// hooks -import { useMember } from "@/hooks/store/use-member"; -import { useUser } from "@/hooks/store/use-user"; - -type Props = { - id: string; -}; - -export const EditorUserMention = observer(function EditorUserMention(props: Props) { - const { id } = props; - // store hooks - const { data: currentUser } = useUser(); - const { getMemberById } = useMember(); - // derived values - const userDetails = getMemberById(id); - - if (!userDetails) { - return ( -
- @deactivated user -
- ); - } - - return ( -
- @{userDetails?.member__display_name} -
- ); -}); diff --git a/apps/space/components/editor/lite-text-editor.tsx b/apps/space/components/editor/lite-text-editor.tsx deleted file mode 100644 index ecd08da70..000000000 --- a/apps/space/components/editor/lite-text-editor.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -// plane imports -import { LiteTextEditorWithRef } from "@plane/editor"; -import type { EditorRefApi, ILiteTextEditorProps, TFileHandler } from "@plane/editor"; -import type { MakeOptional } from "@plane/types"; -import { cn, isCommentEmpty } from "@plane/utils"; -// helpers -import { getEditorFileHandlers } from "@/helpers/editor.helper"; -// hooks -import { useParseEditorContent } from "@/hooks/use-parse-editor-content"; -// plane web imports -import { useEditorFlagging } from "@/hooks/use-editor-flagging"; -// local imports -import { EditorMentionsRoot } from "./embeds/mentions"; -import { IssueCommentToolbar } from "./toolbar"; - -type LiteTextEditorWrapperProps = MakeOptional< - Omit, - "disabledExtensions" | "flaggedExtensions" | "getEditorMetaData" -> & { - anchor: string; - isSubmitting?: boolean; - showSubmitButton?: boolean; - workspaceId: string; -} & ( - | { - editable: false; - } - | { - editable: true; - uploadFile: TFileHandler["upload"]; - } - ); - -export const LiteTextEditor = React.forwardRef(function LiteTextEditor( - props: LiteTextEditorWrapperProps, - ref: React.ForwardedRef -) { - const { - anchor, - containerClassName, - disabledExtensions: additionalDisabledExtensions = [], - editable, - isSubmitting = false, - showSubmitButton = true, - workspaceId, - ...rest - } = props; - function isMutableRefObject(ref: React.ForwardedRef): ref is React.MutableRefObject { - return !!ref && typeof ref === "object" && "current" in ref; - } - // derived values - const isEmpty = isCommentEmpty(props.initialValue); - const editorRef = isMutableRefObject(ref) ? ref.current : null; - const { liteText: liteTextEditorExtensions } = useEditorFlagging(anchor); - // parse content - const { getEditorMetaData } = useParseEditorContent({ - anchor, - }); - - return ( -
- "", - workspaceId, - })} - getEditorMetaData={getEditorMetaData} - mentionHandler={{ - renderComponent: (props) => , - }} - extendedEditorProps={{}} - {...rest} - // overriding the containerClassName to add relative class passed - containerClassName={cn(containerClassName, "relative")} - /> - {editable && ( - { - // TODO: update this while toolbar homogenization - // @ts-expect-error type mismatch here - editorRef?.executeMenuItemCommand({ - itemKey: item.itemKey, - ...item.extraProps, - }); - }} - isSubmitting={isSubmitting} - showSubmitButton={showSubmitButton} - handleSubmit={(e) => rest.onEnterKeyPress?.(e)} - isCommentEmpty={isEmpty} - editorRef={editorRef} - /> - )} -
- ); -}); - -LiteTextEditor.displayName = "LiteTextEditor"; diff --git a/apps/space/components/editor/rich-text-editor.tsx b/apps/space/components/editor/rich-text-editor.tsx deleted file mode 100644 index b7846e408..000000000 --- a/apps/space/components/editor/rich-text-editor.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { forwardRef } from "react"; -// plane imports -import { RichTextEditorWithRef } from "@plane/editor"; -import type { EditorRefApi, IRichTextEditorProps, TFileHandler } from "@plane/editor"; -import type { MakeOptional } from "@plane/types"; -// helpers -import { getEditorFileHandlers } from "@/helpers/editor.helper"; -// hooks -import { useMember } from "@/hooks/store/use-member"; -import { useParseEditorContent } from "@/hooks/use-parse-editor-content"; -// plane web imports -import { useEditorFlagging } from "@/hooks/use-editor-flagging"; -// local imports -import { EditorMentionsRoot } from "./embeds/mentions"; - -type RichTextEditorWrapperProps = MakeOptional< - Omit, - "disabledExtensions" | "flaggedExtensions" | "getEditorMetaData" -> & { - anchor: string; - workspaceId: string; -} & ( - | { - editable: false; - } - | { - editable: true; - uploadFile: TFileHandler["upload"]; - } - ); - -export const RichTextEditor = forwardRef(function RichTextEditor( - props: RichTextEditorWrapperProps, - ref: React.ForwardedRef -) { - const { - anchor, - containerClassName, - editable, - workspaceId, - disabledExtensions: additionalDisabledExtensions = [], - ...rest - } = props; - // store hooks - const { getMemberById } = useMember(); - // parse content - const { getEditorMetaData } = useParseEditorContent({ - anchor, - }); - // editor flaggings - const { richText: richTextEditorExtensions } = useEditorFlagging(anchor); - - return ( - , - getMentionedEntityDetails: (id: string) => ({ - display_name: getMemberById(id)?.member__display_name ?? "", - }), - }} - ref={ref} - disabledExtensions={[...richTextEditorExtensions.disabled, ...additionalDisabledExtensions]} - editable={editable} - fileHandler={getEditorFileHandlers({ - anchor, - uploadFile: editable ? props.uploadFile : async () => "", - workspaceId, - })} - getEditorMetaData={getEditorMetaData} - flaggedExtensions={richTextEditorExtensions.flagged} - extendedEditorProps={{}} - {...rest} - containerClassName={containerClassName} - editorClassName="min-h-[100px] py-2 overflow-hidden" - displayConfig={{ fontSize: "large-font" }} - /> - ); -}); - -RichTextEditor.displayName = "RichTextEditor"; diff --git a/apps/space/components/editor/toolbar.tsx b/apps/space/components/editor/toolbar.tsx deleted file mode 100644 index ce2a0a2c6..000000000 --- a/apps/space/components/editor/toolbar.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useEffect, useState, useCallback } from "react"; -// plane imports -import { TOOLBAR_ITEMS } from "@plane/editor"; -import type { ToolbarMenuItem, EditorRefApi } from "@plane/editor"; -import { Button } from "@plane/propel/button"; -import { Tooltip } from "@plane/propel/tooltip"; -import { cn } from "@plane/utils"; - -type Props = { - executeCommand: (item: ToolbarMenuItem) => void; - handleSubmit: (event: React.MouseEvent) => void; - isCommentEmpty: boolean; - isSubmitting: boolean; - showSubmitButton: boolean; - editorRef: EditorRefApi | null; -}; - -const toolbarItems = TOOLBAR_ITEMS.lite; - -export function IssueCommentToolbar(props: Props) { - const { executeCommand, handleSubmit, isCommentEmpty, editorRef, isSubmitting, showSubmitButton } = props; - // states - const [activeStates, setActiveStates] = useState>({}); - - // Function to update active states - const updateActiveStates = useCallback(() => { - if (!editorRef) return; - const newActiveStates: Record = {}; - Object.values(toolbarItems) - .flat() - .forEach((item) => { - // TODO: update this while toolbar homogenization - // @ts-expect-error type mismatch here - newActiveStates[item.renderKey] = editorRef.isMenuItemActive({ - itemKey: item.itemKey, - ...item.extraProps, - }); - }); - setActiveStates(newActiveStates); - }, [editorRef]); - - // useEffect to call updateActiveStates when isActive prop changes - useEffect(() => { - if (!editorRef) return; - const unsubscribe = editorRef.onStateChange(updateActiveStates); - updateActiveStates(); - return () => unsubscribe(); - }, [editorRef, updateActiveStates]); - - return ( -
-
-
- {Object.keys(toolbarItems).map((key, index) => ( -
- {toolbarItems[key].map((item) => { - const isItemActive = activeStates[item.renderKey]; - - return ( - - {item.name} - {item.shortcut && {item.shortcut.join(" + ")}} -

- } - > - -
- ); - })} -
- ))} -
- {showSubmitButton && ( -
- -
- )} -
-
- ); -} diff --git a/apps/space/components/instance/instance-failure-view.tsx b/apps/space/components/instance/instance-failure-view.tsx deleted file mode 100644 index 2bf810e9d..000000000 --- a/apps/space/components/instance/instance-failure-view.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useTheme } from "next-themes"; -import { Button } from "@plane/propel/button"; -// assets -import InstanceFailureDarkImage from "@/app/assets/instance/instance-failure-dark.svg?url"; -import InstanceFailureImage from "@/app/assets/instance/instance-failure.svg?url"; - -export function InstanceFailureView() { - const { resolvedTheme } = useTheme(); - - const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage; - - const handleRetry = () => { - window.location.reload(); - }; - - return ( -
-
-
- Plane instance failure image -

Unable to fetch instance details.

-

- We were unable to fetch the details of the instance.
- Fret not, it might just be a connectivity work items. -

-
-
- -
-
-
- ); -} diff --git a/apps/space/components/issues/filters/applied-filters/filters-list.tsx b/apps/space/components/issues/filters/applied-filters/filters-list.tsx deleted file mode 100644 index 701fb4959..000000000 --- a/apps/space/components/issues/filters/applied-filters/filters-list.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useTranslation } from "@plane/i18n"; -import { CloseOutline } from "@makeplane/propel/icons"; -// types -import type { TFilters } from "@/types/issue"; -// components -import { AppliedPriorityFilters } from "./priority"; -import { AppliedStateFilters } from "./state"; - -type Props = { - appliedFilters: TFilters; - handleRemoveAllFilters: () => void; - handleRemoveFilter: (key: keyof TFilters, value: string | null) => void; -}; - -export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); - -export const AppliedFiltersList = observer(function AppliedFiltersList(props: Props) { - const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter } = props; - const { t } = useTranslation(); - - return ( -
- {Object.entries(appliedFilters).map(([key, value]) => { - const filterKey = key as keyof TFilters; - const filterValue = value as TFilters[keyof TFilters]; - - if (!filterValue) return; - - return ( -
- {replaceUnderscoreIfSnakeCase(filterKey)} -
- {filterKey === "priority" && ( - handleRemoveFilter("priority", val)} - values={(filterValue ?? []) as TFilters["priority"]} - /> - )} - - {filterKey === "state" && ( - handleRemoveFilter("state", val)} - values={filterValue ?? []} - /> - )} - - -
-
- ); - })} - -
- ); -}); diff --git a/apps/space/components/issues/filters/applied-filters/label.tsx b/apps/space/components/issues/filters/applied-filters/label.tsx deleted file mode 100644 index 23785b5f1..000000000 --- a/apps/space/components/issues/filters/applied-filters/label.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { CloseOutline } from "@makeplane/propel/icons"; -// types -import type { IIssueLabel } from "@/types/issue"; - -type Props = { - handleRemove: (val: string) => void; - labels: IIssueLabel[] | undefined; - values: string[]; -}; - -export function AppliedLabelsFilters(props: Props) { - const { handleRemove, labels, values } = props; - - return ( - <> - {values.map((labelId) => { - const labelDetails = labels?.find((l) => l.id === labelId); - - if (!labelDetails) return null; - - return ( -
- - {labelDetails.name} - -
- ); - })} - - ); -} diff --git a/apps/space/components/issues/filters/applied-filters/priority.tsx b/apps/space/components/issues/filters/applied-filters/priority.tsx deleted file mode 100644 index 1b3a03a85..000000000 --- a/apps/space/components/issues/filters/applied-filters/priority.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { PriorityIcon } from "@plane/propel/icons"; -import { CloseOutline } from "@makeplane/propel/icons"; -import type { TIssuePriorities } from "@plane/propel/icons"; - -type Props = { - handleRemove: (val: string) => void; - values: TIssuePriorities[]; -}; - -export function AppliedPriorityFilters(props: Props) { - const { handleRemove, values } = props; - - return ( - <> - {values?.map((priority) => ( -
- - {priority} - -
- ))} - - ); -} diff --git a/apps/space/components/issues/filters/applied-filters/root.tsx b/apps/space/components/issues/filters/applied-filters/root.tsx deleted file mode 100644 index 58ae48100..000000000 --- a/apps/space/components/issues/filters/applied-filters/root.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback } from "react"; -import { cloneDeep } from "lodash-es"; -import { observer } from "mobx-react"; -import { useRouter } from "next/navigation"; -// hooks -import { useIssueFilter } from "@/hooks/store/use-issue-filter"; -// store -import type { TIssueLayout, TIssueQueryFilters } from "@/types/issue"; -// components -import { AppliedFiltersList } from "./filters-list"; - -type TIssueAppliedFilters = { - anchor: string; -}; - -export const IssueAppliedFilters = observer(function IssueAppliedFilters(props: TIssueAppliedFilters) { - const { anchor } = props; - // router - const router = useRouter(); - // store hooks - const { getIssueFilters, initIssueFilters, updateIssueFilters } = useIssueFilter(); - // derived values - const issueFilters = getIssueFilters(anchor); - const activeLayout = issueFilters?.display_filters?.layout || undefined; - const userFilters = issueFilters?.filters || {}; - - const appliedFilters: any = {}; - Object.entries(userFilters).forEach(([key, value]) => { - if (!value) return; - if (Array.isArray(value) && value.length === 0) return; - appliedFilters[key] = value; - }); - - const updateRouteParams = useCallback( - (key: keyof TIssueQueryFilters, value: string[]) => { - const state = key === "state" ? value : (issueFilters?.filters?.state ?? []); - const priority = key === "priority" ? value : (issueFilters?.filters?.priority ?? []); - const labels = key === "labels" ? value : (issueFilters?.filters?.labels ?? []); - - const params: { - board: TIssueLayout | string; - priority?: string; - states?: string; - labels?: string; - } = { - board: activeLayout || "list", - }; - - if (priority.length > 0) params.priority = priority.join(","); - if (state.length > 0) params.states = state.join(","); - if (labels.length > 0) params.labels = labels.join(","); - - const qs = new URLSearchParams(params).toString(); - router.push(`/issues/${anchor}?${qs}`); - }, - [activeLayout, anchor, issueFilters, router] - ); - - const handleFilters = useCallback( - (key: keyof TIssueQueryFilters, value: string | null) => { - let newValues = cloneDeep(issueFilters?.filters?.[key]) ?? []; - - if (value === null) newValues = []; - else if (newValues.includes(value)) newValues.splice(newValues.indexOf(value), 1); - - updateIssueFilters(anchor, "filters", key, newValues); - updateRouteParams(key, newValues); - }, - [anchor, issueFilters, updateIssueFilters, updateRouteParams] - ); - - const handleRemoveAllFilters = () => { - initIssueFilters( - anchor, - { - display_filters: { layout: activeLayout || "list" }, - filters: { - state: [], - priority: [], - labels: [], - }, - }, - true - ); - - router.push(`/issues/${anchor}?${`board=${activeLayout || "list"}`}`); - }; - - if (Object.keys(appliedFilters).length === 0) return null; - - return ( -
- -
- ); -}); diff --git a/apps/space/components/issues/filters/applied-filters/state.tsx b/apps/space/components/issues/filters/applied-filters/state.tsx deleted file mode 100644 index 1a37861f2..000000000 --- a/apps/space/components/issues/filters/applied-filters/state.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { EIconSize } from "@plane/constants"; -import { StateGroupIcon } from "@plane/propel/icons"; -import { CloseOutline } from "@makeplane/propel/icons"; -// hooks -import { useStates } from "@/hooks/store/use-state"; - -type Props = { - handleRemove: (val: string) => void; - values: string[]; -}; - -export const AppliedStateFilters = observer(function AppliedStateFilters(props: Props) { - const { handleRemove, values } = props; - - const { sortedStates: states } = useStates(); - - return ( - <> - {values.map((stateId) => { - const stateDetails = states?.find((s) => s.id === stateId); - - if (!stateDetails) return null; - - return ( -
- - {stateDetails.name} - -
- ); - })} - - ); -}); diff --git a/apps/space/components/issues/filters/helpers/dropdown.tsx b/apps/space/components/issues/filters/helpers/dropdown.tsx deleted file mode 100644 index 17f7ca66b..000000000 --- a/apps/space/components/issues/filters/helpers/dropdown.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { Fragment, useState } from "react"; -import type { Placement } from "@popperjs/core"; -import { usePopper } from "react-popper"; -import { Popover, Transition } from "@headlessui/react"; -// ui -import { Button } from "@plane/propel/button"; - -type Props = { - children: React.ReactNode; - title?: string; - placement?: Placement; -}; - -export function FiltersDropdown(props: Props) { - const { children, title = "Dropdown", placement } = props; - - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "auto", - }); - - return ( - - {({ open }) => { - if (open) { - } - return ( - <> - - - - - -
-
{children}
-
-
-
- - ); - }} -
- ); -} diff --git a/apps/space/components/issues/filters/helpers/filter-header.tsx b/apps/space/components/issues/filters/helpers/filter-header.tsx deleted file mode 100644 index bd6c677fd..000000000 --- a/apps/space/components/issues/filters/helpers/filter-header.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -// icons -import { ChevronDownOutline, ChevronUpOutline } from "@makeplane/propel/icons"; - -interface IFilterHeader { - title: string; - isPreviewEnabled: boolean; - handleIsPreviewEnabled: () => void; -} - -export function FilterHeader({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) { - return ( -
-
{title}
- -
- ); -} diff --git a/apps/space/components/issues/filters/helpers/filter-option.tsx b/apps/space/components/issues/filters/helpers/filter-option.tsx deleted file mode 100644 index 58d0be53c..000000000 --- a/apps/space/components/issues/filters/helpers/filter-option.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -// plane imports -import { TickOutline } from "@makeplane/propel/icons"; - -type Props = { - icon?: React.ReactNode; - isChecked: boolean; - title: React.ReactNode; - onClick?: () => void; - multiple?: boolean; -}; - -export function FilterOption(props: Props) { - const { icon, isChecked, multiple = true, onClick, title } = props; - - return ( - - ); -} diff --git a/apps/space/components/issues/filters/index.ts b/apps/space/components/issues/filters/index.ts deleted file mode 100644 index d98033459..000000000 --- a/apps/space/components/issues/filters/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./root"; diff --git a/apps/space/components/issues/filters/labels.tsx b/apps/space/components/issues/filters/labels.tsx deleted file mode 100644 index db235c5b7..000000000 --- a/apps/space/components/issues/filters/labels.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useState } from "react"; -// plane imports -import { Loader } from "@plane/ui"; -// types -import type { IIssueLabel } from "@/types/issue"; -// local imports -import { FilterHeader } from "./helpers/filter-header"; -import { FilterOption } from "./helpers/filter-option"; - -function LabelIcons({ color }: { color: string }) { - return ; -} - -type Props = { - appliedFilters: string[] | null; - handleUpdate: (val: string) => void; - labels: IIssueLabel[] | undefined; - searchQuery: string; -}; - -export function FilterLabels(props: Props) { - const { appliedFilters, handleUpdate, labels, searchQuery } = props; - - const [itemsToRender, setItemsToRender] = useState(5); - const [previewEnabled, setPreviewEnabled] = useState(true); - - const appliedFiltersCount = appliedFilters?.length ?? 0; - - const filteredOptions = labels?.filter((label) => label.name.toLowerCase().includes(searchQuery.toLowerCase())); - - const handleViewToggle = () => { - if (!filteredOptions) return; - - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); - }; - - return ( - <> - 0 ? ` (${appliedFiltersCount})` : ""}`} - isPreviewEnabled={previewEnabled} - handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} - /> - {previewEnabled && ( -
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - <> - {filteredOptions.slice(0, itemsToRender).map((label) => ( - handleUpdate(label?.id)} - icon={} - title={label.name} - /> - ))} - {filteredOptions.length > 5 && ( - - )} - - ) : ( -

No matches found

- ) - ) : ( - - - - - - )} -
- )} - - ); -} diff --git a/apps/space/components/issues/filters/priority.tsx b/apps/space/components/issues/filters/priority.tsx deleted file mode 100644 index 06e4a532a..000000000 --- a/apps/space/components/issues/filters/priority.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useState } from "react"; -import { observer } from "mobx-react"; -// plane imports -import { ISSUE_PRIORITY_FILTERS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { PriorityIcon } from "@plane/propel/icons"; -// local imports -import { FilterHeader } from "./helpers/filter-header"; -import { FilterOption } from "./helpers/filter-option"; - -type Props = { - appliedFilters: string[] | null; - handleUpdate: (val: string) => void; - searchQuery: string; -}; - -export const FilterPriority = observer(function FilterPriority(props: Props) { - const { appliedFilters, handleUpdate, searchQuery } = props; - - // hooks - const { t } = useTranslation(); - - const [previewEnabled, setPreviewEnabled] = useState(true); - - const appliedFiltersCount = appliedFilters?.length ?? 0; - - const filteredOptions = ISSUE_PRIORITY_FILTERS.filter((p) => p.key.includes(searchQuery.toLowerCase())); - - return ( - <> - 0 ? ` (${appliedFiltersCount})` : ""}`} - isPreviewEnabled={previewEnabled} - handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} - /> - {previewEnabled && ( -
- {filteredOptions.length > 0 ? ( - filteredOptions.map((priority) => ( - handleUpdate(priority.key)} - icon={} - title={t(priority.titleTranslationKey)} - /> - )) - ) : ( -

{t("common.search.no_matches_found")}

- )} -
- )} - - ); -}); diff --git a/apps/space/components/issues/filters/root.tsx b/apps/space/components/issues/filters/root.tsx deleted file mode 100644 index 983f00ad4..000000000 --- a/apps/space/components/issues/filters/root.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback } from "react"; -import { cloneDeep } from "lodash-es"; -import { observer } from "mobx-react"; -import { useRouter } from "next/navigation"; -// constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@plane/constants"; -// components -import { FiltersDropdown } from "@/components/issues/filters/helpers/dropdown"; -import { FilterSelection } from "@/components/issues/filters/selection"; -// helpers -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { useIssueFilter } from "@/hooks/store/use-issue-filter"; -// types -import type { TIssueQueryFilters } from "@/types/issue"; - -type IssueFiltersDropdownProps = { - anchor: string; -}; - -export const IssueFiltersDropdown = observer(function IssueFiltersDropdown(props: IssueFiltersDropdownProps) { - const { anchor } = props; - // router - const router = useRouter(); - // hooks - const { getIssueFilters, updateIssueFilters } = useIssueFilter(); - // derived values - const issueFilters = getIssueFilters(anchor); - const activeLayout = issueFilters?.display_filters?.layout || undefined; - - const updateRouteParams = useCallback( - (key: keyof TIssueQueryFilters, value: string[]) => { - const state = key === "state" ? value : (issueFilters?.filters?.state ?? []); - const priority = key === "priority" ? value : (issueFilters?.filters?.priority ?? []); - const labels = key === "labels" ? value : (issueFilters?.filters?.labels ?? []); - - const { queryParam } = queryParamGenerator({ board: activeLayout, priority, state, labels }); - router.push(`/issues/${anchor}?${queryParam}`); - }, - [anchor, activeLayout, issueFilters, router] - ); - - const handleFilters = useCallback( - (key: keyof TIssueQueryFilters, value: string) => { - if (!value) return; - - const newValues = cloneDeep(issueFilters?.filters?.[key]) ?? []; - - if (newValues.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); - - updateIssueFilters(anchor, "filters", key, newValues); - updateRouteParams(key, newValues); - }, - [anchor, issueFilters, updateIssueFilters, updateRouteParams] - ); - - return ( -
- - - -
- ); -}); diff --git a/apps/space/components/issues/filters/selection.tsx b/apps/space/components/issues/filters/selection.tsx deleted file mode 100644 index 6781c5243..000000000 --- a/apps/space/components/issues/filters/selection.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useState } from "react"; -import { observer } from "mobx-react"; -import { CloseOutline, SearchOutline } from "@makeplane/propel/icons"; -// types -import type { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue"; -// local imports -import { FilterPriority } from "./priority"; -import { FilterState } from "./state"; - -type Props = { - filters: IIssueFilterOptions; - handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void; - layoutDisplayFiltersOptions: TIssueFilterKeys[]; -}; - -export const FilterSelection = observer(function FilterSelection(props: Props) { - const { filters, handleFilters, layoutDisplayFiltersOptions } = props; - - const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); - - const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions.includes(filter); - - return ( -
-
-
- - setFiltersSearchQuery(e.target.value)} - autoFocus - /> - {filtersSearchQuery !== "" && ( - - )} -
-
-
- {/* priority */} - {isFilterEnabled("priority") && ( -
- handleFilters("priority", val)} - searchQuery={filtersSearchQuery} - /> -
- )} - - {/* state */} - {isFilterEnabled("state") && ( -
- handleFilters("state", val)} - searchQuery={filtersSearchQuery} - /> -
- )} - - {/* labels */} - {/* {isFilterEnabled("labels") && ( -
- handleFilters("labels", val)} - labels={labels} - searchQuery={filtersSearchQuery} - /> -
- )} */} -
-
- ); -}); diff --git a/apps/space/components/issues/filters/state.tsx b/apps/space/components/issues/filters/state.tsx deleted file mode 100644 index f9275a2d2..000000000 --- a/apps/space/components/issues/filters/state.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useState } from "react"; -import { observer } from "mobx-react"; -// ui -import { EIconSize } from "@plane/constants"; -import { StateGroupIcon } from "@plane/propel/icons"; -import { Loader } from "@plane/ui"; -// hooks -import { useStates } from "@/hooks/store/use-state"; -// local imports -import { FilterHeader } from "./helpers/filter-header"; -import { FilterOption } from "./helpers/filter-option"; - -type Props = { - appliedFilters: string[] | null; - handleUpdate: (val: string) => void; - searchQuery: string; -}; - -export const FilterState = observer(function FilterState(props: Props) { - const { appliedFilters, handleUpdate, searchQuery } = props; - - const { sortedStates: states } = useStates(); - - const [itemsToRender, setItemsToRender] = useState(5); - const [previewEnabled, setPreviewEnabled] = useState(true); - - const appliedFiltersCount = appliedFilters?.length ?? 0; - - const filteredOptions = states?.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase())); - - const handleViewToggle = () => { - if (!filteredOptions) return; - - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); - }; - - return ( - <> - 0 ? ` (${appliedFiltersCount})` : ""}`} - isPreviewEnabled={previewEnabled} - handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} - /> - {previewEnabled && ( -
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - <> - {filteredOptions.slice(0, itemsToRender).map((state) => ( - handleUpdate(state.id)} - icon={} - title={state.name} - /> - ))} - {filteredOptions.length > 5 && ( - - )} - - ) : ( -

No matches found

- ) - ) : ( - - - - - - )} -
- )} - - ); -}); diff --git a/apps/space/components/issues/issue-layouts/error.tsx b/apps/space/components/issues/issue-layouts/error.tsx deleted file mode 100644 index 40e4f5540..000000000 --- a/apps/space/components/issues/issue-layouts/error.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// assets -import SomethingWentWrongImage from "@/app/assets/something-went-wrong.svg?url"; - -export function SomethingWentWrongError() { - return ( -
-
-
-
- Oops! Something went wrong -
-
-

Oops! Something went wrong.

-

The public board does not exist. Please check the URL.

-
-
- ); -} diff --git a/apps/space/components/issues/issue-layouts/index.ts b/apps/space/components/issues/issue-layouts/index.ts deleted file mode 100644 index d98033459..000000000 --- a/apps/space/components/issues/issue-layouts/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./root"; diff --git a/apps/space/components/issues/issue-layouts/issue-layout-HOC.tsx b/apps/space/components/issues/issue-layouts/issue-layout-HOC.tsx deleted file mode 100644 index 6a9a719bc..000000000 --- a/apps/space/components/issues/issue-layouts/issue-layout-HOC.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import type { TLoader } from "@plane/types"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; - -interface Props { - children: string | React.ReactNode | React.ReactNode[]; - getGroupIssueCount: ( - groupId: string | undefined, - subGroupId: string | undefined, - isSubGroupCumulative: boolean - ) => number | undefined; - getIssueLoader: (groupId?: string, subGroupId?: string) => TLoader; -} - -export const IssueLayoutHOC = observer(function IssueLayoutHOC(props: Props) { - const { getIssueLoader, getGroupIssueCount } = props; - - const issueCount = getGroupIssueCount(undefined, undefined, false); - - if (getIssueLoader() === "init-loader" || issueCount === undefined) { - return ( -
- -
- ); - } - - if (getGroupIssueCount(undefined, undefined, false) === 0) { - return
No work items found
; - } - - return <>{props.children}; -}); diff --git a/apps/space/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/apps/space/components/issues/issue-layouts/kanban/base-kanban-root.tsx deleted file mode 100644 index c38a577d5..000000000 --- a/apps/space/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useMemo, useRef } from "react"; -import { debounce } from "lodash-es"; -import { observer } from "mobx-react"; -// types -import type { IIssueDisplayProperties } from "@plane/types"; -// components -import { IssueLayoutHOC } from "@/components/issues/issue-layouts/issue-layout-HOC"; -// hooks -import { useIssue } from "@/hooks/store/use-issue"; - -import { KanBan } from "./default"; - -type Props = { - anchor: string; -}; -export const IssueKanbanLayoutRoot = observer(function IssueKanbanLayoutRoot(props: Props) { - const { anchor } = props; - // store hooks - const { groupedIssueIds, getIssueLoader, fetchNextPublicIssues, getGroupIssueCount, getPaginationData } = useIssue(); - - const displayProperties: IIssueDisplayProperties = useMemo( - () => ({ - key: true, - state: true, - labels: true, - priority: true, - due_date: true, - }), - [] - ); - - const fetchMoreIssues = useCallback( - (groupId?: string, subgroupId?: string) => { - if (getIssueLoader(groupId, subgroupId) !== "pagination") { - fetchNextPublicIssues(anchor, groupId, subgroupId); - } - }, - [anchor, getIssueLoader, fetchNextPublicIssues] - ); - - const debouncedFetchMoreIssues = debounce( - (groupId?: string, subgroupId?: string) => fetchMoreIssues(groupId, subgroupId), - 300, - { leading: true, trailing: false } - ); - - const scrollableContainerRef = useRef(null); - - return ( - -
-
-
- -
-
-
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/kanban/block-reactions.tsx b/apps/space/components/issues/issue-layouts/kanban/block-reactions.tsx deleted file mode 100644 index 78e16a43c..000000000 --- a/apps/space/components/issues/issue-layouts/kanban/block-reactions.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane utils -import { cn } from "@plane/utils"; -// components -import { IssueEmojiReactions } from "@/components/issues/reactions/issue-emoji-reactions"; -import { IssueVotes } from "@/components/issues/reactions/issue-vote-reactions"; -// hooks -import { usePublish } from "@/hooks/store/publish"; - -type Props = { - issueId: string; -}; -export const BlockReactions = observer(function BlockReactions(props: Props) { - const { issueId } = props; - const { anchor } = useParams(); - const { canVote, canReact } = usePublish(anchor.toString()); - - // if the user cannot vote or react then return empty - if (!canVote && !canReact) return <>; - - return ( -
-
- {canVote && ( -
- -
- )} - {canReact && ( -
- -
- )} -
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/kanban/block.tsx b/apps/space/components/issues/issue-layouts/kanban/block.tsx deleted file mode 100644 index 3ad4c7191..000000000 --- a/apps/space/components/issues/issue-layouts/kanban/block.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { MutableRefObject } from "react"; -import { observer } from "mobx-react"; -import { Link } from "react-router"; -import { useParams, useSearchParams } from "next/navigation"; -// plane types -import { Tooltip } from "@plane/propel/tooltip"; -import type { IIssueDisplayProperties } from "@plane/types"; -// plane ui -// plane utils -import { cn } from "@plane/utils"; -// components -import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with-display-properties-HOC"; -// helpers -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { usePublish } from "@/hooks/store/publish"; -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -// -import type { IIssue } from "@/types/issue"; -import { IssueProperties } from "../properties/all-properties"; -import { getIssueBlockId } from "../utils"; -import { BlockReactions } from "./block-reactions"; - -interface IssueBlockProps { - issueId: string; - groupId: string; - subGroupId: string; - displayProperties: IIssueDisplayProperties | undefined; - scrollableContainerRef?: MutableRefObject; -} - -interface IssueDetailsBlockProps { - issue: IIssue; - displayProperties: IIssueDisplayProperties | undefined; -} - -const KanbanIssueDetailsBlock = observer(function KanbanIssueDetailsBlock(props: IssueDetailsBlockProps) { - const { issue, displayProperties } = props; - const { anchor } = useParams(); - // hooks - const { project_details } = usePublish(anchor.toString()); - - return ( -
- -
-
- {project_details?.identifier}-{issue.sequence_id} -
-
-
- -
- - {issue.name} - -
- - -
- ); -}); - -export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueBlockProps) { - const { issueId, groupId, subGroupId, displayProperties } = props; - const searchParams = useSearchParams(); - // query params - const board = searchParams.get("board"); - // hooks - const { setPeekId, getIsIssuePeeked, getIssueById } = useIssueDetails(); - - const handleIssuePeekOverview = () => { - setPeekId(issueId); - }; - - const { queryParam } = queryParamGenerator(board ? { board, peekId: issueId } : { peekId: issueId }); - - const issue = getIssueById(issueId); - - if (!issue) return null; - - return ( -
-
- - - - -
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/kanban/blocks-list.tsx b/apps/space/components/issues/issue-layouts/kanban/blocks-list.tsx deleted file mode 100644 index 319bdf741..000000000 --- a/apps/space/components/issues/issue-layouts/kanban/blocks-list.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { MutableRefObject } from "react"; -import { observer } from "mobx-react"; -//types -import type { IIssueDisplayProperties } from "@plane/types"; -// components -import { KanbanIssueBlock } from "./block"; - -interface IssueBlocksListProps { - subGroupId: string; - groupId: string; - issueIds: string[]; - displayProperties: IIssueDisplayProperties | undefined; - scrollableContainerRef?: MutableRefObject; -} - -export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(props: IssueBlocksListProps) { - const { subGroupId, groupId, issueIds, displayProperties, scrollableContainerRef } = props; - - return ( - <> - {issueIds && issueIds.length > 0 - ? issueIds.map((issueId) => { - if (!issueId) return null; - - let draggableId = issueId; - if (groupId) draggableId = `${draggableId}__${groupId}`; - if (subGroupId) draggableId = `${draggableId}__${subGroupId}`; - - return ( - - ); - }) - : null} - - ); -}); diff --git a/apps/space/components/issues/issue-layouts/kanban/default.tsx b/apps/space/components/issues/issue-layouts/kanban/default.tsx deleted file mode 100644 index fa934aeb9..000000000 --- a/apps/space/components/issues/issue-layouts/kanban/default.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { MutableRefObject } from "react"; -import { isNil } from "lodash-es"; -import { observer } from "mobx-react"; -// types -import type { - GroupByColumnTypes, - IGroupByColumn, - TGroupedIssues, - IIssueDisplayProperties, - TSubGroupedIssues, - TIssueGroupByOptions, - TPaginationData, - TLoader, -} from "@plane/types"; -// hooks -import { useCycle } from "@/hooks/store/use-cycle"; -import { useLabel } from "@/hooks/store/use-label"; -import { useMember } from "@/hooks/store/use-member"; -import { useModule } from "@/hooks/store/use-module"; -import { useStates } from "@/hooks/store/use-state"; -// -import { getGroupByColumns } from "../utils"; -// components -import { HeaderGroupByCard } from "./headers/group-by-card"; -import { KanbanGroup } from "./kanban-group"; - -export interface IKanBan { - groupedIssueIds: TGroupedIssues | TSubGroupedIssues; - displayProperties: IIssueDisplayProperties | undefined; - subGroupBy: TIssueGroupByOptions | undefined; - groupBy: TIssueGroupByOptions | undefined; - subGroupId?: string; - loadMoreIssues: (groupId?: string, subGroupId?: string) => void; - getGroupIssueCount: ( - groupId: string | undefined, - subGroupId: string | undefined, - isSubGroupCumulative: boolean - ) => number | undefined; - getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; - getIssueLoader: (groupId?: string, subGroupId?: string) => TLoader; - scrollableContainerRef?: MutableRefObject; - showEmptyGroup?: boolean; -} - -export const KanBan = observer(function KanBan(props: IKanBan) { - const { - groupedIssueIds, - displayProperties, - subGroupBy, - groupBy, - subGroupId = "null", - loadMoreIssues, - getGroupIssueCount, - getPaginationData, - getIssueLoader, - scrollableContainerRef, - showEmptyGroup = true, - } = props; - - const member = useMember(); - const label = useLabel(); - const cycle = useCycle(); - const modules = useModule(); - const state = useStates(); - - const groupList = getGroupByColumns(groupBy as GroupByColumnTypes, cycle, modules, label, state, member); - - if (!groupList) return null; - - const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => { - const groupVisibility = { - showGroup: true, - showIssues: true, - }; - - if (!showEmptyGroup) { - groupVisibility.showGroup = (getGroupIssueCount(_list.id, undefined, false) ?? 0) > 0; - } - return groupVisibility; - }; - - return ( -
- {groupList?.map((subList) => { - const groupByVisibilityToggle = visibilityGroupBy(subList); - - if (groupByVisibilityToggle.showGroup === false) return <>; - return ( -
- {isNil(subGroupBy) && ( -
- -
- )} - - {groupByVisibilityToggle.showIssues && ( - - )} -
- ); - })} -
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/apps/space/components/issues/issue-layouts/kanban/headers/group-by-card.tsx deleted file mode 100644 index b66127ca4..000000000 --- a/apps/space/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { Circle } from "lucide-react"; -// types -import type { TIssueGroupByOptions } from "@plane/types"; - -interface IHeaderGroupByCard { - groupBy: TIssueGroupByOptions | undefined; - icon?: React.ReactNode; - title: string; - count: number; -} - -export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHeaderGroupByCard) { - const { icon, title, count } = props; - - return ( - <> -
-
- {icon ? icon : } -
-
-
{title}
-
{count || 0}
-
-
- - ); -}); diff --git a/apps/space/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx b/apps/space/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx deleted file mode 100644 index 904011682..000000000 --- a/apps/space/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { Circle } from "lucide-react"; -import { ChevronDownOutline, ChevronUpOutline } from "@makeplane/propel/icons"; -// mobx - -interface IHeaderSubGroupByCard { - icon?: React.ReactNode; - title: string; - count: number; - isExpanded: boolean; - toggleExpanded: () => void; -} - -export const HeaderSubGroupByCard = observer(function HeaderSubGroupByCard(props: IHeaderSubGroupByCard) { - const { icon, title, count, isExpanded, toggleExpanded } = props; - return ( -
toggleExpanded()} - > -
- {isExpanded ? : } -
- -
- {icon ? icon : } -
- -
-
{title}
-
{count || 0}
-
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/kanban/kanban-group.tsx b/apps/space/components/issues/issue-layouts/kanban/kanban-group.tsx deleted file mode 100644 index 360b5e901..000000000 --- a/apps/space/components/issues/issue-layouts/kanban/kanban-group.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { MutableRefObject } from "react"; -import { forwardRef, useCallback, useRef, useState } from "react"; -import { observer } from "mobx-react"; -//types -import type { - TGroupedIssues, - IIssueDisplayProperties, - TSubGroupedIssues, - TIssueGroupByOptions, - TPaginationData, - TLoader, -} from "@plane/types"; -import { cn } from "@plane/utils"; -// hooks -import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; -// local imports -import { KanbanIssueBlocksList } from "./blocks-list"; - -interface IKanbanGroup { - groupId: string; - groupedIssueIds: TGroupedIssues | TSubGroupedIssues; - displayProperties: IIssueDisplayProperties | undefined; - subGroupBy: TIssueGroupByOptions | undefined; - subGroupId: string; - loadMoreIssues: (groupId?: string, subGroupId?: string) => void; - getGroupIssueCount: ( - groupId: string | undefined, - subGroupId: string | undefined, - isSubGroupCumulative: boolean - ) => number | undefined; - getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; - getIssueLoader: (groupId?: string, subGroupId?: string) => TLoader; - scrollableContainerRef?: MutableRefObject; -} - -// Loader components -const KanbanIssueBlockLoader = forwardRef(function KanbanIssueBlockLoader( - props: Record, - ref: React.ForwardedRef -) { - return ( - - ); -}); -KanbanIssueBlockLoader.displayName = "KanbanIssueBlockLoader"; - -export const KanbanGroup = observer(function KanbanGroup(props: IKanbanGroup) { - const { - groupId, - subGroupId, - subGroupBy, - displayProperties, - groupedIssueIds, - loadMoreIssues, - getGroupIssueCount, - getPaginationData, - getIssueLoader, - scrollableContainerRef, - } = props; - - // hooks - const [intersectionElement, setIntersectionElement] = useState(null); - const columnRef = useRef(null); - - const containerRef = subGroupBy && scrollableContainerRef ? scrollableContainerRef : columnRef; - - const loadMoreIssuesInThisGroup = useCallback(() => { - loadMoreIssues(groupId, subGroupId === "null" ? undefined : subGroupId); - }, [loadMoreIssues, groupId, subGroupId]); - - const isPaginating = !!getIssueLoader(groupId, subGroupId); - - useIntersectionObserver( - containerRef, - isPaginating ? null : intersectionElement, - loadMoreIssuesInThisGroup, - `0% 100% 100% 100%` - ); - - const isSubGroup = !!subGroupId && subGroupId !== "null"; - - const issueIds = isSubGroup - ? ((groupedIssueIds as TSubGroupedIssues)?.[groupId]?.[subGroupId] ?? []) - : ((groupedIssueIds as TGroupedIssues)?.[groupId] ?? []); - - const groupIssueCount = getGroupIssueCount(groupId, subGroupId, false) ?? 0; - const nextPageResults = getPaginationData(groupId, subGroupId)?.nextPageResults; - - const loadMore = isPaginating ? ( - - ) : ( -
- {" "} - Load More ↓ -
- ); - - const shouldLoadMore = nextPageResults === undefined ? issueIds?.length < groupIssueCount : !!nextPageResults; - - return ( -
- - - {shouldLoadMore && - (isSubGroup ? ( - <>{loadMore} - ) : ( -
- {Array.from({ length: 2 }).map((_, index) => ( - - ))} - -
- ))} -
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/kanban/swimlanes.tsx b/apps/space/components/issues/issue-layouts/kanban/swimlanes.tsx deleted file mode 100644 index 977bf5bb0..000000000 --- a/apps/space/components/issues/issue-layouts/kanban/swimlanes.tsx +++ /dev/null @@ -1,310 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { MutableRefObject } from "react"; -import { useState } from "react"; -import { observer } from "mobx-react"; -// types -import type { - GroupByColumnTypes, - IGroupByColumn, - TGroupedIssues, - IIssueDisplayProperties, - TSubGroupedIssues, - TIssueGroupByOptions, - TIssueOrderByOptions, - TPaginationData, - TLoader, -} from "@plane/types"; -// hooks -import { useCycle } from "@/hooks/store/use-cycle"; -import { useLabel } from "@/hooks/store/use-label"; -import { useMember } from "@/hooks/store/use-member"; -import { useModule } from "@/hooks/store/use-module"; -import { useStates } from "@/hooks/store/use-state"; -// -import { getGroupByColumns } from "../utils"; -import { KanBan } from "./default"; -import { HeaderGroupByCard } from "./headers/group-by-card"; -import { HeaderSubGroupByCard } from "./headers/sub-group-by-card"; - -export interface IKanBanSwimLanes { - groupedIssueIds: TGroupedIssues | TSubGroupedIssues; - displayProperties: IIssueDisplayProperties | undefined; - subGroupBy: TIssueGroupByOptions | undefined; - groupBy: TIssueGroupByOptions | undefined; - loadMoreIssues: (groupId?: string, subGroupId?: string) => void; - getGroupIssueCount: ( - groupId: string | undefined, - subGroupId: string | undefined, - isSubGroupCumulative: boolean - ) => number | undefined; - getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; - getIssueLoader: (groupId?: string, subGroupId?: string) => TLoader; - showEmptyGroup: boolean; - scrollableContainerRef?: MutableRefObject; - orderBy: TIssueOrderByOptions | undefined; -} - -export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanSwimLanes) { - const { - groupedIssueIds, - displayProperties, - subGroupBy, - groupBy, - orderBy, - loadMoreIssues, - getGroupIssueCount, - getPaginationData, - getIssueLoader, - showEmptyGroup, - scrollableContainerRef, - } = props; - - const member = useMember(); - const label = useLabel(); - const cycle = useCycle(); - const modules = useModule(); - const state = useStates(); - - const groupByList = getGroupByColumns(groupBy as GroupByColumnTypes, cycle, modules, label, state, member); - const subGroupByList = getGroupByColumns(subGroupBy as GroupByColumnTypes, cycle, modules, label, state, member); - - if (!groupByList || !subGroupByList) return null; - - return ( -
-
- -
- - {subGroupBy && ( - - )} -
- ); -}); - -interface ISubGroupSwimlaneHeader { - subGroupBy: TIssueGroupByOptions | undefined; - groupBy: TIssueGroupByOptions | undefined; - groupList: IGroupByColumn[]; - showEmptyGroup: boolean; - getGroupIssueCount: ( - groupId: string | undefined, - subGroupId: string | undefined, - isSubGroupCumulative: boolean - ) => number | undefined; -} - -const visibilitySubGroupByGroupCount = (subGroupIssueCount: number, showEmptyGroup: boolean): boolean => { - let subGroupHeaderVisibility = true; - - if (showEmptyGroup) subGroupHeaderVisibility = true; - else { - if (subGroupIssueCount > 0) subGroupHeaderVisibility = true; - else subGroupHeaderVisibility = false; - } - - return subGroupHeaderVisibility; -}; - -const SubGroupSwimlaneHeader = observer(function SubGroupSwimlaneHeader({ - subGroupBy, - groupBy, - groupList, - showEmptyGroup, - getGroupIssueCount, -}: ISubGroupSwimlaneHeader) { - return ( -
- {groupList && - groupList.length > 0 && - groupList.map((group: IGroupByColumn) => { - const groupCount = getGroupIssueCount(group.id, undefined, false) ?? 0; - - const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup); - - if (subGroupByVisibilityToggle === false) return <>; - return ( -
- -
- ); - })} -
- ); -}); - -interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { - groupedIssueIds: TGroupedIssues | TSubGroupedIssues; - showEmptyGroup: boolean; - displayProperties: IIssueDisplayProperties | undefined; - orderBy: TIssueOrderByOptions | undefined; - getGroupIssueCount: ( - groupId: string | undefined, - subGroupId: string | undefined, - isSubGroupCumulative: boolean - ) => number | undefined; - getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; - getIssueLoader: (groupId?: string, subGroupId?: string) => TLoader; - scrollableContainerRef?: MutableRefObject; - loadMoreIssues: (groupId?: string, subGroupId?: string) => void; -} - -const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwimlane) { - const { - groupedIssueIds, - subGroupBy, - groupBy, - groupList, - displayProperties, - loadMoreIssues, - getGroupIssueCount, - getPaginationData, - getIssueLoader, - showEmptyGroup, - scrollableContainerRef, - } = props; - - return ( -
- {groupList && - groupList.length > 0 && - groupList.map((group: IGroupByColumn) => ( - - ))} -
- ); -}); - -interface ISubGroup { - groupedIssueIds: TGroupedIssues | TSubGroupedIssues; - showEmptyGroup: boolean; - displayProperties: IIssueDisplayProperties | undefined; - groupBy: TIssueGroupByOptions | undefined; - subGroupBy: TIssueGroupByOptions | undefined; - group: IGroupByColumn; - getGroupIssueCount: ( - groupId: string | undefined, - subGroupId: string | undefined, - isSubGroupCumulative: boolean - ) => number | undefined; - getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; - getIssueLoader: (groupId?: string, subGroupId?: string) => TLoader; - scrollableContainerRef?: MutableRefObject; - loadMoreIssues: (groupId?: string, subGroupId?: string) => void; -} - -const SubGroup = observer(function SubGroup(props: ISubGroup) { - const { - groupedIssueIds, - subGroupBy, - groupBy, - group, - displayProperties, - loadMoreIssues, - getGroupIssueCount, - getPaginationData, - getIssueLoader, - showEmptyGroup, - scrollableContainerRef, - } = props; - - const [isExpanded, setIsExpanded] = useState(true); - - const toggleExpanded = () => { - setIsExpanded((prevState) => !prevState); - }; - - const visibilitySubGroupBy = ( - _list: IGroupByColumn, - subGroupCount: number - ): { showGroup: boolean; showIssues: boolean } => { - const subGroupVisibility = { - showGroup: true, - showIssues: true, - }; - if (showEmptyGroup) subGroupVisibility.showGroup = true; - else { - if (subGroupCount > 0) subGroupVisibility.showGroup = true; - else subGroupVisibility.showGroup = false; - } - return subGroupVisibility; - }; - - const issueCount = getGroupIssueCount(undefined, group.id, true) ?? 0; - const subGroupByVisibilityToggle = visibilitySubGroupBy(group, issueCount); - if (subGroupByVisibilityToggle.showGroup === false) return <>; - - return ( - <> -
-
-
- -
-
- - {subGroupByVisibilityToggle.showIssues && isExpanded && ( -
- -
- )} -
- - ); -}); diff --git a/apps/space/components/issues/issue-layouts/list/base-list-root.tsx b/apps/space/components/issues/issue-layouts/list/base-list-root.tsx deleted file mode 100644 index fbc1347ae..000000000 --- a/apps/space/components/issues/issue-layouts/list/base-list-root.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useMemo } from "react"; -import { observer } from "mobx-react"; -// types -import type { IIssueDisplayProperties, TGroupedIssues } from "@plane/types"; -// constants -// components -import { IssueLayoutHOC } from "@/components/issues/issue-layouts/issue-layout-HOC"; -// hooks -import { useIssue } from "@/hooks/store/use-issue"; -import { List } from "./default"; - -type Props = { - anchor: string; -}; - -export const IssuesListLayoutRoot = observer(function IssuesListLayoutRoot(props: Props) { - const { anchor } = props; - // store hooks - const { - groupedIssueIds: storeGroupedIssueIds, - fetchNextPublicIssues, - getGroupIssueCount, - getPaginationData, - getIssueLoader, - } = useIssue(); - - const groupedIssueIds = storeGroupedIssueIds as TGroupedIssues | undefined; - // auth - const displayProperties: IIssueDisplayProperties = useMemo( - () => ({ - key: true, - state: true, - labels: true, - priority: true, - due_date: true, - }), - [] - ); - - const loadMoreIssues = useCallback( - (groupId?: string) => { - fetchNextPublicIssues(anchor, groupId); - }, - [anchor, fetchNextPublicIssues] - ); - - return ( - -
- -
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/list/block.tsx b/apps/space/components/issues/issue-layouts/list/block.tsx deleted file mode 100644 index 24fc5f27e..000000000 --- a/apps/space/components/issues/issue-layouts/list/block.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useRef } from "react"; -import { observer } from "mobx-react"; -import { Link } from "react-router"; -import { useParams, useSearchParams } from "next/navigation"; -// plane types -import { Tooltip } from "@plane/propel/tooltip"; -import type { IIssueDisplayProperties } from "@plane/types"; -// plane ui -// plane utils -import { cn } from "@plane/utils"; -// helpers -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { usePublish } from "@/hooks/store/publish"; -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -// -import { IssueProperties } from "../properties/all-properties"; - -interface IssueBlockProps { - issueId: string; - groupId: string; - displayProperties: IIssueDisplayProperties | undefined; -} - -export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) { - const { anchor } = useParams(); - const { issueId, displayProperties } = props; - const searchParams = useSearchParams(); - // query params - const board = searchParams.get("board"); - // ref - const issueRef = useRef(null); - // hooks - const { project_details } = usePublish(anchor.toString()); - const { getIsIssuePeeked, setPeekId, getIssueById } = useIssueDetails(); - - const handleIssuePeekOverview = () => { - setPeekId(issueId); - }; - - const { queryParam } = queryParamGenerator(board ? { board, peekId: issueId } : { peekId: issueId }); - - const issue = getIssueById(issueId); - - if (!issue) return null; - - const projectIdentifier = project_details?.identifier; - - return ( -
-
-
-
- {displayProperties && displayProperties?.key && ( -
- {projectIdentifier}-{issue.sequence_id} -
- )} -
- - - -

{issue.name}

-
- -
-
-
- -
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/list/blocks-list.tsx b/apps/space/components/issues/issue-layouts/list/blocks-list.tsx deleted file mode 100644 index 2580afbfd..000000000 --- a/apps/space/components/issues/issue-layouts/list/blocks-list.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { MutableRefObject } from "react"; -// types -import type { IIssueDisplayProperties } from "@plane/types"; -import { IssueBlock } from "./block"; - -interface Props { - issueIds: string[] | undefined; - groupId: string; - displayProperties?: IIssueDisplayProperties; - containerRef: MutableRefObject; -} - -export function IssueBlocksList(props: Props) { - const { issueIds = [], groupId, displayProperties } = props; - - return ( -
- {issueIds?.map((issueId) => ( - - ))} -
- ); -} diff --git a/apps/space/components/issues/issue-layouts/list/default.tsx b/apps/space/components/issues/issue-layouts/list/default.tsx deleted file mode 100644 index f8f545b47..000000000 --- a/apps/space/components/issues/issue-layouts/list/default.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useRef } from "react"; -import { observer } from "mobx-react"; -// types -import type { - GroupByColumnTypes, - TGroupedIssues, - IIssueDisplayProperties, - TIssueGroupByOptions, - TPaginationData, - TLoader, -} from "@plane/types"; -// hooks -import { useCycle } from "@/hooks/store/use-cycle"; -import { useLabel } from "@/hooks/store/use-label"; -import { useMember } from "@/hooks/store/use-member"; -import { useModule } from "@/hooks/store/use-module"; -import { useStates } from "@/hooks/store/use-state"; -// -import { getGroupByColumns } from "../utils"; -import { ListGroup } from "./list-group"; - -export interface IList { - groupedIssueIds: TGroupedIssues; - groupBy: TIssueGroupByOptions | undefined; - displayProperties: IIssueDisplayProperties | undefined; - showEmptyGroup?: boolean; - loadMoreIssues: (groupId?: string) => void; - getGroupIssueCount: ( - groupId: string | undefined, - subGroupId: string | undefined, - isSubGroupCumulative: boolean - ) => number | undefined; - getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; - getIssueLoader: (groupId?: string, subGroupId?: string) => TLoader; -} - -export const List = observer(function List(props: IList) { - const { - groupedIssueIds, - groupBy, - displayProperties, - showEmptyGroup, - loadMoreIssues, - getGroupIssueCount, - getPaginationData, - getIssueLoader, - } = props; - - const containerRef = useRef(null); - - const member = useMember(); - const label = useLabel(); - const cycle = useCycle(); - const modules = useModule(); - const state = useStates(); - - const groupList = getGroupByColumns(groupBy as GroupByColumnTypes, cycle, modules, label, state, member, true); - - if (!groupList) return null; - - return ( -
- {groupList && ( - <> -
- {groupList.map((group) => ( - - ))} -
- - )} -
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/list/headers/group-by-card.tsx b/apps/space/components/issues/issue-layouts/list/headers/group-by-card.tsx deleted file mode 100644 index 5f37484fa..000000000 --- a/apps/space/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { CircleDashed } from "lucide-react"; - -interface IHeaderGroupByCard { - groupID: string; - icon?: React.ReactNode; - title: string; - count: number; - toggleListGroup: (id: string) => void; -} - -export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHeaderGroupByCard) { - const { groupID, icon, title, count, toggleListGroup } = props; - - return ( - <> -
toggleListGroup(groupID)} - role="button" - > -
- {icon ?? } -
- -
-
{title}
-
{count || 0}
-
-
- - ); -}); diff --git a/apps/space/components/issues/issue-layouts/list/list-group.tsx b/apps/space/components/issues/issue-layouts/list/list-group.tsx deleted file mode 100644 index 26bc49935..000000000 --- a/apps/space/components/issues/issue-layouts/list/list-group.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { MutableRefObject } from "react"; -import { Fragment, forwardRef, useRef, useState } from "react"; -import { observer } from "mobx-react"; -import { useTranslation } from "@plane/i18n"; -// plane types -import type { - IGroupByColumn, - TIssueGroupByOptions, - IIssueDisplayProperties, - TPaginationData, - TLoader, -} from "@plane/types"; -// plane utils -import { cn } from "@plane/utils"; -// hooks -import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; -// -import { IssueBlocksList } from "./blocks-list"; -import { HeaderGroupByCard } from "./headers/group-by-card"; - -interface Props { - groupIssueIds: string[] | undefined; - group: IGroupByColumn; - groupBy: TIssueGroupByOptions | undefined; - displayProperties: IIssueDisplayProperties | undefined; - containerRef: MutableRefObject; - showEmptyGroup?: boolean; - loadMoreIssues: (groupId?: string) => void; - getGroupIssueCount: ( - groupId: string | undefined, - subGroupId: string | undefined, - isSubGroupCumulative: boolean - ) => number | undefined; - getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; - getIssueLoader: (groupId?: string, subGroupId?: string) => TLoader; -} - -// List loader component -const ListLoaderItemRow = forwardRef(function ListLoaderItemRow( - props: Record, - ref: React.ForwardedRef -) { - return ( -
-
- - -
-
- {[...Array(6)].map((_, index) => ( - - - - ))} -
-
- ); -}); -ListLoaderItemRow.displayName = "ListLoaderItemRow"; - -export const ListGroup = observer(function ListGroup(props: Props) { - const { - groupIssueIds = [], - group, - groupBy, - displayProperties, - containerRef, - showEmptyGroup, - loadMoreIssues, - getGroupIssueCount, - getPaginationData, - getIssueLoader, - } = props; - const [isExpanded, setIsExpanded] = useState(true); - const groupRef = useRef(null); - // hooks - const { t } = useTranslation(); - - const [intersectionElement, setIntersectionElement] = useState(null); - - const groupIssueCount = getGroupIssueCount(group.id, undefined, false) ?? 0; - const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults; - const isPaginating = !!getIssueLoader(group.id); - - useIntersectionObserver(containerRef, isPaginating ? null : intersectionElement, loadMoreIssues, `100% 0% 100% 0%`); - - const shouldLoadMore = - nextPageResults === undefined && groupIssueCount !== undefined && groupIssueIds - ? groupIssueIds.length < groupIssueCount - : !!nextPageResults; - - const loadMore = isPaginating ? ( - - ) : ( -
loadMoreIssues(group.id)} - role="button" - > - {t("common.load_more")} ↓ -
- ); - - const validateEmptyIssueGroups = (issueCount: number = 0) => { - if (!showEmptyGroup && issueCount <= 0) return false; - return true; - }; - - const toggleListGroup = () => { - setIsExpanded((prevState) => !prevState); - }; - - const shouldExpand = (!!groupIssueCount && isExpanded) || !groupBy; - - return validateEmptyIssueGroups(groupIssueCount) ? ( -
-
- -
- {shouldExpand && ( -
- {groupIssueIds && ( - - )} - - {shouldLoadMore && (groupBy ? <>{loadMore} : )} -
- )} -
- ) : null; -}); diff --git a/apps/space/components/issues/issue-layouts/properties/all-properties.tsx b/apps/space/components/issues/issue-layouts/properties/all-properties.tsx deleted file mode 100644 index 728ccd4c8..000000000 --- a/apps/space/components/issues/issue-layouts/properties/all-properties.tsx +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { AttachOutline, LinkOutline, ViewsOutline } from "@makeplane/propel/icons"; -// plane imports -import { Tooltip } from "@plane/propel/tooltip"; -import type { IIssueDisplayProperties } from "@plane/types"; -import { cn } from "@plane/utils"; -// components -import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with-display-properties-HOC"; -// helpers -import { getDate } from "@/helpers/date-time.helper"; -//// hooks -import type { IIssue } from "@/types/issue"; -import { IssueBlockCycle } from "./cycle"; -import { IssueBlockDate } from "./due-date"; -import { IssueBlockLabels } from "./labels"; -import { IssueBlockMembers } from "./member"; -import { IssueBlockModules } from "./modules"; -import { IssueBlockPriority } from "./priority"; -import { IssueBlockState } from "./state"; - -export interface IIssueProperties { - issue: IIssue; - displayProperties: IIssueDisplayProperties | undefined; - className: string; -} - -export const IssueProperties = observer(function IssueProperties(props: IIssueProperties) { - const { issue, displayProperties, className } = props; - - if (!displayProperties || !issue.project_id) return null; - - const minDate = getDate(issue.start_date); - minDate?.setDate(minDate.getDate()); - - const maxDate = getDate(issue.target_date); - maxDate?.setDate(maxDate.getDate()); - - return ( -
- {/* basic properties */} - {/* state */} - {issue.state_id && ( - -
- -
-
- )} - - {/* priority */} - -
- -
-
- - {/* label */} - -
- -
-
- - {/* start date */} - {issue?.start_date && ( - -
- -
-
- )} - - {/* target/due date */} - {issue?.target_date && ( - -
- -
-
- )} - - {/* assignee */} - -
- -
-
- - {/* modules */} - {issue.module_ids && issue.module_ids.length > 0 && ( - -
- -
-
- )} - - {/* cycles */} - {issue.cycle_id && ( - -
- -
-
- )} - - {/* estimates */} - {/* {projectId && areEstimateEnabledByProjectId(projectId?.toString()) && ( - -
- -
-
- )} */} - - {/* extra render properties */} - {/* sub-issues */} - !!properties.sub_issue_count && !!issue.sub_issues_count} - > - -
- -
{issue.sub_issues_count}
-
-
-
- - {/* attachments */} - !!properties.attachment_count && !!issue.attachment_count} - > - -
- -
{issue.attachment_count}
-
-
-
- - {/* link */} - !!properties.link && !!issue.link_count} - > - -
- -
{issue.link_count}
-
-
-
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/properties/cycle.tsx b/apps/space/components/issues/issue-layouts/properties/cycle.tsx deleted file mode 100644 index 7f67aee17..000000000 --- a/apps/space/components/issues/issue-layouts/properties/cycle.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane ui -import { CyclesOutline } from "@makeplane/propel/icons"; -import { Tooltip } from "@plane/propel/tooltip"; -// plane utils -import { cn } from "@plane/utils"; -//hooks -import { useCycle } from "@/hooks/store/use-cycle"; - -type Props = { - cycleId: string | undefined; - shouldShowBorder?: boolean; -}; - -export const IssueBlockCycle = observer(function IssueBlockCycle({ cycleId, shouldShowBorder = true }: Props) { - const { getCycleById } = useCycle(); - - const cycle = getCycleById(cycleId); - - return ( - -
-
- -
{cycle?.name ?? "No Cycle"}
-
-
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/properties/due-date.tsx b/apps/space/components/issues/issue-layouts/properties/due-date.tsx deleted file mode 100644 index b3d291c30..000000000 --- a/apps/space/components/issues/issue-layouts/properties/due-date.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { DueDateOutline } from "@makeplane/propel/icons"; -import { Tooltip } from "@plane/propel/tooltip"; -import { cn } from "@plane/utils"; -// helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; -// hooks -import { useStates } from "@/hooks/store/use-state"; - -type Props = { - due_date: string | undefined; - stateId: string | undefined; - shouldHighLight?: boolean; - shouldShowBorder?: boolean; -}; - -export const IssueBlockDate = observer(function IssueBlockDate(props: Props) { - const { due_date, stateId, shouldHighLight = true, shouldShowBorder = true } = props; - const { getStateById } = useStates(); - - const state = getStateById(stateId); - - const formattedDate = renderFormattedDate(due_date); - - return ( - -
- - {formattedDate ? formattedDate : "No Date"} -
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/properties/labels.tsx b/apps/space/components/issues/issue-layouts/properties/labels.tsx deleted file mode 100644 index 909c71629..000000000 --- a/apps/space/components/issues/issue-layouts/properties/labels.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { LabelsOutline } from "@makeplane/propel/icons"; -// plane imports -import { Tooltip } from "@plane/propel/tooltip"; -// hooks -import { useLabel } from "@/hooks/store/use-label"; - -type Props = { - labelIds: string[]; - shouldShowLabel?: boolean; -}; - -export const IssueBlockLabels = observer(function IssueBlockLabels({ labelIds, shouldShowLabel = false }: Props) { - const { getLabelsByIds } = useLabel(); - - const labels = getLabelsByIds(labelIds); - - const labelsString = labels.length > 0 ? labels.map((label) => label.name).join(", ") : "No Labels"; - - if (labels.length <= 0) - return ( - -
- - {shouldShowLabel && No Labels} -
-
- ); - - return ( -
- {labels.length <= 2 ? ( - <> - {labels.map((label) => ( - -
-
- -
{label?.name}
-
-
-
- ))} - - ) : ( -
- -
- - {`${labels.length} Labels`} -
-
-
- )} -
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/properties/member.tsx b/apps/space/components/issues/issue-layouts/properties/member.tsx deleted file mode 100644 index c6c5cc356..000000000 --- a/apps/space/components/issues/issue-layouts/properties/member.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// icons -import type { LucideIcon } from "lucide-react"; -import { MembersOutline } from "@makeplane/propel/icons"; -// plane ui -import { Avatar, AvatarGroup } from "@plane/ui"; -// plane utils -import { cn } from "@plane/utils"; -// hooks -import { useMember } from "@/hooks/store/use-member"; -// -import type { TPublicMember } from "@/types/member"; - -type Props = { - memberIds: string[]; - shouldShowBorder?: boolean; -}; - -type AvatarProps = { - showTooltip: boolean; - members: TPublicMember[]; - icon?: LucideIcon; -}; - -export const ButtonAvatars = observer(function ButtonAvatars(props: AvatarProps) { - const { showTooltip, members, icon: Icon } = props; - - if (Array.isArray(members)) { - if (members.length > 1) { - return ( - - {members.map((member) => { - if (!member) return; - return ; - })} - - ); - } else if (members.length === 1) { - return ( - - ); - } - } - - return Icon ? ( - - ) : ( - - ); -}); - -export const IssueBlockMembers = observer(function IssueBlockMembers({ memberIds, shouldShowBorder = true }: Props) { - const { getMembersByIds } = useMember(); - - const members = getMembersByIds(memberIds); - - return ( -
-
-
- - {!shouldShowBorder && members.length <= 1 && ( - {members?.[0]?.member__display_name ?? "No Assignees"} - )} -
-
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/properties/modules.tsx b/apps/space/components/issues/issue-layouts/properties/modules.tsx deleted file mode 100644 index bd610a497..000000000 --- a/apps/space/components/issues/issue-layouts/properties/modules.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane ui -import { ModuleOutline } from "@makeplane/propel/icons"; -import { Tooltip } from "@plane/propel/tooltip"; -// plane utils -import { cn } from "@plane/utils"; -// hooks -import { useModule } from "@/hooks/store/use-module"; - -type Props = { - moduleIds: string[] | undefined; - shouldShowBorder?: boolean; -}; - -export const IssueBlockModules = observer(function IssueBlockModules({ moduleIds, shouldShowBorder = true }: Props) { - const { getModulesByIds } = useModule(); - - const modules = getModulesByIds(moduleIds ?? []); - - const modulesString = modules.map((module) => module.name).join(", "); - - return ( -
- - {modules.length <= 1 ? ( -
-
- -
{modules?.[0]?.name ?? "No Modules"}
-
-
- ) : ( -
-
-
{modules.length} Modules
-
-
- )} -
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/properties/priority.tsx b/apps/space/components/issues/issue-layouts/properties/priority.tsx deleted file mode 100644 index 9b4dc3d01..000000000 --- a/apps/space/components/issues/issue-layouts/properties/priority.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { SignalHigh } from "lucide-react"; -import { useTranslation } from "@plane/i18n"; -// types -import { PriorityIcon } from "@plane/propel/icons"; -import { Tooltip } from "@plane/propel/tooltip"; -import type { TIssuePriorities } from "@plane/types"; -// constants -import { cn, getIssuePriorityFilters } from "@plane/utils"; - -export function IssueBlockPriority({ - priority, - shouldShowName = false, -}: { - priority: TIssuePriorities | null; - shouldShowName?: boolean; -}) { - // hooks - const { t } = useTranslation(); - const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null; - - const priorityClasses = { - urgent: "bg-layer-2 text-priority-urgent border-priority-urgent px-1", - high: "bg-layer-2 text-priority-high border-priority-high", - medium: "bg-layer-2 text-priority-medium border-priority-medium", - low: "bg-layer-2 text-priority-low border-priority-low", - none: "bg-layer-2 text-priority-none border-priority-none", - }; - - if (priority_detail === null) return <>; - - return ( - - - - ); -} diff --git a/apps/space/components/issues/issue-layouts/properties/state.tsx b/apps/space/components/issues/issue-layouts/properties/state.tsx deleted file mode 100644 index de1596979..000000000 --- a/apps/space/components/issues/issue-layouts/properties/state.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane ui -import { StateGroupIcon } from "@plane/propel/icons"; -import { Tooltip } from "@plane/propel/tooltip"; -import type { TStateGroups } from "@plane/types"; -// plane utils -import { cn } from "@plane/utils"; -//hooks -import { useStates } from "@/hooks/store/use-state"; - -type Props = { - shouldShowBorder?: boolean; -} & ( - | { - stateDetails: { - name: string; - group: TStateGroups; - }; - } - | { - stateId: string; - } -); - -export const IssueBlockState = observer(function IssueBlockState(props: Props) { - const { shouldShowBorder = true } = props; - // store hooks - const { getStateById } = useStates(); - // derived values - const state = "stateId" in props ? getStateById(props.stateId) : props.stateDetails; - if (!state) return null; - - return ( - -
-
- -
{state.name}
-
-
-
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/root.tsx b/apps/space/components/issues/issue-layouts/root.tsx deleted file mode 100644 index 1b8f98524..000000000 --- a/apps/space/components/issues/issue-layouts/root.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import useSWR from "swr"; -// components -import { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root"; -import { IssuePeekOverview } from "@/components/issues/peek-overview"; -// hooks -import { useIssue } from "@/hooks/store/use-issue"; -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -import { useIssueFilter } from "@/hooks/store/use-issue-filter"; -// store -import type { PublishStore } from "@/store/publish/publish.store"; -// local imports -import { SomethingWentWrongError } from "./error"; -import { IssueKanbanLayoutRoot } from "./kanban/base-kanban-root"; -import { IssuesListLayoutRoot } from "./list/base-list-root"; - -type Props = { - peekId: string | undefined; - publishSettings: PublishStore; -}; - -export const IssuesLayoutsRoot = observer(function IssuesLayoutsRoot(props: Props) { - const { peekId, publishSettings } = props; - // store hooks - const { getIssueFilters } = useIssueFilter(); - const { fetchPublicIssues } = useIssue(); - const issueDetailStore = useIssueDetails(); - // derived values - const { anchor } = publishSettings; - const issueFilters = anchor ? getIssueFilters(anchor) : undefined; - // derived values - const activeLayout = issueFilters?.display_filters?.layout || undefined; - - const { error } = useSWR( - anchor ? `PUBLIC_ISSUES_${anchor}` : null, - anchor - ? () => fetchPublicIssues(anchor, "init-loader", { groupedBy: "state", canGroup: true, perPageCount: 50 }) - : null, - { revalidateIfStale: false, revalidateOnFocus: false } - ); - - useEffect(() => { - if (peekId) { - issueDetailStore.setPeekId(peekId.toString()); - } - }, [peekId, issueDetailStore]); - - if (!anchor) return null; - - if (error) return ; - - return ( -
- {peekId && } - {activeLayout && ( -
- {/* applied filters */} - - - {activeLayout === "list" && ( -
- -
- )} - {activeLayout === "kanban" && ( -
- -
- )} -
- )} -
- ); -}); diff --git a/apps/space/components/issues/issue-layouts/utils.tsx b/apps/space/components/issues/issue-layouts/utils.tsx deleted file mode 100644 index 116ceee7a..000000000 --- a/apps/space/components/issues/issue-layouts/utils.tsx +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { isNil } from "lodash-es"; -// types -import { EIconSize, ISSUE_PRIORITIES } from "@plane/constants"; -import { CycleGroupIcon, PriorityIcon, StateGroupIcon } from "@plane/propel/icons"; -import { CyclesOutline, ModuleOutline } from "@makeplane/propel/icons"; -import type { - GroupByColumnTypes, - IGroupByColumn, - TCycleGroups, - IIssueDisplayProperties, - TGroupedIssues, -} from "@plane/types"; -// ui -import { Avatar } from "@plane/ui"; -// components -// constants -// stores -import type { ICycleStore } from "@/store/cycle.store"; -import type { IIssueLabelStore } from "@/store/label.store"; -import type { IIssueMemberStore } from "@/store/members.store"; -import type { IIssueModuleStore } from "@/store/module.store"; -import type { IStateStore } from "@/store/state.store"; - -export const HIGHLIGHT_CLASS = "highlight"; -export const HIGHLIGHT_WITH_LINE = "highlight-with-line"; - -export const getGroupByColumns = ( - groupBy: GroupByColumnTypes | null, - cycle: ICycleStore, - module: IIssueModuleStore, - label: IIssueLabelStore, - projectState: IStateStore, - member: IIssueMemberStore, - includeNone?: boolean -): IGroupByColumn[] | undefined => { - switch (groupBy) { - case "cycle": - return getCycleColumns(cycle); - case "module": - return getModuleColumns(module); - case "state": - return getStateColumns(projectState); - case "priority": - return getPriorityColumns(); - case "labels": - return getLabelsColumns(label) as any; - case "assignees": - return getAssigneeColumns(member); - case "created_by": - return getCreatedByColumns(member) as any; - default: - if (includeNone) return [{ id: `All Issues`, name: `All work items`, payload: {}, icon: undefined }]; - } -}; - -const getCycleColumns = (cycleStore: ICycleStore): IGroupByColumn[] | undefined => { - const { cycles } = cycleStore; - - if (!cycles) return; - - const cycleGroups: IGroupByColumn[] = []; - - cycles.map((cycle) => { - if (cycle) { - const cycleStatus = cycle?.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft"; - cycleGroups.push({ - id: cycle.id, - name: cycle.name, - icon: , - payload: { cycle_id: cycle.id }, - }); - } - }); - cycleGroups.push({ - id: "None", - name: "None", - icon: , - payload: { cycle_id: null }, - }); - - return cycleGroups; -}; - -const getModuleColumns = (moduleStore: IIssueModuleStore): IGroupByColumn[] | undefined => { - const { modules } = moduleStore; - - if (!modules) return; - - const moduleGroups: IGroupByColumn[] = []; - - modules.map((moduleInfo) => { - if (moduleInfo) - moduleGroups.push({ - id: moduleInfo.id, - name: moduleInfo.name, - icon: , - payload: { module_ids: [moduleInfo.id] }, - }); - }) as any; - moduleGroups.push({ - id: "None", - name: "None", - icon: , - payload: { module_ids: [] }, - }); - - return moduleGroups as any; -}; - -const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => { - const { sortedStates } = projectState; - if (!sortedStates) return; - - return sortedStates.map((state) => ({ - id: state.id, - name: state.name, - icon: ( -
- -
- ), - payload: { state_id: state.id }, - })) as any; -}; - -const getPriorityColumns = () => { - const priorities = ISSUE_PRIORITIES; - - return priorities.map((priority) => ({ - id: priority.key, - name: priority.title, - icon: , - payload: { priority: priority.key }, - })); -}; - -const getLabelsColumns = (label: IIssueLabelStore) => { - const { labels: storeLabels } = label; - - if (!storeLabels) return; - - const labels = [...storeLabels, { id: "None", name: "None", color: "#666" }]; - - return labels.map((label) => ({ - id: label.id, - name: label.name, - icon: ( -
- ), - payload: label?.id === "None" ? {} : { label_ids: [label.id] }, - })); -}; - -const getAssigneeColumns = (member: IIssueMemberStore) => { - const { members } = member; - - if (!members) return; - - const assigneeColumns: any = members.map((member) => ({ - id: member.id, - name: member?.member__display_name || "", - icon: , - payload: { assignee_ids: [member.id] }, - })); - - assigneeColumns.push({ id: "None", name: "None", icon: , payload: {} }); - - return assigneeColumns; -}; - -const getCreatedByColumns = (member: IIssueMemberStore) => { - const { members } = member; - - if (!members) return; - - return members.map((member) => ({ - id: member.id, - name: member?.member__display_name || "", - icon: , - payload: {}, - })); -}; - -export const getDisplayPropertiesCount = ( - displayProperties: IIssueDisplayProperties, - ignoreFields?: (keyof IIssueDisplayProperties)[] -) => { - const propertyKeys = Object.keys(displayProperties) as (keyof IIssueDisplayProperties)[]; - - let count = 0; - - for (const propertyKey of propertyKeys) { - if (ignoreFields && ignoreFields.includes(propertyKey)) continue; - if (displayProperties[propertyKey]) count++; - } - - return count; -}; - -export const getIssueBlockId = (issueId: string | undefined, groupId: string | undefined, subGroupId?: string) => - `issue_${issueId}_${groupId}_${subGroupId}`; - -/** - * returns empty Array if groupId is None - * @param groupId - * @returns - */ -export const getGroupId = (groupId: string) => { - if (groupId === "None") return []; - return [groupId]; -}; - -/** - * method that removes Null or undefined Keys from object - * @param obj - * @returns - */ -export const removeNillKeys = (obj: T) => - Object.fromEntries(Object.entries(obj ?? {}).filter(([key, value]) => key && !isNil(value))); - -/** - * This Method returns if the grouped values are subGrouped - * @param groupedIssueIds - * @returns - */ -export const isSubGrouped = (groupedIssueIds: TGroupedIssues) => { - if (!groupedIssueIds || Array.isArray(groupedIssueIds)) { - return false; - } - - if (Array.isArray(groupedIssueIds[Object.keys(groupedIssueIds)[0]])) { - return false; - } - - return true; -}; diff --git a/apps/space/components/issues/issue-layouts/with-display-properties-HOC.tsx b/apps/space/components/issues/issue-layouts/with-display-properties-HOC.tsx deleted file mode 100644 index bd6a66579..000000000 --- a/apps/space/components/issues/issue-layouts/with-display-properties-HOC.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import type { IIssueDisplayProperties } from "@plane/types"; - -interface IWithDisplayPropertiesHOC { - displayProperties: IIssueDisplayProperties; - shouldRenderProperty?: (displayProperties: IIssueDisplayProperties) => boolean; - displayPropertyKey: keyof IIssueDisplayProperties | (keyof IIssueDisplayProperties)[]; - children: React.ReactNode; -} - -export const WithDisplayPropertiesHOC = observer(function WithDisplayPropertiesHOC({ - displayProperties, - shouldRenderProperty, - displayPropertyKey, - children, -}: IWithDisplayPropertiesHOC) { - let shouldDisplayPropertyFromFilters = false; - if (Array.isArray(displayPropertyKey)) - shouldDisplayPropertyFromFilters = displayPropertyKey.every((key) => !!displayProperties[key]); - else shouldDisplayPropertyFromFilters = !!displayProperties[displayPropertyKey]; - - const renderProperty = - shouldDisplayPropertyFromFilters && (shouldRenderProperty ? shouldRenderProperty(displayProperties) : true); - - if (!renderProperty) return null; - - return <>{children}; -}); diff --git a/apps/space/components/issues/navbar/controls.tsx b/apps/space/components/issues/navbar/controls.tsx deleted file mode 100644 index f5902dda0..000000000 --- a/apps/space/components/issues/navbar/controls.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import { useRouter, useSearchParams } from "next/navigation"; -// components -import { IssueFiltersDropdown } from "@/components/issues/filters"; -// helpers -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -import { useIssueFilter } from "@/hooks/store/use-issue-filter"; -import useIsInIframe from "@/hooks/use-is-in-iframe"; -// store -import type { PublishStore } from "@/store/publish/publish.store"; -// types -import type { TIssueLayout } from "@/types/issue"; -// local imports -import { IssuesLayoutSelection } from "./layout-selection"; -import { NavbarTheme } from "./theme"; -import { UserAvatar } from "./user-avatar"; - -export type NavbarControlsProps = { - publishSettings: PublishStore; -}; - -export const NavbarControls = observer(function NavbarControls(props: NavbarControlsProps) { - // props - const { publishSettings } = props; - // router - const router = useRouter(); - const searchParams = useSearchParams(); - // query params - const board = searchParams.get("board") || undefined; - const labels = searchParams.get("labels") || undefined; - const state = searchParams.get("state") || undefined; - const priority = searchParams.get("priority") || undefined; - const peekId = searchParams.get("peekId") || undefined; - // hooks - const { getIssueFilters, isIssueFiltersUpdated, initIssueFilters } = useIssueFilter(); - const { setPeekId } = useIssueDetails(); - // derived values - const { anchor, view_props, workspace_detail } = publishSettings; - const issueFilters = anchor ? getIssueFilters(anchor) : undefined; - const activeLayout = issueFilters?.display_filters?.layout || undefined; - - const isInIframe = useIsInIframe(); - - useEffect(() => { - if (anchor && workspace_detail) { - const viewsAcceptable: string[] = []; - let currentBoard: TIssueLayout | null = null; - - if (view_props?.list) viewsAcceptable.push("list"); - if (view_props?.kanban) viewsAcceptable.push("kanban"); - if (view_props?.calendar) viewsAcceptable.push("calendar"); - if (view_props?.gantt) viewsAcceptable.push("gantt"); - if (view_props?.spreadsheet) viewsAcceptable.push("spreadsheet"); - - if (board) { - if (viewsAcceptable.includes(board.toString())) currentBoard = board.toString() as TIssueLayout; - else { - if (viewsAcceptable && viewsAcceptable.length > 0) currentBoard = viewsAcceptable[0] as TIssueLayout; - } - } else { - if (viewsAcceptable && viewsAcceptable.length > 0) currentBoard = viewsAcceptable[0] as TIssueLayout; - } - - if (currentBoard) { - if (activeLayout === undefined || activeLayout !== currentBoard) { - const { query, queryParam } = queryParamGenerator({ board: currentBoard, peekId, priority, state, labels }); - const params: any = { - display_filters: { layout: (query?.board as string[])[0] }, - filters: { - priority: query?.priority ?? undefined, - state: query?.state ?? undefined, - labels: query?.labels ?? undefined, - }, - }; - - if (!isIssueFiltersUpdated(anchor, params)) { - initIssueFilters(anchor, params); - router.push(`/issues/${anchor}?${queryParam}`); - } - } - } - } - }, [ - anchor, - board, - labels, - state, - priority, - peekId, - activeLayout, - router, - initIssueFilters, - setPeekId, - isIssueFiltersUpdated, - view_props, - workspace_detail, - ]); - - if (!anchor) return null; - - return ( - <> - {/* issue views */} -
- -
- - {/* issue filters */} -
- -
- - {/* theming */} -
- -
- - {!isInIframe && } - - ); -}); diff --git a/apps/space/components/issues/navbar/index.ts b/apps/space/components/issues/navbar/index.ts deleted file mode 100644 index d98033459..000000000 --- a/apps/space/components/issues/navbar/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./root"; diff --git a/apps/space/components/issues/navbar/layout-icon.tsx b/apps/space/components/issues/navbar/layout-icon.tsx deleted file mode 100644 index d3951d4aa..000000000 --- a/apps/space/components/issues/navbar/layout-icon.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { TIssueLayout } from "@plane/constants"; -import { BoardOutline, ListOutline } from "@makeplane/propel/icons"; -import type { ISvgIcons } from "@plane/propel/icons"; - -export function IssueLayoutIcon({ - layout, - size, - ...props -}: { layout: TIssueLayout; size?: number } & Omit) { - const iconProps = { - ...props, - ...(size && { width: size, height: size }), - }; - - switch (layout) { - case "list": - return ; - case "kanban": - return ; - default: - return null; - } -} diff --git a/apps/space/components/issues/navbar/layout-selection.tsx b/apps/space/components/issues/navbar/layout-selection.tsx deleted file mode 100644 index 927fd7d13..000000000 --- a/apps/space/components/issues/navbar/layout-selection.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useRouter, useSearchParams } from "next/navigation"; -// ui -import { SITES_ISSUE_LAYOUTS } from "@plane/constants"; -// plane i18n -import { useTranslation } from "@plane/i18n"; -import { Tooltip } from "@plane/propel/tooltip"; -// helpers -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { useIssueFilter } from "@/hooks/store/use-issue-filter"; -// mobx -import type { TIssueLayout } from "@/types/issue"; -import { IssueLayoutIcon } from "./layout-icon"; - -type Props = { - anchor: string; -}; - -export const IssuesLayoutSelection = observer(function IssuesLayoutSelection(props: Props) { - const { anchor } = props; - // hooks - const { t } = useTranslation(); - // router - const router = useRouter(); - const searchParams = useSearchParams(); - // query params - const labels = searchParams.get("labels"); - const state = searchParams.get("state"); - const priority = searchParams.get("priority"); - const peekId = searchParams.get("peekId"); - // hooks - const { layoutOptions, getIssueFilters, updateIssueFilters } = useIssueFilter(); - // derived values - const issueFilters = getIssueFilters(anchor); - const activeLayout = issueFilters?.display_filters?.layout || undefined; - - const handleCurrentBoardView = (boardView: TIssueLayout) => { - updateIssueFilters(anchor, "display_filters", "layout", boardView); - const { queryParam } = queryParamGenerator({ board: boardView, peekId, priority, state, labels }); - router.push(`/issues/${anchor}?${queryParam}`); - }; - - return ( -
- {SITES_ISSUE_LAYOUTS.map((layout) => { - if (!layoutOptions[layout.key]) return; - - return ( - - - - ); - })} -
- ); -}); diff --git a/apps/space/components/issues/navbar/root.tsx b/apps/space/components/issues/navbar/root.tsx deleted file mode 100644 index 7a383f88b..000000000 --- a/apps/space/components/issues/navbar/root.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { ProjectsOutline } from "@makeplane/propel/icons"; -// components -import { ProjectLogo } from "@/components/common/project-logo"; -// store -import type { PublishStore } from "@/store/publish/publish.store"; -// local imports -import { NavbarControls } from "./controls"; - -type Props = { - publishSettings: PublishStore; -}; - -export const IssuesNavbarRoot = observer(function IssuesNavbarRoot(props: Props) { - const { publishSettings } = props; - // hooks - const { project_details } = publishSettings; - - return ( -
- {/* project detail */} -
- {project_details ? ( - - - - ) : ( - - - - )} -
- {project_details?.name || `...`} -
-
-
- -
-
- ); -}); diff --git a/apps/space/components/issues/navbar/theme.tsx b/apps/space/components/issues/navbar/theme.tsx deleted file mode 100644 index 3a362aee6..000000000 --- a/apps/space/components/issues/navbar/theme.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import { useTheme } from "next-themes"; -import { AppearanceOutline, MoonOutline } from "@makeplane/propel/icons"; - -export const NavbarTheme = observer(function NavbarTheme() { - // states - const [appTheme, setAppTheme] = useState("light"); - // theme - const { setTheme, theme } = useTheme(); - - const handleTheme = () => { - setTheme(theme === "light" ? "dark" : "light"); - }; - - useEffect(() => { - if (!theme) return; - setAppTheme(theme); - }, [theme]); - - return ( - - ); -}); diff --git a/apps/space/components/issues/navbar/user-avatar.tsx b/apps/space/components/issues/navbar/user-avatar.tsx deleted file mode 100644 index c3557fd6c..000000000 --- a/apps/space/components/issues/navbar/user-avatar.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Fragment, useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import { Link } from "react-router"; -import { usePathname, useSearchParams } from "next/navigation"; -import { usePopper } from "react-popper"; -import { LogOutOutline } from "@makeplane/propel/icons"; -import { Popover, Transition } from "@headlessui/react"; -// plane imports -import { API_BASE_URL } from "@plane/constants"; -import { Button } from "@plane/propel/button"; -import { AuthService } from "@plane/services"; -import { Avatar } from "@plane/ui"; -import { getFileURL } from "@plane/utils"; -// helpers -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { useUser } from "@/hooks/store/use-user"; - -const authService = new AuthService(); - -export const UserAvatar = observer(function UserAvatar() { - const pathName = usePathname(); - const searchParams = useSearchParams(); - // query params - const board = searchParams.get("board") || undefined; - const labels = searchParams.get("labels") || undefined; - const state = searchParams.get("state") || undefined; - const priority = searchParams.get("priority") || undefined; - const peekId = searchParams.get("peekId") || undefined; - // hooks - const { data: currentUser, signOut } = useUser(); - // states - const [csrfToken, setCsrfToken] = useState(undefined); - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - - useEffect(() => { - if (csrfToken === undefined) - authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); - }, [csrfToken]); - - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: "bottom-end", - modifiers: [ - { - name: "offset", - options: { - offset: [0, 40], - }, - }, - ], - }); - - // derived values - const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels }); - - return ( -
- {currentUser?.id ? ( -
- - - - - - -
- {csrfToken && ( -
- - - -
- )} -
-
-
-
-
- ) : ( -
- - - -
- )} -
- ); -}); diff --git a/apps/space/components/issues/peek-overview/comment/add-comment.tsx b/apps/space/components/issues/peek-overview/comment/add-comment.tsx deleted file mode 100644 index acd86cdd5..000000000 --- a/apps/space/components/issues/peek-overview/comment/add-comment.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useRef, useState } from "react"; -import { observer } from "mobx-react"; -import { useForm, Controller } from "react-hook-form"; -// plane imports -import type { EditorRefApi } from "@plane/editor"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { SitesFileService } from "@plane/services"; -import type { TIssuePublicComment } from "@plane/types"; -// editor components -import { LiteTextEditor } from "@/components/editor/lite-text-editor"; -// hooks -import { usePublish } from "@/hooks/store/publish"; -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -import { useUser } from "@/hooks/store/use-user"; -// services -const fileService = new SitesFileService(); - -const defaultValues: Partial = { - comment_html: "", -}; - -type Props = { - anchor: string; - disabled?: boolean; -}; - -export const AddComment = observer(function AddComment(props: Props) { - const { anchor } = props; - // states - const [uploadedAssetIds, setUploadAssetIds] = useState([]); - // refs - const editorRef = useRef(null); - // store hooks - const { peekId: issueId, addIssueComment, uploadCommentAsset } = useIssueDetails(); - const { data: currentUser } = useUser(); - const { workspace: workspaceID } = usePublish(anchor); - // form info - const { - handleSubmit, - control, - watch, - formState: { isSubmitting }, - reset, - } = useForm({ defaultValues }); - - const onSubmit = async (formData: TIssuePublicComment) => { - if (!anchor || !issueId || isSubmitting || !formData.comment_html) return; - - await addIssueComment(anchor, issueId, formData) - .then(async (res) => { - reset(defaultValues); - editorRef.current?.clearEditor(); - if (uploadedAssetIds.length > 0) { - await fileService.updateBulkAssetsUploadStatus(anchor, res.id, { - asset_ids: uploadedAssetIds, - }); - setUploadAssetIds([]); - } - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Comment could not be posted. Please try again.", - }) - ); - }; - - // TODO: on click if he user is not logged in redirect to login page - return ( -
-
- ( - { - if (currentUser) handleSubmit(onSubmit)(e); - }} - anchor={anchor} - workspaceId={workspaceID?.toString() ?? ""} - ref={editorRef} - id="peek-overview-add-comment" - initialValue={ - !value || value === "" || (typeof value === "object" && Object.keys(value).length === 0) - ? watch("comment_html") - : value - } - onChange={(comment_json, comment_html) => onChange(comment_html)} - isSubmitting={isSubmitting} - placeholder="Add comment..." - uploadFile={async (blockId, file) => { - const { asset_id } = await uploadCommentAsset(file, anchor); - setUploadAssetIds((prev) => [...prev, asset_id]); - return asset_id; - }} - displayConfig={{ - fontSize: "small-font", - }} - /> - )} - /> -
-
- ); -}); diff --git a/apps/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/apps/space/components/issues/peek-overview/comment/comment-detail-card.tsx deleted file mode 100644 index 7a1aa65da..000000000 --- a/apps/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useRef, useState } from "react"; -import { observer } from "mobx-react"; -import { Controller, useForm } from "react-hook-form"; -import { ChatOutline, CloseOutline, MoreVerticalOutline, TickOutline } from "@makeplane/propel/icons"; -import { Menu, Transition } from "@headlessui/react"; -// plane imports -import type { EditorRefApi } from "@plane/editor"; -import type { TIssuePublicComment } from "@plane/types"; -import { getFileURL } from "@plane/utils"; -// components -import { LiteTextEditor } from "@/components/editor/lite-text-editor"; -import { CommentReactions } from "@/components/issues/peek-overview/comment/comment-reactions"; -// helpers -import { timeAgo } from "@/helpers/date-time.helper"; -// hooks -import { usePublish } from "@/hooks/store/publish"; -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -import { useUser } from "@/hooks/store/use-user"; -import useIsInIframe from "@/hooks/use-is-in-iframe"; - -type Props = { - anchor: string; - comment: TIssuePublicComment; -}; - -export const CommentCard = observer(function CommentCard(props: Props) { - const { anchor, comment } = props; - // store hooks - const { peekId, deleteIssueComment, updateIssueComment, uploadCommentAsset } = useIssueDetails(); - const { data: currentUser } = useUser(); - const { workspace: workspaceID } = usePublish(anchor); - const isInIframe = useIsInIframe(); - - // states - const [isEditing, setIsEditing] = useState(false); - // refs - const editorRef = useRef(null); - const showEditorRef = useRef(null); - // form info - const { - control, - formState: { isSubmitting }, - handleSubmit, - } = useForm({ - defaultValues: { comment_html: comment.comment_html }, - }); - - const handleDelete = () => { - if (!anchor || !peekId) return; - deleteIssueComment(anchor, peekId, comment.id); - }; - - const handleCommentUpdate = async (formData: TIssuePublicComment) => { - if (!anchor || !peekId) return; - updateIssueComment(anchor, peekId, comment.id, formData); - setIsEditing(false); - editorRef.current?.setEditorValue(formData.comment_html); - showEditorRef.current?.setEditorValue(formData.comment_html); - }; - - return ( -
-
- {comment.actor_detail.avatar_url && comment.actor_detail.avatar_url !== "" ? ( - { - ) : ( -
- {comment.actor_detail.is_bot - ? comment?.actor_detail?.first_name?.charAt(0) - : comment?.actor_detail?.display_name?.charAt(0)} -
- )} - - - -
-
-
-
- {comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name} -
-

- <>commented {timeAgo(comment.created_at)} -

-
-
-
-
- ( - onChange(comment_html)} - isSubmitting={isSubmitting} - showSubmitButton={false} - uploadFile={async (blockId, file) => { - const { asset_id } = await uploadCommentAsset(file, anchor, comment.id); - return asset_id; - }} - displayConfig={{ - fontSize: "small-font", - }} - /> - )} - /> -
-
- - -
-
-
- - -
-
-
- {!isInIframe && currentUser?.id === comment?.actor_detail?.id && ( - - {}} - className="relative grid cursor-pointer place-items-center rounded-sm p-1 text-tertiary outline-none hover:bg-layer-transparent-hover" - > - - - - - - - {({ active }) => ( -
- -
- )} -
- - {({ active }) => ( -
- -
- )} -
-
-
-
- )} -
- ); -}); diff --git a/apps/space/components/issues/peek-overview/comment/comment-reactions.tsx b/apps/space/components/issues/peek-overview/comment/comment-reactions.tsx deleted file mode 100644 index be6a6fb48..000000000 --- a/apps/space/components/issues/peek-overview/comment/comment-reactions.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useMemo, useState } from "react"; -import { observer } from "mobx-react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -// plane imports -import { stringToEmoji } from "@plane/propel/emoji-icon-picker"; -import { EmojiReactionGroup, EmojiReactionPicker } from "@plane/propel/emoji-reaction"; -import type { EmojiReactionType } from "@plane/propel/emoji-reaction"; -// helpers -import { groupReactions } from "@/helpers/emoji.helper"; -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -import { useUser } from "@/hooks/store/use-user"; -import useIsInIframe from "@/hooks/use-is-in-iframe"; - -type Props = { - anchor: string; - commentId: string; -}; - -export const CommentReactions = observer(function CommentReactions(props: Props) { - const { anchor, commentId } = props; - // state - const [isPickerOpen, setIsPickerOpen] = useState(false); - const router = useRouter(); - const pathName = usePathname(); - const searchParams = useSearchParams(); - // query params - const board = searchParams.get("board") || undefined; - const state = searchParams.get("state") || undefined; - const priority = searchParams.get("priority") || undefined; - const labels = searchParams.get("labels") || undefined; - - // hooks - const { addCommentReaction, removeCommentReaction, details, peekId } = useIssueDetails(); - const { data: user } = useUser(); - const isInIframe = useIsInIframe(); - - const commentReactions = useMemo(() => { - if (!peekId) return []; - const peekDetails = details[peekId]; - if (!peekDetails) return []; - const comment = peekDetails.comments?.find((c) => c.id === commentId); - return comment?.comment_reactions ?? []; - }, [peekId, details, commentId]); - - const groupedReactions = useMemo(() => { - if (!peekId) return {}; - return groupReactions(commentReactions ?? [], "reaction"); - }, [peekId, commentReactions]); - - const userReactions = commentReactions?.filter((r) => r?.actor_detail?.id === user?.id); - - const handleAddReaction = (reactionHex: string) => { - if (!anchor || !peekId) return; - addCommentReaction(anchor, peekId, commentId, reactionHex); - }; - - const handleRemoveReaction = (reactionHex: string) => { - if (!anchor || !peekId) return; - removeCommentReaction(anchor, peekId, commentId, reactionHex); - }; - - const handleReactionClick = (reactionHex: string) => { - const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex); - - if (userReaction) handleRemoveReaction(reactionHex); - else handleAddReaction(reactionHex); - }; - - // derived values - const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels }); - - // Transform reactions data to Propel EmojiReactionType format - const propelReactions: EmojiReactionType[] = useMemo(() => { - const REACTIONS_LIMIT = 1000; - - return Object.keys(groupedReactions || {}) - .filter((reaction) => groupedReactions?.[reaction]?.length > 0) - .map((reaction) => { - const reactionList = groupedReactions?.[reaction] ?? []; - const userNames = reactionList - .map((r) => r?.actor_detail?.display_name) - .filter((name): name is string => !!name) - .slice(0, REACTIONS_LIMIT); - - return { - emoji: stringToEmoji(reaction), - count: reactionList.length, - reacted: commentReactions?.some((r) => r?.actor_detail?.id === user?.id && r.reaction === reaction) || false, - users: userNames, - }; - }); - }, [groupedReactions, commentReactions, user?.id]); - - const handleEmojiClick = (emoji: string) => { - if (isInIframe) return; - if (!user) { - router.push(`/?next_path=${pathName}?${queryParam}`); - return; - } - // Convert emoji back to decimal string format for the API - const emojiCodePoints = Array.from(emoji) - .map((char) => char.codePointAt(0)) - .filter((cp): cp is number => cp !== undefined); - const reactionString = emojiCodePoints.join("-"); - handleReactionClick(reactionString); - }; - - const handleEmojiSelect = (emoji: string) => { - if (!user) { - router.push(`/?next_path=${pathName}?${queryParam}`); - return; - } - // emoji is already in decimal string format from EmojiReactionPicker - handleReactionClick(emoji); - }; - - return ( -
- setIsPickerOpen(true)} - /> - } - placement="bottom-start" - /> -
- ); -}); diff --git a/apps/space/components/issues/peek-overview/full-screen-peek-view.tsx b/apps/space/components/issues/peek-overview/full-screen-peek-view.tsx deleted file mode 100644 index 4cf425ba8..000000000 --- a/apps/space/components/issues/peek-overview/full-screen-peek-view.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { Loader } from "@plane/ui"; -// types -import type { IIssue } from "@/types/issue"; -// local imports -import { PeekOverviewHeader } from "./header"; -import { PeekOverviewIssueActivity } from "./issue-activity"; -import { PeekOverviewIssueDetails } from "./issue-details"; -import { PeekOverviewIssueProperties } from "./issue-properties"; - -type Props = { - anchor: string; - handleClose: () => void; - issueDetails: IIssue | undefined; -}; - -export const FullScreenPeekView = observer(function FullScreenPeekView(props: Props) { - const { anchor, handleClose, issueDetails } = props; - - return ( -
-
-
- -
- {issueDetails ? ( -
- {/* issue title and description */} -
- -
- {/* divider */} -
- {/* issue activity/comments */} -
- -
-
- ) : ( - - -
- - - -
-
- )} -
-
- {/* issue properties */} -
- {issueDetails ? ( - - ) : ( - - - - - - - )} -
-
-
- ); -}); diff --git a/apps/space/components/issues/peek-overview/header.tsx b/apps/space/components/issues/peek-overview/header.tsx deleted file mode 100644 index 7543eccc9..000000000 --- a/apps/space/components/issues/peek-overview/header.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -import { observer } from "mobx-react"; -import { - ArrowNarrowRightOutline, - FullScreenPeekOutline, - LinkOutline, - ModalPeekOutline, - SidePeekOutline, -} from "@makeplane/propel/icons"; -import { Listbox, Transition } from "@headlessui/react"; -// ui -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -// helpers -import { copyTextToClipboard } from "@/helpers/string.helper"; -// hooks -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -import useClipboardWritePermission from "@/hooks/use-clipboard-write-permission"; -// types -import type { IIssue, IPeekMode } from "@/types/issue"; - -type Props = { - handleClose: () => void; - issueDetails: IIssue | undefined; -}; - -const PEEK_MODES: { - key: IPeekMode; - icon: any; - label: string; -}[] = [ - { key: "side", icon: SidePeekOutline, label: "Side Peek" }, - { - key: "modal", - icon: ModalPeekOutline, - label: "Modal", - }, - { - key: "full", - icon: FullScreenPeekOutline, - label: "Full Screen", - }, -]; - -export const PeekOverviewHeader = observer(function PeekOverviewHeader(props: Props) { - const { handleClose } = props; - - const { peekMode, setPeekMode } = useIssueDetails(); - const isClipboardWriteAllowed = useClipboardWritePermission(); - - const handleCopyLink = () => { - const urlToCopy = window.location.href; - - copyTextToClipboard(urlToCopy).then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Link copied!", - message: "Work item link copied to clipboard.", - }); - }); - }; - - const Icon = PEEK_MODES.find((m) => m.key === peekMode)?.icon ?? SidePeekOutline; - - return ( - <> -
-
- {peekMode === "side" && ( - - )} - setPeekMode(val)} - className="relative shrink-0 text-left" - > - - - - - - -
- {PEEK_MODES.map((mode) => ( - - `cursor-pointer truncate rounded-sm px-1 py-1.5 select-none ${ - active ? "bg-layer-transparent-hover" : "" - } ${selected ? "text-primary" : "text-secondary"}` - } - > -
- - {mode.label} -
-
- ))} -
-
-
-
-
- {isClipboardWriteAllowed && (peekMode === "side" || peekMode === "modal") && ( - - )} -
- - ); -}); diff --git a/apps/space/components/issues/peek-overview/index.ts b/apps/space/components/issues/peek-overview/index.ts deleted file mode 100644 index d0bf32463..000000000 --- a/apps/space/components/issues/peek-overview/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./layout"; diff --git a/apps/space/components/issues/peek-overview/issue-activity.tsx b/apps/space/components/issues/peek-overview/issue-activity.tsx deleted file mode 100644 index 3519dc5cc..000000000 --- a/apps/space/components/issues/peek-overview/issue-activity.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { Link } from "react-router"; -import { usePathname } from "next/navigation"; -import { LockOutline } from "@makeplane/propel/icons"; -// plane imports -import { Button } from "@plane/propel/button"; -// components -import { AddComment } from "@/components/issues/peek-overview/comment/add-comment"; -import { CommentCard } from "@/components/issues/peek-overview/comment/comment-detail-card"; -// hooks -import { usePublish } from "@/hooks/store/publish"; -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -import { useUser } from "@/hooks/store/use-user"; -import useIsInIframe from "@/hooks/use-is-in-iframe"; -// types -import type { IIssue } from "@/types/issue"; - -type Props = { - anchor: string; - issueDetails: IIssue; -}; - -export const PeekOverviewIssueActivity = observer(function PeekOverviewIssueActivity(props: Props) { - const { anchor } = props; - // router - const pathname = usePathname(); - // store hooks - const { details, peekId } = useIssueDetails(); - const { data: currentUser } = useUser(); - const { canComment } = usePublish(anchor); - // derived values - const comments = details[peekId || ""]?.comments || []; - const isInIframe = useIsInIframe(); - - return ( -
-

Comments

-
-
- {comments.map((comment) => ( - - ))} -
- {!isInIframe && - (currentUser ? ( - <> - {canComment && ( -
- -
- )} - - ) : ( -
-

- - Sign in to add your comment -

- - - -
- ))} -
-
- ); -}); diff --git a/apps/space/components/issues/peek-overview/issue-details.tsx b/apps/space/components/issues/peek-overview/issue-details.tsx deleted file mode 100644 index 51139b460..000000000 --- a/apps/space/components/issues/peek-overview/issue-details.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { RichTextEditor } from "@/components/editor/rich-text-editor"; -import { usePublish } from "@/hooks/store/publish"; -// types -import type { IIssue } from "@/types/issue"; -// local imports -import { IssueReactions } from "./issue-reaction"; - -type Props = { - anchor: string; - issueDetails: IIssue; -}; - -export const PeekOverviewIssueDetails = observer(function PeekOverviewIssueDetails(props: Props) { - const { anchor, issueDetails } = props; - // store hooks - const { project_details, workspace: workspaceID } = usePublish(anchor); - // derived values - const description = issueDetails.description_html; - - return ( -
-
- {project_details?.identifier}-{issueDetails?.sequence_id} -
-

{issueDetails.name}

- {description && description !== "" && description !== "

" && ( - - )} - -
- ); -}); diff --git a/apps/space/components/issues/peek-overview/issue-properties.tsx b/apps/space/components/issues/peek-overview/issue-properties.tsx deleted file mode 100644 index 2e5eb5c37..000000000 --- a/apps/space/components/issues/peek-overview/issue-properties.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { useTranslation } from "@plane/i18n"; -import { StateGroupIcon, PriorityIcon } from "@plane/propel/icons"; -import { DueDateOutline, LinkOutline, PriorityOutline, StateOutline } from "@makeplane/propel/icons"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { cn, getIssuePriorityFilters } from "@plane/utils"; -// helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; -import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper"; -// hooks -import { usePublish } from "@/hooks/store/publish"; -import { useStates } from "@/hooks/store/use-state"; -// types -import type { IIssue, IPeekMode } from "@/types/issue"; - -type Props = { - issueDetails: IIssue; - mode?: IPeekMode; -}; - -export const PeekOverviewIssueProperties = observer(function PeekOverviewIssueProperties({ - issueDetails, - mode, -}: Props) { - // hooks - const { t } = useTranslation(); - const { getStateById } = useStates(); - const state = getStateById(issueDetails?.state_id ?? undefined); - - const { anchor } = useParams(); - - const { project_details } = usePublish(anchor?.toString()); - - const priority = issueDetails.priority ? getIssuePriorityFilters(issueDetails.priority) : null; - - const handleCopyLink = () => { - const urlToCopy = window.location.href; - - copyTextToClipboard(urlToCopy).then(() => { - setToast({ - type: TOAST_TYPE.INFO, - title: "Link copied!", - message: "Work item link copied to clipboard", - }); - }); - }; - - return ( -
- {mode === "full" && ( -
-
- {project_details?.identifier}-{issueDetails.sequence_id} -
-
- -
-
- )} -
-
-
- - State -
-
- - {addSpaceIfCamelCase(state?.name ?? "")} -
-
- -
-
- - Priority -
-
-
- {priority && } - {t(priority?.titleTranslationKey || "common.none")} -
-
-
- -
-
- - Due date -
-
- {issueDetails.target_date ? ( -
- - {renderFormattedDate(issueDetails.target_date)} -
- ) : ( - Empty - )} -
-
-
-
- ); -}); diff --git a/apps/space/components/issues/peek-overview/issue-reaction.tsx b/apps/space/components/issues/peek-overview/issue-reaction.tsx deleted file mode 100644 index 2b847d476..000000000 --- a/apps/space/components/issues/peek-overview/issue-reaction.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// components -import { IssueEmojiReactions } from "@/components/issues/reactions/issue-emoji-reactions"; -import { IssueVotes } from "@/components/issues/reactions/issue-vote-reactions"; -// hooks -import { usePublish } from "@/hooks/store/publish"; -import useIsInIframe from "@/hooks/use-is-in-iframe"; - -type Props = { - anchor: string; -}; - -export const IssueReactions = observer(function IssueReactions(props: Props) { - const { anchor } = props; - // store hooks - const { canVote, canReact } = usePublish(anchor); - const isInIframe = useIsInIframe(); - - return ( -
- {canVote && ( -
- -
- )} - {!isInIframe && canReact && ( -
- -
- )} -
- ); -}); diff --git a/apps/space/components/issues/peek-overview/layout.tsx b/apps/space/components/issues/peek-overview/layout.tsx deleted file mode 100644 index ebde720fa..000000000 --- a/apps/space/components/issues/peek-overview/layout.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Fragment, useEffect } from "react"; -import { observer } from "mobx-react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Dialog, Transition } from "@headlessui/react"; -// hooks -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -// local imports -import { FullScreenPeekView } from "./full-screen-peek-view"; -import { SidePeekView } from "./side-peek-view"; - -type TIssuePeekOverview = { - anchor: string; - peekId: string; - handlePeekClose?: () => void; -}; - -export const IssuePeekOverview = observer(function IssuePeekOverview(props: TIssuePeekOverview) { - const { anchor, peekId, handlePeekClose } = props; - const router = useRouter(); - const searchParams = useSearchParams(); - // query params - const board = searchParams.get("board") || undefined; - const state = searchParams.get("state") || undefined; - const priority = searchParams.get("priority") || undefined; - const labels = searchParams.get("labels") || undefined; - // store - const { peekMode, setPeekId, getIssueById, fetchIssueDetails } = useIssueDetails(); - // derived values - const issueDetails = peekId ? getIssueById(peekId.toString()) : undefined; - // state - const isSidePeekOpen = !!peekId && peekMode === "side"; - const isModalPeekOpen = !!peekId && (peekMode === "modal" || peekMode === "full"); - - useEffect(() => { - if (anchor && peekId) { - fetchIssueDetails(anchor, peekId.toString()); - } - }, [anchor, fetchIssueDetails, peekId]); - - const handleClose = () => { - // if close logic is passed down, call that instead of the below logic - if (handlePeekClose) { - handlePeekClose(); - return; - } - - setPeekId(null); - let queryParams: any = { - board, - }; - if (priority && priority.length > 0) queryParams = { ...queryParams, priority: priority }; - if (state && state.length > 0) queryParams = { ...queryParams, state: state }; - if (labels && labels.length > 0) queryParams = { ...queryParams, labels: labels }; - queryParams = new URLSearchParams(queryParams).toString(); - router.push(`/issues/${anchor}?${queryParams}`); - }; - - return ( - <> - - - - - - - - - - - - -
- - - -
- {peekMode === "modal" && ( - - )} - {peekMode === "full" && ( - - )} -
-
-
-
-
- - ); -}); diff --git a/apps/space/components/issues/peek-overview/side-peek-view.tsx b/apps/space/components/issues/peek-overview/side-peek-view.tsx deleted file mode 100644 index aa981d8d9..000000000 --- a/apps/space/components/issues/peek-overview/side-peek-view.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { Loader } from "@plane/ui"; -// store hooks -import { usePublish } from "@/hooks/store/publish"; -// types -import type { IIssue } from "@/types/issue"; -// local imports -import { PeekOverviewHeader } from "./header"; -import { PeekOverviewIssueActivity } from "./issue-activity"; -import { PeekOverviewIssueDetails } from "./issue-details"; -import { PeekOverviewIssueProperties } from "./issue-properties"; - -type Props = { - anchor: string; - handleClose: () => void; - issueDetails: IIssue | undefined; -}; - -export const SidePeekView = observer(function SidePeekView(props: Props) { - const { anchor, handleClose, issueDetails } = props; - // store hooks - const { canComment } = usePublish(anchor); - - return ( -
-
- -
- {issueDetails ? ( -
- {/* issue title and description */} -
- -
- {/* issue properties */} -
- -
- {/* divider */} -
- {/* issue activity/comments */} - {canComment && ( -
- -
- )} -
- ) : ( - - -
- - - -
-
- )} -
- ); -}); diff --git a/apps/space/components/issues/reactions/issue-emoji-reactions.tsx b/apps/space/components/issues/reactions/issue-emoji-reactions.tsx deleted file mode 100644 index 7727ec90b..000000000 --- a/apps/space/components/issues/reactions/issue-emoji-reactions.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useMemo, useState } from "react"; -import { observer } from "mobx-react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -// lib -import { stringToEmoji } from "@plane/propel/emoji-icon-picker"; -import { EmojiReactionGroup, EmojiReactionPicker } from "@plane/propel/emoji-reaction"; -import type { EmojiReactionType } from "@plane/propel/emoji-reaction"; -// helpers -import { groupReactions } from "@/helpers/emoji.helper"; -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -import { useUser } from "@/hooks/store/use-user"; - -type IssueEmojiReactionsProps = { - anchor: string; - issueIdFromProps?: string; -}; - -export const IssueEmojiReactions = observer(function IssueEmojiReactions(props: IssueEmojiReactionsProps) { - const { anchor, issueIdFromProps } = props; - // state - const [isPickerOpen, setIsPickerOpen] = useState(false); - // router - const router = useRouter(); - const pathName = usePathname(); - const searchParams = useSearchParams(); - // query params - const peekId = searchParams.get("peekId") || undefined; - const board = searchParams.get("board") || undefined; - const state = searchParams.get("state") || undefined; - const priority = searchParams.get("priority") || undefined; - const labels = searchParams.get("labels") || undefined; - // store hooks - const issueDetailsStore = useIssueDetails(); - const { data: user } = useUser(); - - const issueId = issueIdFromProps ?? issueDetailsStore.peekId; - const reactions = issueDetailsStore.details[issueId ?? ""]?.reaction_items ?? []; - const groupedReactions = groupReactions(reactions, "reaction"); - - const userReactions = reactions.filter((r) => r.actor_details?.id === user?.id); - - const handleAddReaction = (reactionHex: string) => { - if (!issueId) return; - issueDetailsStore.addIssueReaction(anchor, issueId, reactionHex); - }; - - const handleRemoveReaction = (reactionHex: string) => { - if (!issueId) return; - issueDetailsStore.removeIssueReaction(anchor, issueId, reactionHex); - }; - - const handleReactionClick = (reactionHex: string) => { - const userReaction = userReactions?.find((r) => r.actor_details?.id === user?.id && r.reaction === reactionHex); - if (userReaction) handleRemoveReaction(reactionHex); - else handleAddReaction(reactionHex); - }; - - // derived values - const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels }); - - // Transform reactions data to Propel EmojiReactionType format - const propelReactions: EmojiReactionType[] = useMemo(() => { - const REACTIONS_LIMIT = 1000; - - return Object.keys(groupedReactions || {}) - .filter((reaction) => groupedReactions?.[reaction]?.length > 0) - .map((reaction) => { - const reactionList = groupedReactions?.[reaction] ?? []; - const userNames = reactionList - .map((r) => r?.actor_details?.display_name) - .filter((name): name is string => !!name) - .slice(0, REACTIONS_LIMIT); - - return { - emoji: stringToEmoji(reaction), - count: reactionList.length, - reacted: reactionList.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction), - users: userNames, - }; - }); - }, [groupedReactions, user?.id]); - - const handleEmojiClick = (emoji: string) => { - if (!user) { - router.push(`/?next_path=${pathName}?${queryParam}`); - return; - } - // Convert emoji back to decimal string format for the API - const emojiCodePoints = Array.from(emoji).map((char) => char.codePointAt(0)); - const reactionString = emojiCodePoints.join("-"); - handleReactionClick(reactionString); - }; - - const handleEmojiSelect = (emoji: string) => { - if (!user) { - router.push(`/?next_path=${pathName}?${queryParam}`); - return; - } - // emoji is already in decimal string format from EmojiReactionPicker - handleReactionClick(emoji); - }; - - return ( - setIsPickerOpen(true)} - /> - } - placement="bottom-start" - /> - ); -}); diff --git a/apps/space/components/issues/reactions/issue-vote-reactions.tsx b/apps/space/components/issues/reactions/issue-vote-reactions.tsx deleted file mode 100644 index 7dd41d391..000000000 --- a/apps/space/components/issues/reactions/issue-vote-reactions.tsx +++ /dev/null @@ -1,165 +0,0 @@ -/** - * 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 { ArrowDownOutline, TopArrowOutline } from "@makeplane/propel/icons"; -import { observer } from "mobx-react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -// plane imports -import { Tooltip } from "@plane/propel/tooltip"; -import { cn } from "@plane/utils"; -// helpers -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { useIssueDetails } from "@/hooks/store/use-issue-details"; -import { useUser } from "@/hooks/store/use-user"; -import useIsInIframe from "@/hooks/use-is-in-iframe"; - -type TIssueVotes = { - anchor: string; - issueIdFromProps?: string; - size?: "md" | "sm"; -}; - -export const IssueVotes = observer(function IssueVotes(props: TIssueVotes) { - const { anchor, issueIdFromProps, size = "md" } = props; - // states - const [isSubmitting, setIsSubmitting] = useState(false); - // router - const router = useRouter(); - const pathName = usePathname(); - const searchParams = useSearchParams(); - // query params - const peekId = searchParams.get("peekId") || undefined; - const board = searchParams.get("board") || undefined; - const state = searchParams.get("state") || undefined; - const priority = searchParams.get("priority") || undefined; - const labels = searchParams.get("labels") || undefined; - // store hooks - const issueDetailsStore = useIssueDetails(); - const { data: user } = useUser(); - - const isInIframe = useIsInIframe(); - - const issueId = issueIdFromProps ?? issueDetailsStore.peekId; - - const votes = issueDetailsStore.details[issueId ?? ""]?.vote_items ?? []; - - const allUpVotes = votes.filter((vote) => vote.vote === 1); - const allDownVotes = votes.filter((vote) => vote.vote === -1); - - const isUpVotedByUser = allUpVotes.some((vote) => vote.actor_details?.id === user?.id); - const isDownVotedByUser = allDownVotes.some((vote) => vote.actor_details?.id === user?.id); - - const handleVote = async (e: any, voteValue: 1 | -1) => { - if (!issueId) return; - - setIsSubmitting(true); - - const actionPerformed = votes?.find((vote) => vote.actor_details?.id === user?.id && vote.vote === voteValue); - - if (actionPerformed) await issueDetailsStore.removeIssueVote(anchor, issueId); - else { - await issueDetailsStore.addIssueVote(anchor, issueId, { - vote: voteValue, - }); - } - - setIsSubmitting(false); - }; - - const VOTES_LIMIT = 1000; - - // derived values - const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels }); - const votingDimensions = size === "sm" ? "px-1 h-6 min-w-9" : "px-2 h-7"; - - return ( -
- {/* upvote button 👇 */} - - {allUpVotes.length > 0 ? ( - <> - {allUpVotes - .map((r) => r.actor_details?.display_name) - .splice(0, VOTES_LIMIT) - .join(", ")} - {allUpVotes.length > VOTES_LIMIT && " and " + (allUpVotes.length - VOTES_LIMIT) + " more"} - - ) : ( - "No upvotes yet" - )} -
- } - > - - - - {/* downvote button 👇 */} - - {allDownVotes.length > 0 ? ( - <> - {allDownVotes - .map((r) => r.actor_details.display_name) - .splice(0, VOTES_LIMIT) - .join(", ")} - {allDownVotes.length > VOTES_LIMIT && " and " + (allDownVotes.length - VOTES_LIMIT) + " more"} - - ) : ( - "No downvotes yet" - )} -
- } - > - - -
- ); -}); diff --git a/apps/space/components/ui/not-found.tsx b/apps/space/components/ui/not-found.tsx deleted file mode 100644 index e3952f58c..000000000 --- a/apps/space/components/ui/not-found.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// images -import Image404 from "@/app/assets/404.svg?url"; - -export function PageNotFound() { - return ( -
-
-
-
- 404- Page not found -
-
-

Oops! Something went wrong.

-

- Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is - temporarily unavailable. -

-
-
-
-
- ); -} diff --git a/apps/space/components/views/auth.tsx b/apps/space/components/views/auth.tsx deleted file mode 100644 index 11591ea8c..000000000 --- a/apps/space/components/views/auth.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// components -import { AuthRoot } from "@/components/account/auth-forms"; -import { PoweredBy } from "@/components/common/powered-by"; -// local imports -import { AuthHeader } from "./header"; - -export function AuthView() { - return ( -
- - - -
- ); -} diff --git a/apps/space/components/views/header.tsx b/apps/space/components/views/header.tsx deleted file mode 100644 index dc2dabed0..000000000 --- a/apps/space/components/views/header.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -import { Link } from "react-router"; -import { PlaneLockup } from "@plane/propel/icons"; - -export function AuthHeader() { - return ( -
- - - -
- ); -} diff --git a/apps/space/components/views/index.ts b/apps/space/components/views/index.ts deleted file mode 100644 index 68908cdd4..000000000 --- a/apps/space/components/views/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./auth"; diff --git a/apps/space/helpers/authentication.helper.tsx b/apps/space/helpers/authentication.helper.tsx deleted file mode 100644 index 6e11d61bd..000000000 --- a/apps/space/helpers/authentication.helper.tsx +++ /dev/null @@ -1,405 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Link } from "react-router"; -// helpers -import { SUPPORT_EMAIL } from "@plane/constants"; - -export enum EPageTypes { - INIT = "INIT", - PUBLIC = "PUBLIC", - NON_AUTHENTICATED = "NON_AUTHENTICATED", - ONBOARDING = "ONBOARDING", - AUTHENTICATED = "AUTHENTICATED", -} - -export enum EErrorAlertType { - BANNER_ALERT = "BANNER_ALERT", - TOAST_ALERT = "TOAST_ALERT", - INLINE_FIRST_NAME = "INLINE_FIRST_NAME", - INLINE_EMAIL = "INLINE_EMAIL", - INLINE_PASSWORD = "INLINE_PASSWORD", - INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE", -} - -export enum EAuthenticationErrorCodes { - // Global - INSTANCE_NOT_CONFIGURED = "5000", - INVALID_EMAIL = "5005", - EMAIL_REQUIRED = "5010", - SIGNUP_DISABLED = "5015", - // Password strength - INVALID_PASSWORD = "5020", - SMTP_NOT_CONFIGURED = "5025", - // Sign Up - USER_ALREADY_EXIST = "5030", - AUTHENTICATION_FAILED_SIGN_UP = "5035", - REQUIRED_EMAIL_PASSWORD_SIGN_UP = "5040", - INVALID_EMAIL_SIGN_UP = "5045", - INVALID_EMAIL_MAGIC_SIGN_UP = "5050", - MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055", - // Sign In - BOT_USER_LOGIN_FORBIDDEN = "5017", - USER_ACCOUNT_DEACTIVATED = "5019", - USER_DOES_NOT_EXIST = "5060", - AUTHENTICATION_FAILED_SIGN_IN = "5065", - REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070", - INVALID_EMAIL_SIGN_IN = "5075", - INVALID_EMAIL_MAGIC_SIGN_IN = "5080", - MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED = "5085", - // Both Sign in and Sign up for magic - INVALID_MAGIC_CODE_SIGN_IN = "5090", - INVALID_MAGIC_CODE_SIGN_UP = "5092", - EXPIRED_MAGIC_CODE_SIGN_IN = "5095", - EXPIRED_MAGIC_CODE_SIGN_UP = "5097", - EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN = "5100", - EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP = "5102", - // Oauth - OAUTH_NOT_CONFIGURED = "5104", - GOOGLE_NOT_CONFIGURED = "5105", - GITHUB_NOT_CONFIGURED = "5110", - GITLAB_NOT_CONFIGURED = "5111", - GOOGLE_OAUTH_PROVIDER_ERROR = "5115", - GITHUB_OAUTH_PROVIDER_ERROR = "5120", - GITLAB_OAUTH_PROVIDER_ERROR = "5121", - // Reset Password - INVALID_PASSWORD_TOKEN = "5125", - EXPIRED_PASSWORD_TOKEN = "5130", - // Change password - INCORRECT_OLD_PASSWORD = "5135", - MISSING_PASSWORD = "5138", - INVALID_NEW_PASSWORD = "5140", - // set password - PASSWORD_ALREADY_SET = "5145", - // Admin - ADMIN_ALREADY_EXIST = "5150", - REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5155", - INVALID_ADMIN_EMAIL = "5160", - INVALID_ADMIN_PASSWORD = "5165", - REQUIRED_ADMIN_EMAIL_PASSWORD = "5170", - ADMIN_AUTHENTICATION_FAILED = "5175", - ADMIN_USER_ALREADY_EXIST = "5180", - ADMIN_USER_DOES_NOT_EXIST = "5185", -} - -export type TAuthErrorInfo = { - type: EErrorAlertType; - code: EAuthenticationErrorCodes; - title: string; - message: React.ReactNode; -}; - -const errorCodeMessages: { - [key in EAuthenticationErrorCodes]: { title: string; message: (email?: string) => React.ReactNode }; -} = { - // global - [EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED]: { - title: `Instance not configured`, - message: () => `Instance not configured. Please contact your administrator.`, - }, - [EAuthenticationErrorCodes.SIGNUP_DISABLED]: { - title: `Sign up disabled`, - message: () => `Sign up disabled. Please contact your administrator.`, - }, - [EAuthenticationErrorCodes.INVALID_PASSWORD]: { - title: `Invalid password`, - message: () => `Invalid password. Please try again.`, - }, - [EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED]: { - title: `SMTP not configured`, - message: () => `SMTP not configured. Please contact your administrator.`, - }, - - // email check in both sign up and sign in - [EAuthenticationErrorCodes.INVALID_EMAIL]: { - title: `Invalid email`, - message: () => `Invalid email. Please try again.`, - }, - [EAuthenticationErrorCodes.EMAIL_REQUIRED]: { - title: `Email required`, - message: () => `Email required. Please try again.`, - }, - - // sign up - [EAuthenticationErrorCodes.USER_ALREADY_EXIST]: { - title: `User already exists`, - message: (email = undefined) => ( -
- Your account is already registered.  - - Sign In - -  now. -
- ), - }, - [EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP]: { - title: `Email and password required`, - message: () => `Email and password required. Please try again.`, - }, - [EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP]: { - title: `Authentication failed`, - message: () => `Authentication failed. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP]: { - title: `Invalid email`, - message: () => `Invalid email. Please try again.`, - }, - [EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED]: { - title: `Email and code required`, - message: () => `Email and code required. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP]: { - title: `Invalid email`, - message: () => `Invalid email. Please try again.`, - }, - - // sign in - [EAuthenticationErrorCodes.BOT_USER_LOGIN_FORBIDDEN]: { - title: `Sign in not allowed`, - message: () => `This account cannot be used to sign in. Please use a personal account.`, - }, - [EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: { - title: `User account deactivated`, - message: () => `User account deactivated. Please contact ${SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`, - }, - - [EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: { - title: `User does not exist`, - message: (email = undefined) => ( -
- No account found.  - - Create one - -  to get started. -
- ), - }, - [EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN]: { - title: `Email and password required`, - message: () => `Email and password required. Please try again.`, - }, - [EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN]: { - title: `Authentication failed`, - message: () => `Authentication failed. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN]: { - title: `Invalid email`, - message: () => `Invalid email. Please try again.`, - }, - [EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED]: { - title: `Email and code required`, - message: () => `Email and code required. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN]: { - title: `Invalid email`, - message: () => `Invalid email. Please try again.`, - }, - - // Both Sign in and Sign up - [EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_IN]: { - title: `Authentication failed`, - message: () => `Invalid magic code. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_UP]: { - title: `Authentication failed`, - message: () => `Invalid magic code. Please try again.`, - }, - [EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN]: { - title: `Expired magic code`, - message: () => `Expired magic code. Please try again.`, - }, - [EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP]: { - title: `Expired magic code`, - message: () => `Expired magic code. Please try again.`, - }, - [EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN]: { - title: `Expired magic code`, - message: () => `Expired magic code. Please try again.`, - }, - [EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP]: { - title: `Expired magic code`, - message: () => `Expired magic code. Please try again.`, - }, - - // Oauth - [EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED]: { - title: `OAuth not configured`, - message: () => `OAuth not configured. Please contact your administrator.`, - }, - [EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: { - title: `Google not configured`, - message: () => `Google not configured. Please contact your administrator.`, - }, - [EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED]: { - title: `GitHub not configured`, - message: () => `GitHub not configured. Please contact your administrator.`, - }, - [EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED]: { - title: `GitLab not configured`, - message: () => `GitLab not configured. Please contact your administrator.`, - }, - [EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: { - title: `Google OAuth provider error`, - message: () => `Google OAuth provider error. Please try again.`, - }, - [EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR]: { - title: `GitHub OAuth provider error`, - message: () => `GitHub OAuth provider error. Please try again.`, - }, - [EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR]: { - title: `GitLab OAuth provider error`, - message: () => `GitLab OAuth provider error. Please try again.`, - }, - - // Reset Password - [EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: { - title: `Invalid password token`, - message: () => `Invalid password token. Please try again.`, - }, - [EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN]: { - title: `Expired password token`, - message: () => `Expired password token. Please try again.`, - }, - - // Change password - [EAuthenticationErrorCodes.MISSING_PASSWORD]: { - title: `Password required`, - message: () => `Password required. Please try again.`, - }, - [EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD]: { - title: `Incorrect old password`, - message: () => `Incorrect old password. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_NEW_PASSWORD]: { - title: `Invalid new password`, - message: () => `Invalid new password. Please try again.`, - }, - - // set password - [EAuthenticationErrorCodes.PASSWORD_ALREADY_SET]: { - title: `Password already set`, - message: () => `Password already set. Please try again.`, - }, - - // admin - [EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: { - title: `Admin already exists`, - message: () => `Admin already exists. Please try again.`, - }, - [EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: { - title: `Email, password and first name required`, - message: () => `Email, password and first name required. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: { - title: `Invalid admin email`, - message: () => `Invalid admin email. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: { - title: `Invalid admin password`, - message: () => `Invalid admin password. Please try again.`, - }, - [EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: { - title: `Email and password required`, - message: () => `Email and password required. Please try again.`, - }, - [EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: { - title: `Authentication failed`, - message: () => `Authentication failed. Please try again.`, - }, - [EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: { - title: `Admin user already exists`, - message: () => ( -
- Admin user already exists.  - - Sign In - -  now. -
- ), - }, - [EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: { - title: `Admin user does not exist`, - message: () => ( -
- Admin user does not exist.  - - Sign In - -  now. -
- ), - }, -}; - -export const authErrorHandler = (errorCode: EAuthenticationErrorCodes, email?: string): TAuthErrorInfo | undefined => { - const bannerAlertErrorCodes = [ - EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED, - EAuthenticationErrorCodes.INVALID_EMAIL, - EAuthenticationErrorCodes.EMAIL_REQUIRED, - EAuthenticationErrorCodes.SIGNUP_DISABLED, - EAuthenticationErrorCodes.INVALID_PASSWORD, - EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED, - EAuthenticationErrorCodes.USER_ALREADY_EXIST, - EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP, - EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP, - EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP, - EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP, - EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED, - EAuthenticationErrorCodes.USER_DOES_NOT_EXIST, - EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN, - EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN, - EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN, - EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN, - EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED, - EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_IN, - EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_UP, - EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN, - EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP, - EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN, - EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP, - EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED, - EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED, - EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED, - EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED, - EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR, - EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR, - EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR, - EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN, - EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN, - EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD, - EAuthenticationErrorCodes.INVALID_NEW_PASSWORD, - EAuthenticationErrorCodes.PASSWORD_ALREADY_SET, - EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST, - EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME, - EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL, - EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD, - EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD, - EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED, - EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST, - EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST, - EAuthenticationErrorCodes.BOT_USER_LOGIN_FORBIDDEN, - EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED, - ]; - - if (bannerAlertErrorCodes.includes(errorCode)) - return { - type: EErrorAlertType.BANNER_ALERT, - code: errorCode, - title: errorCodeMessages[errorCode]?.title || "Error", - message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.", - }; - - return undefined; -}; diff --git a/apps/space/helpers/common.helper.ts b/apps/space/helpers/common.helper.ts deleted file mode 100644 index f11f634e6..000000000 --- a/apps/space/helpers/common.helper.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export const resolveGeneralTheme = (resolvedTheme: string | undefined) => - resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system"; diff --git a/apps/space/helpers/date-time.helper.ts b/apps/space/helpers/date-time.helper.ts deleted file mode 100644 index eb6ce9b3b..000000000 --- a/apps/space/helpers/date-time.helper.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { format, isValid } from "date-fns"; -import { isNumber } from "lodash-es"; - -export const timeAgo = (time: any) => { - switch (typeof time) { - case "number": - break; - case "string": - time = +new Date(time); - break; - case "object": - if (time.constructor === Date) time = time.getTime(); - break; - default: - time = +new Date(); - } -}; - -/** - * This method returns a date from string of type yyyy-mm-dd - * This method is recommended to use instead of new Date() as this does not introduce any timezone offsets - * @param date - * @returns date or undefined - */ -export const getDate = (date: string | Date | undefined | null): Date | undefined => { - try { - if (!date || date === "") return; - - if (typeof date !== "string" && !(date instanceof String)) return date; - - const [yearString, monthString, dayString] = date.substring(0, 10).split("-"); - const year = parseInt(yearString); - const month = parseInt(monthString); - const day = parseInt(dayString); - if (!isNumber(year) || !isNumber(month) || !isNumber(day)) return; - - return new Date(year, month - 1, day); - } catch (_err) { - return undefined; - } -}; - -/** - * @returns {string | null} formatted date in the format of MMM dd, yyyy - * @description Returns date in the formatted format - * @param {Date | string} date - * @example renderFormattedDate("2024-01-01") // Jan 01, 2024 - */ -export const renderFormattedDate = (date: string | Date | undefined | null): string | null => { - // Parse the date to check if it is valid - const parsedDate = getDate(date); - // return if undefined - if (!parsedDate) return null; - // Check if the parsed date is valid before formatting - if (!isValid(parsedDate)) return null; // Return null for invalid dates - // Format the date in format (MMM dd, yyyy) - const formattedDate = format(parsedDate, "MMM dd, yyyy"); - return formattedDate; -}; diff --git a/apps/space/helpers/editor.helper.ts b/apps/space/helpers/editor.helper.ts deleted file mode 100644 index 26c787dc5..000000000 --- a/apps/space/helpers/editor.helper.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane imports -import { MAX_FILE_SIZE } from "@plane/constants"; -import type { TFileHandler } from "@plane/editor"; -import { SitesFileService } from "@plane/services"; -import { getFileURL } from "@plane/utils"; -// services -const sitesFileService = new SitesFileService(); - -/** - * @description generate the file source using assetId - * @param {string} anchor - */ -export const getEditorAssetSrc = (anchor: string, assetId: string): string | undefined => { - const url = getFileURL(`/api/public/assets/v2/anchor/${anchor}/${assetId}/`); - return url; -}; - -type TArgs = { - anchor: string; - uploadFile: TFileHandler["upload"]; - workspaceId: string; -}; - -/** - * @description this function returns the file handler required by the editors - * @param {TArgs} args - */ -export const getEditorFileHandlers = (args: TArgs): TFileHandler => { - const { anchor, uploadFile, workspaceId } = args; - - const getAssetSrc = async (path: string) => { - if (!path) return ""; - if (path?.startsWith("http")) { - return path; - } else { - return getEditorAssetSrc(anchor, path) ?? ""; - } - }; - - return { - checkIfAssetExists: async () => true, - assetsUploadStatus: {}, - getAssetDownloadSrc: getAssetSrc, - getAssetSrc: getAssetSrc, - upload: uploadFile, - delete: async (src: string) => { - if (src?.startsWith("http")) { - await sitesFileService.deleteOldEditorAsset(workspaceId, src); - } else { - await sitesFileService.deleteNewAsset(getEditorAssetSrc(anchor, src) ?? ""); - } - }, - cancel: sitesFileService.cancelUpload, - restore: async (src: string) => { - if (src?.startsWith("http")) { - await sitesFileService.restoreOldEditorAsset(workspaceId, src); - } else { - await sitesFileService.restoreNewAsset(anchor, src); - } - }, - duplicate: async (assetId: string) => - // Duplication is not supported for sites/space app - // Return the same assetId as a fallback - assetId, - validation: { - maxFileSize: MAX_FILE_SIZE, - }, - }; -}; diff --git a/apps/space/helpers/emoji.helper.tsx b/apps/space/helpers/emoji.helper.tsx deleted file mode 100644 index 12d359a73..000000000 --- a/apps/space/helpers/emoji.helper.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export const renderEmoji = ( - emoji: - | string - | { - name: string; - color: string; - } -) => { - if (!emoji) return; - - if (typeof emoji === "object") - return ( - - {emoji.name} - - ); - else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji)); -}; - -export const groupReactions = (reactions: T[], key: string) => { - const groupedReactions = reactions.reduce( - (acc: { [key: string]: T[] }, reaction: any) => { - if (!acc[reaction[key]]) { - acc[reaction[key]] = []; - } - acc[reaction[key]].push(reaction); - return acc; - }, - {} as { [key: string]: T[] } - ); - - return groupedReactions; -}; diff --git a/apps/space/helpers/file.helper.ts b/apps/space/helpers/file.helper.ts deleted file mode 100644 index 1a5947e82..000000000 --- a/apps/space/helpers/file.helper.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane imports -import { API_BASE_URL } from "@plane/constants"; - -/** - * @description combine the file path with the base URL - * @param {string} path - * @returns {string} final URL with the base URL - */ -export const getFileURL = (path: string): string | undefined => { - if (!path) return undefined; - const isValidURL = path.startsWith("http"); - if (isValidURL) return path; - return `${API_BASE_URL}${path}`; -}; - -/** - * @description this function returns the assetId from the asset source - * @param {string} src - * @returns {string} assetId - */ -export const getAssetIdFromUrl = (src: string): string => { - const sourcePaths = src.split("/"); - const assetUrl = sourcePaths[sourcePaths.length - 1]; - return assetUrl ?? ""; -}; diff --git a/apps/space/helpers/issue.helper.ts b/apps/space/helpers/issue.helper.ts deleted file mode 100644 index aa110349c..000000000 --- a/apps/space/helpers/issue.helper.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays"; -// plane internal -import { STATE_GROUPS } from "@plane/constants"; -import type { TStateGroups } from "@plane/types"; -// helpers -import { getDate } from "@/helpers/date-time.helper"; - -/** - * @description check if the issue due date should be highlighted - * @param date - * @param stateGroup - * @returns boolean - */ -export const shouldHighlightIssueDueDate = ( - date: string | Date | null, - stateGroup: TStateGroups | undefined -): boolean => { - if (!date || !stateGroup) return false; - // if the issue is completed or cancelled, don't highlight the due date - if ([STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateGroup)) return false; - - const parsedDate = getDate(date); - if (!parsedDate) return false; - - const targetDateDistance = differenceInCalendarDays(parsedDate, new Date()); - - // if the issue is overdue, highlight the due date - return targetDateDistance <= 0; -}; diff --git a/apps/space/helpers/query-param-generator.ts b/apps/space/helpers/query-param-generator.ts deleted file mode 100644 index 6f2ffcd2c..000000000 --- a/apps/space/helpers/query-param-generator.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -type TQueryParamValue = string | string[] | boolean | number | bigint | undefined | null; - -export const queryParamGenerator = (queryObject: Record) => { - const queryParamObject: Record = {}; - const queryParam = new URLSearchParams(); - - Object.entries(queryObject).forEach(([key, value]) => { - if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") { - queryParamObject[key] = value; - queryParam.append(key, value.toString()); - } else if (typeof value === "string" && value.length > 0) { - queryParamObject[key] = value.split(","); - queryParam.append(key, value); - } else if (Array.isArray(value) && value.length > 0) { - queryParamObject[key] = value; - queryParam.append(key, value.toString()); - } - }); - - return { - query: queryParamObject, - queryParam: queryParam.toString(), - }; -}; diff --git a/apps/space/helpers/state.helper.ts b/apps/space/helpers/state.helper.ts deleted file mode 100644 index 4c891a092..000000000 --- a/apps/space/helpers/state.helper.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { STATE_GROUPS } from "@plane/constants"; -import type { IState } from "@plane/types"; - -export const sortStates = (states: IState[]) => { - if (!states || states.length === 0) return; - - return [...states].toSorted((stateA: IState, stateB: IState) => { - if (stateA.group === stateB.group) { - return stateA.sequence - stateB.sequence; - } - return Object.keys(STATE_GROUPS).indexOf(stateA.group) - Object.keys(STATE_GROUPS).indexOf(stateB.group); - }); -}; diff --git a/apps/space/helpers/string.helper.ts b/apps/space/helpers/string.helper.ts deleted file mode 100644 index 7787fc365..000000000 --- a/apps/space/helpers/string.helper.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2"); - -const fallbackCopyTextToClipboard = (text: string) => { - const textArea = document.createElement("textarea"); - textArea.value = text; - - // Avoid scrolling to bottom - textArea.style.top = "0"; - textArea.style.left = "0"; - textArea.style.position = "fixed"; - - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - - try { - // FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this. - // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand - document.execCommand("copy"); - } catch (_err) {} - - document.body.removeChild(textArea); -}; - -export const copyTextToClipboard = async (text: string) => { - if (!navigator.clipboard) { - fallbackCopyTextToClipboard(text); - return; - } - await navigator.clipboard.writeText(text); -}; - -/** - * @returns {boolean} true if email is valid, false otherwise - * @description Returns true if email is valid, false otherwise - * @param {string} email string to check if it is a valid email - * @example checkEmailIsValid("hello world") => false - * @example checkEmailIsValid("example@plane.so") => true - */ -export const checkEmailValidity = (email: string): boolean => { - if (!email) return false; - - const isEmailValid = - /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - email - ); - - return isEmailValid; -}; - -export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); - -export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); - -/** - * @description - * This function test whether a URL is valid or not. - * - * It accepts URLs with or without the protocol. - * @param {string} url - * @returns {boolean} - * @example - * checkURLValidity("https://example.com") => true - * checkURLValidity("example.com") => true - * checkURLValidity("example") => false - */ -export const checkURLValidity = (url: string): boolean => { - if (!url) return false; - - // regex to support complex query parameters and fragments - const urlPattern = - /^(https?:\/\/)?((([a-z\d-]+\.)*[a-z\d-]+\.[a-z]{2,6})|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:\d+)?(\/[\w.-]*)*(\?[^#\s]*)?(#[\w-]*)?$/i; - - return urlPattern.test(url); -}; diff --git a/apps/space/hooks/oauth/core.tsx b/apps/space/hooks/oauth/core.tsx deleted file mode 100644 index 63a18cc2e..000000000 --- a/apps/space/hooks/oauth/core.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane imports -import { useSearchParams } from "next/navigation"; -import { useTheme } from "next-themes"; -import { API_BASE_URL } from "@plane/constants"; -import type { TOAuthConfigs, TOAuthOption } from "@plane/types"; -// assets -import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url"; -import githubLightLogo from "@/app/assets/logos/github-black.png?url"; -import githubDarkLogo from "@/app/assets/logos/github-dark.svg?url"; -import gitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url"; -import googleLogo from "@/app/assets/logos/google-logo.svg?url"; -// hooks -import { useInstance } from "@/hooks/store/use-instance"; - -export const useCoreOAuthConfig = (oauthActionText: string): TOAuthConfigs => { - //router - const searchParams = useSearchParams(); - // query params - const next_path = searchParams.get("next_path"); - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { config } = useInstance(); - // derived values - const isOAuthEnabled = - (config && - (config?.is_google_enabled || - config?.is_github_enabled || - config?.is_gitlab_enabled || - config?.is_gitea_enabled)) || - false; - const oAuthOptions: TOAuthOption[] = [ - { - id: "google", - text: `${oauthActionText} with Google`, - icon: Google Logo, - onClick: () => { - window.location.assign(`${API_BASE_URL}/auth/google/${next_path ? `?next_path=${next_path}` : ``}`); - }, - enabled: config?.is_google_enabled, - }, - { - id: "github", - text: `${oauthActionText} with GitHub`, - icon: ( - GitHub Logo - ), - onClick: () => { - window.location.assign(`${API_BASE_URL}/auth/github/${next_path ? `?next_path=${next_path}` : ``}`); - }, - enabled: config?.is_github_enabled, - }, - { - id: "gitlab", - text: `${oauthActionText} with GitLab`, - icon: GitLab Logo, - onClick: () => { - window.location.assign(`${API_BASE_URL}/auth/gitlab/${next_path ? `?next_path=${next_path}` : ``}`); - }, - enabled: config?.is_gitlab_enabled, - }, - { - id: "gitea", - text: `${oauthActionText} with Gitea`, - icon: Gitea Logo, - onClick: () => { - window.location.assign(`${API_BASE_URL}/auth/gitea/${next_path ? `?next_path=${next_path}` : ``}`); - }, - enabled: config?.is_gitea_enabled, - }, - ]; - - return { - isOAuthEnabled, - oAuthOptions, - }; -}; diff --git a/apps/space/hooks/oauth/extended.tsx b/apps/space/hooks/oauth/extended.tsx deleted file mode 100644 index 9e7ca61f1..000000000 --- a/apps/space/hooks/oauth/extended.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane imports -import type { TOAuthConfigs } from "@plane/types"; - -export const useExtendedOAuthConfig = (_oauthActionText: string): TOAuthConfigs => ({ - isOAuthEnabled: false, - oAuthOptions: [], -}); diff --git a/apps/space/hooks/oauth/index.ts b/apps/space/hooks/oauth/index.ts deleted file mode 100644 index e88c74399..000000000 --- a/apps/space/hooks/oauth/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane imports -import type { TOAuthConfigs } from "@plane/types"; -// local imports -import { useCoreOAuthConfig } from "./core"; -import { useExtendedOAuthConfig } from "./extended"; - -export const useOAuthConfig = (oauthActionText: string = "Continue"): TOAuthConfigs => { - const coreOAuthConfig = useCoreOAuthConfig(oauthActionText); - const extendedOAuthConfig = useExtendedOAuthConfig(oauthActionText); - return { - isOAuthEnabled: coreOAuthConfig.isOAuthEnabled || extendedOAuthConfig.isOAuthEnabled, - oAuthOptions: [...coreOAuthConfig.oAuthOptions, ...extendedOAuthConfig.oAuthOptions], - }; -}; diff --git a/apps/space/hooks/store/publish/index.ts b/apps/space/hooks/store/publish/index.ts deleted file mode 100644 index 6b565416f..000000000 --- a/apps/space/hooks/store/publish/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./use-publish-list"; -export * from "./use-publish"; diff --git a/apps/space/hooks/store/publish/use-publish-list.ts b/apps/space/hooks/store/publish/use-publish-list.ts deleted file mode 100644 index 8a731bd42..000000000 --- a/apps/space/hooks/store/publish/use-publish-list.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { IPublishListStore } from "@/store/publish/publish_list.store"; - -export const usePublishList = (): IPublishListStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("usePublishList must be used within StoreProvider"); - return context.publishList; -}; diff --git a/apps/space/hooks/store/publish/use-publish.ts b/apps/space/hooks/store/publish/use-publish.ts deleted file mode 100644 index 50ea52776..000000000 --- a/apps/space/hooks/store/publish/use-publish.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { PublishStore } from "@/store/publish/publish.store"; - -export const usePublish = (anchor: string): PublishStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("usePublish must be used within StoreProvider"); - return context.publishList.publishMap?.[anchor] ?? {}; -}; diff --git a/apps/space/hooks/store/use-cycle.ts b/apps/space/hooks/store/use-cycle.ts deleted file mode 100644 index 4b7e9e4d5..000000000 --- a/apps/space/hooks/store/use-cycle.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { ICycleStore } from "@/store/cycle.store"; - -export const useCycle = (): ICycleStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useCycle must be used within StoreProvider"); - return context.cycle; -}; diff --git a/apps/space/hooks/store/use-instance.ts b/apps/space/hooks/store/use-instance.ts deleted file mode 100644 index 3f783a90e..000000000 --- a/apps/space/hooks/store/use-instance.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { IInstanceStore } from "@/store/instance.store"; - -export const useInstance = (): IInstanceStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); - return context.instance; -}; diff --git a/apps/space/hooks/store/use-issue-details.tsx b/apps/space/hooks/store/use-issue-details.tsx deleted file mode 100644 index a30403e1f..000000000 --- a/apps/space/hooks/store/use-issue-details.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { IIssueDetailStore } from "@/store/issue-detail.store"; - -export const useIssueDetails = (): IIssueDetailStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); - return context.issueDetail; -}; diff --git a/apps/space/hooks/store/use-issue-filter.ts b/apps/space/hooks/store/use-issue-filter.ts deleted file mode 100644 index 1f60bae99..000000000 --- a/apps/space/hooks/store/use-issue-filter.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { IIssueFilterStore } from "@/store/issue-filters.store"; - -export const useIssueFilter = (): IIssueFilterStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); - return context.issueFilter; -}; diff --git a/apps/space/hooks/store/use-issue.ts b/apps/space/hooks/store/use-issue.ts deleted file mode 100644 index 808117e05..000000000 --- a/apps/space/hooks/store/use-issue.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { IIssueStore } from "@/store/issue.store"; - -export const useIssue = (): IIssueStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useIssue must be used within StoreProvider"); - return context.issue; -}; diff --git a/apps/space/hooks/store/use-label.ts b/apps/space/hooks/store/use-label.ts deleted file mode 100644 index f7c261839..000000000 --- a/apps/space/hooks/store/use-label.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { IIssueLabelStore } from "@/store/label.store"; - -export const useLabel = (): IIssueLabelStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useLabel must be used within StoreProvider"); - return context.label; -}; diff --git a/apps/space/hooks/store/use-member.ts b/apps/space/hooks/store/use-member.ts deleted file mode 100644 index 82443db77..000000000 --- a/apps/space/hooks/store/use-member.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { IIssueMemberStore } from "@/store/members.store"; - -export const useMember = (): IIssueMemberStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useMember must be used within StoreProvider"); - return context.member; -}; diff --git a/apps/space/hooks/store/use-module.ts b/apps/space/hooks/store/use-module.ts deleted file mode 100644 index c2d5768da..000000000 --- a/apps/space/hooks/store/use-module.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { IIssueModuleStore } from "@/store/module.store"; - -export const useModule = (): IIssueModuleStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useModule must be used within StoreProvider"); - return context.module; -}; diff --git a/apps/space/hooks/store/use-state.ts b/apps/space/hooks/store/use-state.ts deleted file mode 100644 index 8f239777d..000000000 --- a/apps/space/hooks/store/use-state.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { IStateStore } from "@/store/state.store"; - -export const useStates = (): IStateStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useState must be used within StoreProvider"); - return context.state; -}; diff --git a/apps/space/hooks/store/use-user-profile.ts b/apps/space/hooks/store/use-user-profile.ts deleted file mode 100644 index ec53da955..000000000 --- a/apps/space/hooks/store/use-user-profile.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { IProfileStore } from "@/store/profile.store"; - -export const useUserProfile = (): IProfileStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); - return context.user.profile; -}; diff --git a/apps/space/hooks/store/use-user.ts b/apps/space/hooks/store/use-user.ts deleted file mode 100644 index b3af67a51..000000000 --- a/apps/space/hooks/store/use-user.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import type { IUserStore } from "@/store/user.store"; - -export const useUser = (): IUserStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useUser must be used within StoreProvider"); - return context.user; -}; diff --git a/apps/space/hooks/use-clipboard-write-permission.tsx b/apps/space/hooks/use-clipboard-write-permission.tsx deleted file mode 100644 index 8b1bf16ac..000000000 --- a/apps/space/hooks/use-clipboard-write-permission.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useState, useEffect } from "react"; - -const useClipboardWritePermission = () => { - const [isClipboardWriteAllowed, setClipboardWriteAllowed] = useState(false); - - useEffect(() => { - const checkClipboardWriteAccess = () => { - navigator.permissions - - .query({ name: "clipboard-write" as PermissionName }) - .then((result) => { - if (result.state === "granted") { - setClipboardWriteAllowed(true); - } else { - setClipboardWriteAllowed(false); - } - }) - .catch(() => { - setClipboardWriteAllowed(false); - }); - }; - - checkClipboardWriteAccess(); - }, []); - - return isClipboardWriteAllowed; -}; - -export default useClipboardWritePermission; diff --git a/apps/space/hooks/use-editor-flagging.ts b/apps/space/hooks/use-editor-flagging.ts deleted file mode 100644 index 88e82f769..000000000 --- a/apps/space/hooks/use-editor-flagging.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// editor -import type { TExtensions } from "@plane/editor"; - -export type TEditorFlaggingHookReturnType = { - document: { - disabled: TExtensions[]; - flagged: TExtensions[]; - }; - liteText: { - disabled: TExtensions[]; - flagged: TExtensions[]; - }; - richText: { - disabled: TExtensions[]; - flagged: TExtensions[]; - }; -}; - -/** - * @description extensions disabled in various editors - */ -export const useEditorFlagging = (_anchor: string): TEditorFlaggingHookReturnType => ({ - document: { - disabled: [], - flagged: [], - }, - liteText: { - disabled: [], - flagged: [], - }, - richText: { - disabled: [], - flagged: [], - }, -}); diff --git a/apps/space/hooks/use-intersection-observer.tsx b/apps/space/hooks/use-intersection-observer.tsx deleted file mode 100644 index 4cf9a270a..000000000 --- a/apps/space/hooks/use-intersection-observer.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { RefObject } from "react"; -import { useEffect } from "react"; - -export type UseIntersectionObserverProps = { - containerRef: RefObject | undefined; - elementRef: HTMLElement | null; - callback: () => void; - rootMargin?: string; -}; - -export const useIntersectionObserver = ( - containerRef: RefObject, - elementRef: HTMLElement | null, - callback: (() => void) | undefined, - rootMargin?: string -) => { - useEffect(() => { - if (elementRef) { - const observer = new IntersectionObserver( - (entries) => { - if (entries[entries.length - 1].isIntersecting) { - if (callback) { - callback(); - } - } - }, - { - root: containerRef?.current, - rootMargin, - } - ); - observer.observe(elementRef); - return () => { - if (elementRef) { - observer.unobserve(elementRef); - } - }; - } - // When i am passing callback as a dependency, it is causing infinite loop, - // Please make sure you fix this eslint lint disable error with caution - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rootMargin, callback, elementRef, containerRef.current]); -}; diff --git a/apps/space/hooks/use-is-in-iframe.tsx b/apps/space/hooks/use-is-in-iframe.tsx deleted file mode 100644 index 3fb5b5575..000000000 --- a/apps/space/hooks/use-is-in-iframe.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useState, useEffect } from "react"; - -const useIsInIframe = () => { - const [isInIframe, setIsInIframe] = useState(false); - - useEffect(() => { - const checkIfInIframe = () => { - setIsInIframe(window.self !== window.top); - }; - - checkIfInIframe(); - }, []); - - return isInIframe; -}; - -export default useIsInIframe; diff --git a/apps/space/hooks/use-mention.tsx b/apps/space/hooks/use-mention.tsx deleted file mode 100644 index 1d84cd7b3..000000000 --- a/apps/space/hooks/use-mention.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useRef, useEffect } from "react"; -import useSWR from "swr"; -// plane imports -import { UserService } from "@plane/services"; -import type { IUser } from "@plane/types"; - -export const useMention = () => { - const userService = new UserService(); - const { data: user, isLoading: userDataLoading } = useSWR("currentUser", async () => userService.me()); - - const userRef = useRef(undefined); - - useEffect(() => { - if (userRef) { - userRef.current = user; - } - }, [user]); - - const waitForUserDate = async () => - new Promise((resolve) => { - const checkData = () => { - if (userRef.current) { - resolve(userRef.current); - } else { - setTimeout(checkData, 100); - } - }; - checkData(); - }); - - const mentionHighlights = async () => { - if (!userDataLoading && userRef.current) { - return [userRef.current.id]; - } else { - const user = await waitForUserDate(); - return [user.id]; - } - }; - - return { - mentionHighlights, - }; -}; diff --git a/apps/space/hooks/use-parse-editor-content.ts b/apps/space/hooks/use-parse-editor-content.ts deleted file mode 100644 index ec13194ea..000000000 --- a/apps/space/hooks/use-parse-editor-content.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback } from "react"; -// helpers -import type { TCustomComponentsMetaData } from "@plane/utils"; -// helpers -import { getEditorAssetSrc } from "@/helpers/editor.helper"; -// hooks -import { useMember } from "@/hooks/store/use-member"; - -type TArgs = { - anchor: string; -}; - -export const useParseEditorContent = (args: TArgs) => { - const { anchor } = args; - // store hooks - const { getMemberById } = useMember(); - - const getEditorMetaData = useCallback( - (htmlContent: string): TCustomComponentsMetaData => { - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlContent, "text/html"); - const imageMetaData: TCustomComponentsMetaData["file_assets"] = []; - // process image components - const imageComponents = doc.querySelectorAll("image-component"); - imageComponents.forEach((element) => { - const src = element.getAttribute("src"); - if (src) { - const assetSrc = src.startsWith("http") ? src : getEditorAssetSrc(anchor, src); - if (assetSrc) { - imageMetaData.push({ - id: src, - name: src, - url: assetSrc, - }); - } - } - }); - // process user mentions - const userMentions: TCustomComponentsMetaData["user_mentions"] = []; - const mentionComponents = doc.querySelectorAll("mention-component"); - mentionComponents.forEach((element) => { - const id = element.getAttribute("entity_identifier"); - if (id) { - const userDetails = getMemberById(id); - const originUrl = typeof window !== "undefined" && (window.location.origin ?? ""); - const path = `profile/${id}`; - const url = `${originUrl}/${path}`; - if (userDetails) { - userMentions.push({ - id, - display_name: userDetails.member__display_name, - url, - }); - } - } - }); - - return { - file_assets: imageMetaData, - user_mentions: userMentions, - }; - }, - [anchor, getMemberById] - ); - - return { - getEditorMetaData, - }; -}; diff --git a/apps/space/hooks/use-timer.tsx b/apps/space/hooks/use-timer.tsx deleted file mode 100644 index c663d8007..000000000 --- a/apps/space/hooks/use-timer.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useState, useEffect } from "react"; - -const TIMER = 30; - -const useTimer = (initialValue: number = TIMER) => { - const [timer, setTimer] = useState(initialValue); - - useEffect(() => { - const interval = setInterval(() => { - setTimer((prev) => prev - 1); - }, 1000); - - return () => clearInterval(interval); - }, []); - - return { timer, setTimer }; -}; - -export default useTimer; diff --git a/apps/space/lib/b-progress/AppProgressBar.tsx b/apps/space/lib/b-progress/AppProgressBar.tsx deleted file mode 100644 index e4362581d..000000000 --- a/apps/space/lib/b-progress/AppProgressBar.tsx +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect, useRef } from "react"; -import { BProgress } from "@bprogress/core"; -import { useNavigation } from "react-router"; -import "@bprogress/core/css"; - -/** - * Progress bar configuration options - */ -interface ProgressConfig { - /** Whether to show the loading spinner */ - showSpinner: boolean; - /** Minimum progress percentage (0-1) */ - minimum: number; - /** Animation speed in milliseconds */ - speed: number; - /** Auto-increment speed in milliseconds */ - trickleSpeed: number; - /** CSS easing function */ - easing: string; - /** Enable auto-increment */ - trickle: boolean; - /** Delay before showing progress bar in milliseconds */ - delay: number; - /** Whether to disable the progress bar */ - isDisabled?: boolean; -} - -/** - * Configuration for the progress bar - */ -const PROGRESS_CONFIG: Readonly = { - showSpinner: false, - minimum: 0.1, - speed: 400, - trickleSpeed: 800, - easing: "ease", - trickle: true, - delay: 0, -} as const; - -/** - * Navigation Progress Bar Component - * - * Automatically displays a progress bar at the top of the page during React Router navigation. - * Integrates with React Router's useNavigation hook to monitor route changes. - * - * Note: Progress bar is disabled in production builds. - * - * @returns null - This component doesn't render any visible elements - * - * @example - * ```tsx - * function App() { - * return ( - * <> - * - * - * - * ); - * } - * ``` - */ -export function AppProgressBar(): null { - const navigation = useNavigation(); - const timerRef = useRef | null>(null); - const startedRef = useRef(false); - - // Initialize BProgress once on mount - useEffect(() => { - // Skip initialization in production builds - if (PROGRESS_CONFIG.isDisabled) { - return; - } - - // Configure BProgress with our settings - BProgress.configure({ - showSpinner: PROGRESS_CONFIG.showSpinner, - minimum: PROGRESS_CONFIG.minimum, - speed: PROGRESS_CONFIG.speed, - trickleSpeed: PROGRESS_CONFIG.trickleSpeed, - easing: PROGRESS_CONFIG.easing, - trickle: PROGRESS_CONFIG.trickle, - }); - - // Render the progress bar element in the DOM - BProgress.render(true); - - // Cleanup on unmount - return () => { - if (BProgress.isStarted()) { - BProgress.done(); - } - }; - }, []); - - // Handle navigation state changes - useEffect(() => { - // Skip navigation tracking in production builds - if (PROGRESS_CONFIG.isDisabled) { - return; - } - - if (navigation.state === "idle") { - // Navigation complete - clear any pending timer - if (timerRef.current !== null) { - clearTimeout(timerRef.current); - timerRef.current = null; - } - - // Complete progress if it was started - if (startedRef.current) { - BProgress.done(); - startedRef.current = false; - } - } else { - // Navigation in progress (loading or submitting) - // Only start if not already started and no timer pending - if (timerRef.current === null && !startedRef.current) { - timerRef.current = setTimeout((): void => { - if (!BProgress.isStarted()) { - BProgress.start(); - startedRef.current = true; - } - timerRef.current = null; - }, PROGRESS_CONFIG.delay); - } - } - - return () => { - if (timerRef.current !== null) { - clearTimeout(timerRef.current); - } - }; - }, [navigation.state]); - - return null; -} diff --git a/apps/space/lib/b-progress/index.tsx b/apps/space/lib/b-progress/index.tsx deleted file mode 100644 index 592017255..000000000 --- a/apps/space/lib/b-progress/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./AppProgressBar"; diff --git a/apps/space/lib/instance-provider.tsx b/apps/space/lib/instance-provider.tsx deleted file mode 100644 index bd18f8164..000000000 --- a/apps/space/lib/instance-provider.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { Link } from "react-router"; -import { useTheme } from "next-themes"; -import useSWR from "swr"; -// plane imports -import { SPACE_BASE_PATH } from "@plane/constants"; -import { PlaneLockup } from "@plane/propel/icons"; -// assets -import PlaneBackgroundPatternDark from "@/app/assets/auth/background-pattern-dark.svg?url"; -import PlaneBackgroundPattern from "@/app/assets/auth/background-pattern.svg?url"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { InstanceFailureView } from "@/components/instance/instance-failure-view"; -// hooks -import { useInstance } from "@/hooks/store/use-instance"; -import { useUser } from "@/hooks/store/use-user"; - -export const InstanceProvider = observer(function InstanceProvider({ children }: { children: React.ReactNode }) { - const { fetchInstanceInfo, instance, error } = useInstance(); - const { fetchCurrentUser } = useUser(); - const { resolvedTheme } = useTheme(); - - const patternBackground = resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern; - - useSWR("INSTANCE_INFO", () => fetchInstanceInfo(), { - revalidateOnFocus: false, - revalidateIfStale: false, - errorRetryCount: 0, - }); - useSWR("CURRENT_USER", () => fetchCurrentUser(), { - shouldRetryOnError: false, - revalidateOnFocus: true, - revalidateIfStale: true, - }); - - if (!instance && !error) - return ( -
- -
- ); - - if (error) { - return ( -
-
-
-
- - - -
-
-
- Plane background pattern -
-
-
- -
-
-
-
- ); - } - - return children; -}); diff --git a/apps/space/lib/store-provider.tsx b/apps/space/lib/store-provider.tsx deleted file mode 100644 index 298759090..000000000 --- a/apps/space/lib/store-provider.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { createContext } from "react"; -// plane web store -import { RootStore } from "@/store/root.store"; - -let rootStore = new RootStore(); - -export const StoreContext = createContext(rootStore); - -function initializeStore() { - const singletonRootStore = rootStore ?? new RootStore(); - // For SSG and SSR always create a new store - if (typeof window === "undefined") return singletonRootStore; - // Create the store once in the client - if (!rootStore) rootStore = singletonRootStore; - return singletonRootStore; -} - -export type StoreProviderProps = { - children: React.ReactNode; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - initialState?: any; -}; - -export function StoreProvider({ children, initialState = undefined }: StoreProviderProps) { - const store = initializeStore(); - // If your page has Next.js data fetching methods that use a Mobx store, it will - // get hydrated here, check `pages/ssg.js` and `pages/ssr.js` for more details - if (initialState) { - store.hydrate(initialState); - } - - return {children}; -} diff --git a/apps/space/lib/toast-provider.tsx b/apps/space/lib/toast-provider.tsx deleted file mode 100644 index 420c80113..000000000 --- a/apps/space/lib/toast-provider.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useTheme } from "next-themes"; -// plane imports -import { Toast } from "@plane/propel/toast"; -import { resolveGeneralTheme } from "@plane/utils"; - -export function ToastProvider({ children }: { children: React.ReactNode }) { - // themes - const { resolvedTheme } = useTheme(); - - return ( - <> - - {children} - - ); -} diff --git a/apps/space/nginx/nginx.conf b/apps/space/nginx/nginx.conf deleted file mode 100644 index e6f15b1ee..000000000 --- a/apps/space/nginx/nginx.conf +++ /dev/null @@ -1,30 +0,0 @@ -worker_processes 4; - -events { - worker_connections 1024; -} - -http { - include mime.types; - - default_type application/octet-stream; - - set_real_ip_from 0.0.0.0/0; - real_ip_recursive on; - real_ip_header X-Forwarded-For; - limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s; - - access_log /dev/stdout; - error_log /dev/stderr; - - server { - listen 3000; - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /spaces/index.html; - } - } -} - diff --git a/apps/space/package.json b/apps/space/package.json deleted file mode 100644 index ea56f37d7..000000000 --- a/apps/space/package.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "name": "space", - "version": "1.4.2", - "private": true, - "license": "AGPL-3.0", - "type": "module", - "scripts": { - "dev": "react-router dev --port 3002", - "build": "react-router build", - "preview": "react-router build && PORT=3002 react-router-serve ./build/server/index.js", - "start": "PORT=3002 react-router-serve ./build/server/index.js", - "clean": "rm -rf .turbo && rm -rf .next && rm -rf .react-router && rm -rf node_modules && rm -rf dist && rm -rf build", - "check:lint": "oxlint --max-warnings=676 .", - "check:types": "react-router typegen && tsc --noEmit", - "check:format": "oxfmt --check .", - "fix:lint": "oxlint --fix .", - "fix:format": "oxfmt ." - }, - "dependencies": { - "@bprogress/core": "catalog:", - "@fontsource-variable/inter": "catalog:", - "@fontsource/ibm-plex-mono": "catalog:", - "@fontsource/material-symbols-rounded": "catalog:", - "@headlessui/react": "catalog:", - "@makeplane/propel": "catalog:", - "@plane/constants": "workspace:*", - "@plane/editor": "workspace:*", - "@plane/i18n": "workspace:*", - "@plane/propel": "workspace:*", - "@plane/services": "workspace:*", - "@plane/types": "workspace:*", - "@plane/ui": "workspace:*", - "@plane/utils": "workspace:*", - "@popperjs/core": "catalog:", - "@react-router/node": "catalog:", - "@react-router/serve": "catalog:", - "axios": "catalog:", - "clsx": "catalog:", - "date-fns": "catalog:", - "isbot": "catalog:", - "lodash-es": "catalog:", - "lucide-react": "catalog:", - "mobx": "catalog:", - "mobx-react": "catalog:", - "mobx-utils": "catalog:", - "next-themes": "catalog:", - "react": "catalog:", - "react-dom": "catalog:", - "react-dropzone": "catalog:", - "react-hook-form": "catalog:", - "react-popper": "catalog:", - "react-router": "catalog:", - "swr": "catalog:", - "uuid": "catalog:" - }, - "devDependencies": { - "@plane/tailwind-config": "workspace:*", - "@plane/typescript-config": "workspace:*", - "@react-router/dev": "catalog:", - "@tailwindcss/postcss": "catalog:", - "@tailwindcss/typography": "catalog:", - "@types/lodash-es": "catalog:", - "@types/node": "catalog:", - "@types/react": "catalog:", - "@types/react-dom": "catalog:", - "dotenv": "catalog:", - "typescript": "catalog:", - "vite": "catalog:", - "vite-tsconfig-paths": "catalog:" - } -} diff --git a/apps/space/postcss.config.js b/apps/space/postcss.config.js deleted file mode 100644 index 3ad28f15f..000000000 --- a/apps/space/postcss.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import postcssConfig from "@plane/tailwind-config/postcss.config.js"; - -export default postcssConfig; diff --git a/apps/space/public/favicon/android-chrome-192x192.png b/apps/space/public/favicon/android-chrome-192x192.png deleted file mode 100644 index 4a005e542..000000000 Binary files a/apps/space/public/favicon/android-chrome-192x192.png and /dev/null differ diff --git a/apps/space/public/favicon/android-chrome-512x512.png b/apps/space/public/favicon/android-chrome-512x512.png deleted file mode 100644 index 27fafe822..000000000 Binary files a/apps/space/public/favicon/android-chrome-512x512.png and /dev/null differ diff --git a/apps/space/public/site.webmanifest.json b/apps/space/public/site.webmanifest.json deleted file mode 100644 index 8885d137b..000000000 --- a/apps/space/public/site.webmanifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "Plane Space", - "short_name": "Plane Space", - "description": "Plane helps you plan your work items, cycles, and product modules.", - "start_url": ".", - "display": "standalone", - "background_color": "#f9fafb", - "theme_color": "#3f76ff", - "icons": [ - { "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } - ] -} diff --git a/apps/space/react-router.config.ts b/apps/space/react-router.config.ts deleted file mode 100644 index 78a174398..000000000 --- a/apps/space/react-router.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Config } from "@react-router/dev/config"; -import { joinUrlPath } from "@plane/utils"; - -const basePath = joinUrlPath(process.env.VITE_SPACE_BASE_PATH ?? "", "/") ?? "/"; - -export default { - appDirectory: "app", - basename: basePath, - ssr: true, -} satisfies Config; diff --git a/apps/space/store/cycle.store.ts b/apps/space/store/cycle.store.ts deleted file mode 100644 index 13cb6950a..000000000 --- a/apps/space/store/cycle.store.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { action, makeObservable, observable, runInAction } from "mobx"; -// plane imports -import { SitesCycleService } from "@plane/services"; -import type { TPublicCycle } from "@/types/cycle"; -// store -import type { RootStore } from "./root.store"; - -export interface ICycleStore { - // observables - cycles: TPublicCycle[] | undefined; - // computed actions - getCycleById: (cycleId: string | undefined) => TPublicCycle | undefined; - // fetch actions - fetchCycles: (anchor: string) => Promise; -} - -export class CycleStore implements ICycleStore { - cycles: TPublicCycle[] | undefined = undefined; - cycleService: SitesCycleService; - rootStore: RootStore; - - constructor(_rootStore: RootStore) { - makeObservable(this, { - // observables - cycles: observable, - // fetch action - fetchCycles: action, - }); - this.cycleService = new SitesCycleService(); - this.rootStore = _rootStore; - } - - getCycleById = (cycleId: string | undefined) => this.cycles?.find((cycle) => cycle.id === cycleId); - - fetchCycles = async (anchor: string) => { - const cyclesResponse = await this.cycleService.list(anchor); - runInAction(() => { - this.cycles = cyclesResponse; - }); - return cyclesResponse; - }; -} diff --git a/apps/space/store/helpers/base-issues.store.ts b/apps/space/store/helpers/base-issues.store.ts deleted file mode 100644 index e0fdb564c..000000000 --- a/apps/space/store/helpers/base-issues.store.ts +++ /dev/null @@ -1,521 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { concat, get, set, uniq, update } from "lodash-es"; -import { action, makeObservable, observable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; -// plane imports -import { ALL_ISSUES } from "@plane/constants"; -import { SitesIssueService } from "@plane/services"; -import type { - TIssueGroupByOptions, - TGroupedIssues, - TSubGroupedIssues, - TLoader, - IssuePaginationOptions, - TIssues, - TIssuePaginationData, - TGroupedIssueCount, - TPaginationData, -} from "@plane/types"; -// types -import type { IIssue, TIssuesResponse } from "@/types/issue"; -import type { RootStore } from "../root.store"; -// constants -// helpers - -export type TIssueDisplayFilterOptions = Exclude | "target_date"; - -export enum EIssueGroupedAction { - ADD = "ADD", - DELETE = "DELETE", - REORDER = "REORDER", -} - -export interface IBaseIssuesStore { - // observable - loader: Record; - // actions - addIssue(issues: IIssue[], shouldReplace?: boolean): void; - // helper methods - groupedIssueIds: TGroupedIssues | TSubGroupedIssues | undefined; // object to store Issue Ids based on group or subgroup - groupedIssueCount: TGroupedIssueCount; // map of groupId/subgroup and issue count of that particular group/subgroup - issuePaginationData: TIssuePaginationData; // map of groupId/subgroup and pagination Data of that particular group/subgroup - - // helper methods - getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined; - getPaginationData(groupId: string | undefined, subGroupId: string | undefined): TPaginationData | undefined; - getIssueLoader(groupId?: string, subGroupId?: string): TLoader; - getGroupIssueCount: ( - groupId: string | undefined, - subGroupId: string | undefined, - isSubGroupCumulative: boolean - ) => number | undefined; -} - -export const ISSUE_FILTER_DEFAULT_DATA: Record = { - project: "project_id", - cycle: "cycle_id", - module: "module_ids", - state: "state_id", - "state_detail.group": "state_group" as keyof IIssue, // state_detail.group is only being used for state_group display, - priority: "priority", - labels: "label_ids", - created_by: "created_by", - assignees: "assignee_ids", - target_date: "target_date", -}; - -export abstract class BaseIssuesStore implements IBaseIssuesStore { - loader: Record = {}; - groupedIssueIds: TIssues | undefined = undefined; - issuePaginationData: TIssuePaginationData = {}; - groupedIssueCount: TGroupedIssueCount = {}; - // - paginationOptions: IssuePaginationOptions | undefined = undefined; - - issueService; - // root store - rootIssueStore; - - constructor(_rootStore: RootStore) { - makeObservable(this, { - // observable - loader: observable, - groupedIssueIds: observable, - issuePaginationData: observable, - groupedIssueCount: observable, - - paginationOptions: observable, - // action - storePreviousPaginationValues: action.bound, - - onfetchIssues: action.bound, - onfetchNexIssues: action.bound, - clear: action.bound, - setLoader: action.bound, - }); - this.rootIssueStore = _rootStore; - this.issueService = new SitesIssueService(); - } - - getIssueIds = (groupId?: string, subGroupId?: string) => { - const groupedIssueIds = this.groupedIssueIds; - - if (!groupedIssueIds) return undefined; - - const allIssues = groupedIssueIds[ALL_ISSUES] ?? []; - if (allIssues && Array.isArray(allIssues)) { - return allIssues; - } - - if (groupId && groupedIssueIds?.[groupId] && Array.isArray(groupedIssueIds[groupId])) { - return groupedIssueIds[groupId] ?? []; - } - - if (groupId && subGroupId) { - return (groupedIssueIds as TSubGroupedIssues)[groupId]?.[subGroupId] ?? []; - } - - return undefined; - }; - - /** - * @description This method will add issues to the issuesMap - * @param {IIssue[]} issues - * @returns {void} - */ - addIssue = (issues: IIssue[], shouldReplace = false) => { - if (issues && issues.length <= 0) return; - runInAction(() => { - issues.forEach((issue) => { - if (!this.rootIssueStore.issueDetail.getIssueById(issue.id) || shouldReplace) - set(this.rootIssueStore.issueDetail.details, issue.id, issue); - }); - }); - }; - - /** - * Store the pagination data required for next subsequent issue pagination calls - * @param prevCursor cursor value of previous page - * @param nextCursor cursor value of next page - * @param nextPageResults boolean to indicate if the next page results exist i.e, have we reached end of pages - * @param groupId groupId and subGroupId to add the pagination data for the particular group/subgroup - * @param subGroupId - */ - setPaginationData( - prevCursor: string, - nextCursor: string, - nextPageResults: boolean, - groupId?: string, - subGroupId?: string - ) { - const cursorObject = { - prevCursor, - nextCursor, - nextPageResults, - }; - - set(this.issuePaginationData, [this.getGroupKey(groupId, subGroupId)], cursorObject); - } - - /** - * Sets the loader value of the particular groupId/subGroupId, or to ALL_ISSUES if both are undefined - * @param loaderValue - * @param groupId - * @param subGroupId - */ - setLoader(loaderValue: TLoader, groupId?: string, subGroupId?: string) { - runInAction(() => { - set(this.loader, this.getGroupKey(groupId, subGroupId), loaderValue); - }); - } - - /** - * gets the Loader value of particular group/subgroup/ALL_ISSUES - */ - getIssueLoader = (groupId?: string, subGroupId?: string) => get(this.loader, this.getGroupKey(groupId, subGroupId)); - - /** - * gets the pagination data of particular group/subgroup/ALL_ISSUES - */ - getPaginationData = computedFn( - (groupId: string | undefined, subGroupId: string | undefined): TPaginationData | undefined => - get(this.issuePaginationData, [this.getGroupKey(groupId, subGroupId)]) - ); - - /** - * gets the issue count of particular group/subgroup/ALL_ISSUES - * - * if isSubGroupCumulative is true, sum up all the issueCount of the subGroupId, across all the groupIds - */ - getGroupIssueCount = computedFn( - ( - groupId: string | undefined, - subGroupId: string | undefined, - isSubGroupCumulative: boolean - ): number | undefined => { - if (isSubGroupCumulative && subGroupId) { - const groupIssuesKeys = Object.keys(this.groupedIssueCount); - let subGroupCumulativeCount = 0; - - for (const groupKey of groupIssuesKeys) { - if (groupKey.includes(`_${subGroupId}`)) subGroupCumulativeCount += this.groupedIssueCount[groupKey]; - } - - return subGroupCumulativeCount; - } - - return get(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)]); - } - ); - - /** - * This Method is called after fetching the first paginated issues - * - * This method updates the appropriate issue list based on if groupByKey or subGroupByKey are defined - * If both groupByKey and subGroupByKey are not defined, then the issue list are added to another group called ALL_ISSUES - * @param issuesResponse Paginated Response received from the API - * @param options Pagination options - * @param workspaceSlug - * @param projectId - * @param id Id can be anything from cycleId, moduleId, viewId or userId based on the store - */ - onfetchIssues(issuesResponse: TIssuesResponse, options: IssuePaginationOptions) { - // Process the Issue Response to get the following data from it - const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse); - - // The Issue list is added to the main Issue Map - this.addIssue(issueList); - - // Update all the GroupIds to this Store's groupedIssueIds and update Individual group issue counts - runInAction(() => { - this.updateGroupedIssueIds(groupedIssues, groupedIssueCount); - this.loader[this.getGroupKey()] = undefined; - }); - - // store Pagination options for next subsequent calls and data like next cursor etc - this.storePreviousPaginationValues(issuesResponse, options); - } - - /** - * This Method is called on the subsequent pagination calls after the first initial call - * - * This method updates the appropriate issue list based on if groupId or subgroupIds are Passed - * @param issuesResponse Paginated Response received from the API - * @param groupId - * @param subGroupId - */ - onfetchNexIssues(issuesResponse: TIssuesResponse, groupId?: string, subGroupId?: string) { - // Process the Issue Response to get the following data from it - const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse); - - // The Issue list is added to the main Issue Map - this.addIssue(issueList); - - // Update all the GroupIds to this Store's groupedIssueIds and update Individual group issue counts - runInAction(() => { - this.updateGroupedIssueIds(groupedIssues, groupedIssueCount, groupId, subGroupId); - this.loader[this.getGroupKey(groupId, subGroupId)] = undefined; - }); - - // store Pagination data like next cursor etc - this.storePreviousPaginationValues(issuesResponse, undefined, groupId, subGroupId); - } - - /** - * Method called to clear out the current store - */ - clear(shouldClearPaginationOptions = true) { - runInAction(() => { - this.groupedIssueIds = undefined; - this.issuePaginationData = {}; - this.groupedIssueCount = {}; - if (shouldClearPaginationOptions) { - this.paginationOptions = undefined; - } - }); - } - - /** - * This method processes the issueResponse to provide data that can be used to update the store - * @param issueResponse - * @returns issueList, list of issue Data - * @returns groupedIssues, grouped issue Ids - * @returns groupedIssueCount, object containing issue counts of individual groups - */ - processIssueResponse(issueResponse: TIssuesResponse): { - issueList: IIssue[]; - groupedIssues: TIssues; - groupedIssueCount: TGroupedIssueCount; - } { - const issueResult = issueResponse?.results; - - // if undefined return empty objects - if (!issueResult) - return { - issueList: [], - groupedIssues: {}, - groupedIssueCount: {}, - }; - - //if is an array then it's an ungrouped response. return values with groupId as ALL_ISSUES - if (Array.isArray(issueResult)) { - return { - issueList: issueResult, - groupedIssues: { - [ALL_ISSUES]: issueResult.map((issue) => issue.id), - }, - groupedIssueCount: { - [ALL_ISSUES]: issueResponse.total_count, - }, - }; - } - - const issueList: IIssue[] = []; - const groupedIssues: TGroupedIssues | TSubGroupedIssues = {}; - const groupedIssueCount: TGroupedIssueCount = {}; - - // update total issue count to ALL_ISSUES - set(groupedIssueCount, [ALL_ISSUES], issueResponse.total_count); - - // loop through all the groupIds from issue Result - for (const groupId in issueResult) { - const groupIssuesObject = issueResult[groupId]; - const groupIssueResult = groupIssuesObject?.results; - - // if groupIssueResult is undefined then continue the loop - if (!groupIssueResult) continue; - - // set grouped Issue count of the current groupId - set(groupedIssueCount, [groupId], groupIssuesObject.total_results); - - // if groupIssueResult, the it is not subGrouped - if (Array.isArray(groupIssueResult)) { - // add the result to issueList - issueList.push(...groupIssueResult); - // set the issue Ids to the groupId path - set( - groupedIssues, - [groupId], - groupIssueResult.map((issue) => issue.id) - ); - continue; - } - - // loop through all the subGroupIds from issue Result - for (const subGroupId in groupIssueResult) { - const subGroupIssuesObject = groupIssueResult[subGroupId]; - const subGroupIssueResult = subGroupIssuesObject?.results; - - // if subGroupIssueResult is undefined then continue the loop - if (!subGroupIssueResult) continue; - - // set sub grouped Issue count of the current groupId - set(groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], subGroupIssuesObject.total_results); - - if (Array.isArray(subGroupIssueResult)) { - // add the result to issueList - issueList.push(...subGroupIssueResult); - // set the issue Ids to the [groupId, subGroupId] path - set( - groupedIssues, - [groupId, subGroupId], - subGroupIssueResult.map((issue) => issue.id) - ); - - continue; - } - } - } - - return { issueList, groupedIssues, groupedIssueCount }; - } - - /** - * This method is used to update the grouped issue Ids to it's respected lists and also to update group Issue Counts - * @param groupedIssues Object that contains list of issueIds with respect to their groups/subgroups - * @param groupedIssueCount Object the contains the issue count of each groups - * @param groupId groupId string - * @param subGroupId subGroupId string - * @returns updates the store with the values - */ - updateGroupedIssueIds( - groupedIssues: TIssues, - groupedIssueCount: TGroupedIssueCount, - groupId?: string, - subGroupId?: string - ) { - // if groupId exists and groupedIssues has ALL_ISSUES as a group, - // then it's an individual group/subgroup pagination - if (groupId && groupedIssues[ALL_ISSUES] && Array.isArray(groupedIssues[ALL_ISSUES])) { - const issueGroup = groupedIssues[ALL_ISSUES]; - const issueGroupCount = groupedIssueCount[ALL_ISSUES]; - const issuesPath = [groupId]; - // issuesPath is the path for the issue List in the Grouped Issue List - // issuePath is either [groupId] for grouped pagination or [groupId, subGroupId] for subGrouped pagination - if (subGroupId) issuesPath.push(subGroupId); - - // update the issue Count of the particular group/subGroup - set(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], issueGroupCount); - - // update the issue list in the issuePath - this.updateIssueGroup(issueGroup, issuesPath); - return; - } - - // if not in the above condition the it's a complete grouped pagination not individual group/subgroup pagination - // update total issue count as ALL_ISSUES count in `groupedIssueCount` object - set(this.groupedIssueCount, [ALL_ISSUES], groupedIssueCount[ALL_ISSUES]); - - // loop through the groups of groupedIssues. - for (const groupId in groupedIssues) { - const issueGroup = groupedIssues[groupId]; - const issueGroupCount = groupedIssueCount[groupId]; - - // update the groupId's issue count - set(this.groupedIssueCount, [groupId], issueGroupCount); - - // This updates the group issue list in the store, if the issueGroup is a string - const storeUpdated = this.updateIssueGroup(issueGroup, [groupId]); - // if issueGroup is indeed a string, continue - if (storeUpdated) continue; - - // if issueGroup is not a string, loop through the sub group Issues - for (const subGroupId in issueGroup) { - const issueSubGroup = (issueGroup as TGroupedIssues)[subGroupId]; - const issueSubGroupCount = groupedIssueCount[this.getGroupKey(groupId, subGroupId)]; - - // update the subGroupId's issue count - set(this.groupedIssueCount, [this.getGroupKey(groupId, subGroupId)], issueSubGroupCount); - // This updates the subgroup issue list in the store - this.updateIssueGroup(issueSubGroup, [groupId, subGroupId]); - } - } - } - - /** - * This Method is used to update the issue Id list at the particular issuePath - * @param groupedIssueIds could be an issue Id List for grouped issues or an object that contains a issue Id list in case of subGrouped - * @param issuePath array of string, to identify the path of the issueList to be updated with the above issue Id list - * @returns a boolean that indicates if the groupedIssueIds is indeed a array Id list, in which case the issue Id list is added to the store at issuePath - */ - updateIssueGroup(groupedIssueIds: TGroupedIssues | string[], issuePath: string[]): boolean { - if (!groupedIssueIds) return true; - - // if groupedIssueIds is an array, update the `groupedIssueIds` store at the issuePath - if (groupedIssueIds && Array.isArray(groupedIssueIds)) { - update(this, ["groupedIssueIds", ...issuePath], (issueIds: string[] = []) => - uniq(concat(issueIds, groupedIssueIds)) - ); - // return true to indicate the store has been updated - return true; - } - - // return false to indicate the store has been updated and the groupedIssueIds is likely Object for subGrouped Issues - return false; - } - - /** - * This method is used to update the count of the issues at the path with the increment - * @param path issuePath, corresponding key is to be incremented - * @param increment - */ - updateIssueCount(accumulatedUpdatesForCount: { [key: string]: EIssueGroupedAction }) { - const updateKeys = Object.keys(accumulatedUpdatesForCount); - for (const updateKey of updateKeys) { - const update = accumulatedUpdatesForCount[updateKey]; - if (!update) continue; - - const increment = update === EIssueGroupedAction.ADD ? 1 : -1; - // get current count at the key - const issueCount = get(this.groupedIssueCount, updateKey) ?? 0; - // update the count at the key - set(this.groupedIssueCount, updateKey, issueCount + increment); - } - } - - /** - * This Method is called to store the pagination options and paginated data from response - * @param issuesResponse issue list response - * @param options pagination options to be stored for next page call - * @param groupId - * @param subGroupId - */ - storePreviousPaginationValues = ( - issuesResponse: TIssuesResponse, - options?: IssuePaginationOptions, - groupId?: string, - subGroupId?: string - ) => { - if (options) this.paginationOptions = options; - - this.setPaginationData( - issuesResponse.prev_cursor, - issuesResponse.next_cursor, - issuesResponse.next_page_results, - groupId, - subGroupId - ); - }; - - /** - * returns, - * A compound key, if both groupId & subGroupId are defined - * groupId, only if groupId is defined - * ALL_ISSUES, if both groupId & subGroupId are not defined - * @param groupId - * @param subGroupId - * @returns - */ - getGroupKey = (groupId?: string, subGroupId?: string) => { - if (groupId && subGroupId && subGroupId !== "null") return `${groupId}_${subGroupId}`; - - if (groupId) return groupId; - - return ALL_ISSUES; - }; -} diff --git a/apps/space/store/helpers/filter.helpers.ts b/apps/space/store/helpers/filter.helpers.ts deleted file mode 100644 index 8a0e45464..000000000 --- a/apps/space/store/helpers/filter.helpers.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { EIssueGroupByToServerOptions, EServerGroupByToFilterOptions } from "@plane/constants"; -import type { IssuePaginationOptions, TIssueParams } from "@plane/types"; - -/** - * This Method is used to construct the url params along with paginated values - * @param filterParams params generated from filters - * @param options pagination options - * @param cursor cursor if exists - * @param groupId groupId if to fetch By group - * @param subGroupId groupId if to fetch By sub group - * @returns - */ -export const getPaginationParams = ( - filterParams: Partial> | undefined, - options: IssuePaginationOptions, - cursor: string | undefined, - groupId?: string, - subGroupId?: string -) => { - // if cursor exists, use the cursor. If it doesn't exist construct the cursor based on per page count - const pageCursor = cursor ? cursor : groupId ? `${options.perPageCount}:1:0` : `${options.perPageCount}:0:0`; - - // pagination params - const paginationParams: Partial> = { - ...filterParams, - cursor: pageCursor, - per_page: options.perPageCount.toString(), - }; - - // If group by is specifically sent through options, like that for calendar layout, use that to group - if (options.groupedBy) { - paginationParams.group_by = EIssueGroupByToServerOptions[options.groupedBy]; - } - - // If group by is specifically sent through options, like that for calendar layout, use that to group - if (options.subGroupedBy) { - paginationParams.sub_group_by = EIssueGroupByToServerOptions[options.subGroupedBy]; - } - - // If group by is specifically sent through options, like that for calendar layout, use that to group - if (options.orderBy) { - paginationParams.order_by = options.orderBy; - } - - // If before and after dates are sent from option to filter by then, add them to filter the options - if (options.after && options.before) { - paginationParams["target_date"] = `${options.after};after,${options.before};before`; - } - - // If groupId is passed down, add a filter param for that group Id - if (groupId) { - const groupBy = paginationParams["group_by"] as EIssueGroupByToServerOptions | undefined; - delete paginationParams["group_by"]; - - if (groupBy) { - const groupByFilterOption = EServerGroupByToFilterOptions[groupBy]; - paginationParams[groupByFilterOption] = groupId; - } - } - - // If subGroupId is passed down, add a filter param for that subGroup Id - if (subGroupId) { - const subGroupBy = paginationParams["sub_group_by"] as EIssueGroupByToServerOptions | undefined; - delete paginationParams["sub_group_by"]; - - if (subGroupBy) { - const subGroupByFilterOption = EServerGroupByToFilterOptions[subGroupBy]; - paginationParams[subGroupByFilterOption] = subGroupId; - } - } - - return paginationParams; -}; diff --git a/apps/space/store/instance.store.ts b/apps/space/store/instance.store.ts deleted file mode 100644 index 37dc9318e..000000000 --- a/apps/space/store/instance.store.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { set } from "lodash-es"; -import { observable, action, makeObservable, runInAction } from "mobx"; -// plane imports -import { InstanceService } from "@plane/services"; -import type { IInstance, IInstanceConfig } from "@plane/types"; -// store -import type { RootStore } from "@/store/root.store"; - -type TError = { - status: string; - message: string; - data?: { - is_activated: boolean; - is_setup_done: boolean; - }; -}; - -export interface IInstanceStore { - // observables - isLoading: boolean; - instance: IInstance | undefined; - config: IInstanceConfig | undefined; - error: TError | undefined; - // action - fetchInstanceInfo: () => Promise; - hydrate: (data: IInstance) => void; -} - -export class InstanceStore implements IInstanceStore { - isLoading: boolean = true; - instance: IInstance | undefined = undefined; - config: IInstanceConfig | undefined = undefined; - error: TError | undefined = undefined; - // services - instanceService; - - constructor(private store: RootStore) { - makeObservable(this, { - // observable - isLoading: observable.ref, - instance: observable, - config: observable, - error: observable, - // actions - fetchInstanceInfo: action, - hydrate: action, - }); - // services - this.instanceService = new InstanceService(); - } - - hydrate = (data: IInstance) => set(this, "instance", data); - - /** - * @description fetching instance information - */ - fetchInstanceInfo = async () => { - try { - this.isLoading = true; - this.error = undefined; - const instanceInfo = await this.instanceService.info(); - runInAction(() => { - this.isLoading = false; - this.instance = instanceInfo.instance; - this.config = instanceInfo.config; - }); - } catch (_error) { - runInAction(() => { - this.isLoading = false; - this.error = { - status: "error", - message: "Failed to fetch instance info", - }; - }); - } - }; -} diff --git a/apps/space/store/issue-detail.store.ts b/apps/space/store/issue-detail.store.ts deleted file mode 100644 index 407a25b49..000000000 --- a/apps/space/store/issue-detail.store.ts +++ /dev/null @@ -1,447 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { isEmpty, set } from "lodash-es"; -import { makeObservable, observable, action, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; -import { v4 as uuidv4 } from "uuid"; -// plane imports -import { SitesFileService, SitesIssueService } from "@plane/services"; -import type { TFileSignedURLResponse, TIssuePublicComment } from "@plane/types"; -import { EFileAssetType } from "@plane/types"; -// store -import type { RootStore } from "@/store/root.store"; -// types -import type { IIssue, IPeekMode, IVote } from "@/types/issue"; - -export interface IIssueDetailStore { - loader: boolean; - error: any; - // observables - peekId: string | null; - peekMode: IPeekMode; - details: { - [key: string]: IIssue; - }; - // computed actions - getIsIssuePeeked: (issueID: string) => boolean; - // actions - getIssueById: (issueId: string) => IIssue | undefined; - setPeekId: (issueID: string | null) => void; - setPeekMode: (mode: IPeekMode) => void; - // issue actions - fetchIssueDetails: (anchor: string, issueID: string) => void; - // comment actions - addIssueComment: (anchor: string, issueID: string, data: any) => Promise; - updateIssueComment: (anchor: string, issueID: string, commentID: string, data: any) => Promise; - deleteIssueComment: (anchor: string, issueID: string, commentID: string) => void; - uploadCommentAsset: (file: File, anchor: string, commentID?: string) => Promise; - uploadIssueAsset: (file: File, anchor: string, commentID?: string) => Promise; - addCommentReaction: (anchor: string, issueID: string, commentID: string, reactionHex: string) => void; - removeCommentReaction: (anchor: string, issueID: string, commentID: string, reactionHex: string) => void; - // reaction actions - addIssueReaction: (anchor: string, issueID: string, reactionHex: string) => void; - removeIssueReaction: (anchor: string, issueID: string, reactionHex: string) => void; - // vote actions - addIssueVote: (anchor: string, issueID: string, data: { vote: 1 | -1 }) => Promise; - removeIssueVote: (anchor: string, issueID: string) => Promise; -} - -export class IssueDetailStore implements IIssueDetailStore { - loader: boolean = false; - error: any = null; - // observables - peekId: string | null = null; - peekMode: IPeekMode = "side"; - details: { - [key: string]: IIssue; - } = {}; - // root store - rootStore: RootStore; - // services - issueService: SitesIssueService; - fileService: SitesFileService; - - constructor(_rootStore: RootStore) { - makeObservable(this, { - loader: observable.ref, - error: observable.ref, - // observables - peekId: observable.ref, - peekMode: observable.ref, - details: observable, - // actions - setPeekId: action, - setPeekMode: action, - // issue actions - fetchIssueDetails: action, - // comment actions - addIssueComment: action, - updateIssueComment: action, - deleteIssueComment: action, - uploadCommentAsset: action, - uploadIssueAsset: action, - addCommentReaction: action, - removeCommentReaction: action, - // reaction actions - addIssueReaction: action, - removeIssueReaction: action, - // vote actions - addIssueVote: action, - removeIssueVote: action, - }); - this.rootStore = _rootStore; - this.issueService = new SitesIssueService(); - this.fileService = new SitesFileService(); - } - - setPeekId = (issueID: string | null) => { - this.peekId = issueID; - }; - - setPeekMode = (mode: IPeekMode) => { - this.peekMode = mode; - }; - - getIsIssuePeeked = (issueID: string) => this.peekId === issueID; - - /** - * @description This method will return the issue from the issuesMap - * @param {string} issueId - * @returns {IIssue | undefined} - */ - getIssueById = computedFn((issueId: string) => { - if (!issueId || isEmpty(this.details) || !this.details[issueId]) return undefined; - return this.details[issueId]; - }); - - /** - * Retrieves issue from API - * @param anchorId ] - * @param issueId - * @returns - */ - fetchIssueById = async (anchorId: string, issueId: string) => { - try { - const issueDetails = await this.issueService.retrieve(anchorId, issueId); - - runInAction(() => { - set(this.details, [issueId], issueDetails); - }); - - return issueDetails; - } catch (e) { - console.error(`Error fetching issue details for issueId ${issueId}: `, e); - } - }; - - /** - * @description fetc - * @param {string} anchor - * @param {string} issueID - */ - fetchIssueDetails = async (anchor: string, issueID: string) => { - try { - this.loader = true; - this.error = null; - - const issueDetails = await this.fetchIssueById(anchor, issueID); - const commentsResponse = await this.issueService.listComments(anchor, issueID); - - if (issueDetails) { - runInAction(() => { - this.details = { - ...this.details, - [issueID]: { - ...(this.details[issueID] ?? issueDetails), - comments: commentsResponse, - }, - }; - }); - } - } catch (error) { - this.loader = false; - this.error = error; - } - }; - - addIssueComment = async (anchor: string, issueID: string, data: any) => { - try { - const issueDetails = this.getIssueById(issueID); - const issueCommentResponse = await this.issueService.addComment(anchor, issueID, data); - if (issueDetails) { - runInAction(() => { - set(this.details, [issueID, "comments"], [...(issueDetails?.comments ?? []), issueCommentResponse]); - }); - } - return issueCommentResponse; - } catch (error) { - console.log("Failed to add issue comment"); - throw error; - } - }; - - updateIssueComment = async (anchor: string, issueID: string, commentID: string, data: any) => { - try { - runInAction(() => { - this.details = { - ...this.details, - [issueID]: { - ...this.details[issueID], - comments: this.details[issueID].comments.map((c) => ({ - ...c, - ...(c.id === commentID ? data : {}), - })), - }, - }; - }); - - await this.issueService.updateComment(anchor, issueID, commentID, data); - } catch (_error) { - const issueComments = await this.issueService.listComments(anchor, issueID); - - runInAction(() => { - this.details = { - ...this.details, - [issueID]: { - ...this.details[issueID], - comments: issueComments, - }, - }; - }); - } - }; - - deleteIssueComment = async (anchor: string, issueID: string, commentID: string) => { - try { - await this.issueService.removeComment(anchor, issueID, commentID); - const remainingComments = this.details[issueID].comments.filter((c) => c.id != commentID); - runInAction(() => { - this.details = { - ...this.details, - [issueID]: { - ...this.details[issueID], - comments: remainingComments, - }, - }; - }); - } catch (_error) { - console.log("Failed to add issue vote"); - } - }; - - uploadCommentAsset = async (file: File, anchor: string, commentID?: string) => { - try { - const res = await this.fileService.uploadAsset( - anchor, - { - entity_identifier: commentID ?? "", - entity_type: EFileAssetType.COMMENT_DESCRIPTION, - }, - file - ); - return res; - } catch (error) { - console.log("Error in uploading comment asset:", error); - throw new Error("Asset upload failed. Please try again later.", { cause: error }); - } - }; - - uploadIssueAsset = async (file: File, anchor: string, commentID?: string) => { - try { - const res = await this.fileService.uploadAsset( - anchor, - { - entity_identifier: commentID ?? "", - entity_type: EFileAssetType.ISSUE_DESCRIPTION, - }, - file - ); - return res; - } catch (error) { - console.log("Error in uploading comment asset:", error); - throw new Error("Asset upload failed. Please try again later.", { cause: error }); - } - }; - - addCommentReaction = async (anchor: string, issueID: string, commentID: string, reactionHex: string) => { - const newReaction = { - id: uuidv4(), - comment: commentID, - reaction: reactionHex, - actor_detail: this.rootStore.user.currentActor, - }; - const newComments = this.details[issueID].comments.map((comment) => ({ - ...comment, - comment_reactions: - comment.id === commentID ? [...comment.comment_reactions, newReaction] : comment.comment_reactions, - })); - - try { - runInAction(() => { - this.details = { - ...this.details, - [issueID]: { - ...this.details[issueID], - comments: [...newComments], - }, - }; - }); - - await this.issueService.addCommentReaction(anchor, commentID, { - reaction: reactionHex, - }); - } catch (_error) { - const issueComments = await this.issueService.listComments(anchor, issueID); - - runInAction(() => { - this.details = { - ...this.details, - [issueID]: { - ...this.details[issueID], - comments: issueComments, - }, - }; - }); - } - }; - - removeCommentReaction = async (anchor: string, issueID: string, commentID: string, reactionHex: string) => { - try { - const comment = this.details[issueID].comments.find((c) => c.id === commentID); - const newCommentReactions = comment?.comment_reactions.filter((r) => r.reaction !== reactionHex) ?? []; - - runInAction(() => { - this.details = { - ...this.details, - [issueID]: { - ...this.details[issueID], - comments: this.details[issueID].comments.map((c) => ({ - ...c, - comment_reactions: c.id === commentID ? newCommentReactions : c.comment_reactions, - })), - }, - }; - }); - - await this.issueService.removeCommentReaction(anchor, commentID, reactionHex); - } catch (_error) { - const issueComments = await this.issueService.listComments(anchor, issueID); - - runInAction(() => { - this.details = { - ...this.details, - [issueID]: { - ...this.details[issueID], - comments: issueComments, - }, - }; - }); - } - }; - - addIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => { - try { - runInAction(() => { - set( - this.details, - [issueID, "reaction_items"], - [ - ...this.details[issueID].reaction_items, - { - reaction: reactionHex, - actor_details: this.rootStore.user.currentActor, - }, - ] - ); - }); - - await this.issueService.addReaction(anchor, issueID, { - reaction: reactionHex, - }); - } catch (_error) { - console.log("Failed to add issue vote"); - const issueReactions = await this.issueService.listReactions(anchor, issueID); - runInAction(() => { - set(this.details, [issueID, "reaction_items"], issueReactions); - }); - } - }; - - removeIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => { - try { - const newReactions = this.details[issueID].reaction_items.filter( - (_r) => !(_r.reaction === reactionHex && _r.actor_details.id === this.rootStore.user.data?.id) - ); - - runInAction(() => { - set(this.details, [issueID, "reaction_items"], newReactions); - }); - - await this.issueService.removeReaction(anchor, issueID, reactionHex); - } catch (_error) { - console.log("Failed to remove issue reaction"); - const reactions = await this.issueService.listReactions(anchor, issueID); - runInAction(() => { - set(this.details, [issueID, "reaction_items"], reactions); - }); - } - }; - - addIssueVote = async (anchor: string, issueID: string, data: { vote: 1 | -1 }) => { - const publishSettings = this.rootStore.publishList?.publishMap?.[anchor]; - const projectID = publishSettings?.project; - const workspaceSlug = publishSettings?.workspace_detail?.slug; - if (!projectID || !workspaceSlug) throw new Error("Publish settings not found"); - - const newVote: IVote = { - actor_details: this.rootStore.user.currentActor, - vote: data.vote, - }; - - const filteredVotes = this.details[issueID].vote_items.filter( - (v) => v.actor_details?.id !== this.rootStore.user.data?.id - ); - - try { - runInAction(() => { - runInAction(() => { - set(this.details, [issueID, "vote_items"], [...filteredVotes, newVote]); - }); - }); - - await this.issueService.addVote(anchor, issueID, data); - } catch (_error) { - console.log("Failed to add issue vote"); - const issueVotes = await this.issueService.listVotes(anchor, issueID); - - runInAction(() => { - set(this.details, [issueID, "vote_items"], issueVotes); - }); - } - }; - - removeIssueVote = async (anchor: string, issueID: string) => { - const newVotes = this.details[issueID].vote_items.filter( - (v) => v.actor_details?.id !== this.rootStore.user.data?.id - ); - - try { - runInAction(() => { - set(this.details, [issueID, "vote_items"], newVotes); - }); - - await this.issueService.removeVote(anchor, issueID); - } catch (_error) { - console.log("Failed to remove issue vote"); - const issueVotes = await this.issueService.listVotes(anchor, issueID); - - runInAction(() => { - set(this.details, [issueID, "vote_items"], issueVotes); - }); - } - }; -} diff --git a/apps/space/store/issue-filters.store.ts b/apps/space/store/issue-filters.store.ts deleted file mode 100644 index e2cb514de..000000000 --- a/apps/space/store/issue-filters.store.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { cloneDeep, isEqual, set } from "lodash-es"; -import { action, makeObservable, observable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; -// plane internal -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@plane/constants"; -import type { IssuePaginationOptions, TIssueParams } from "@plane/types"; -// store -import type { RootStore } from "@/store/root.store"; -// types -import type { - TIssueLayoutOptions, - TIssueFilters, - TIssueQueryFilters, - TIssueQueryFiltersParams, - TIssueFilterKeys, -} from "@/types/issue"; -import { getPaginationParams } from "./helpers/filter.helpers"; - -export interface IIssueFilterStore { - // observables - layoutOptions: TIssueLayoutOptions; - filters: { [anchor: string]: TIssueFilters } | undefined; - // computed - isIssueFiltersUpdated: (anchor: string, filters: TIssueFilters) => boolean; - // helpers - getIssueFilters: (anchor: string) => TIssueFilters | undefined; - getAppliedFilters: (anchor: string) => TIssueQueryFiltersParams | undefined; - // actions - updateLayoutOptions: (layout: TIssueLayoutOptions) => void; - initIssueFilters: (anchor: string, filters: TIssueFilters, shouldFetchIssues?: boolean) => void; - updateIssueFilters: ( - anchor: string, - filterKind: K, - filterKey: keyof TIssueFilters[K], - filters: TIssueFilters[K][typeof filterKey] - ) => Promise; - getFilterParams: ( - options: IssuePaginationOptions, - anchor: string, - cursor: string | undefined, - groupId: string | undefined, - subGroupId: string | undefined - ) => Partial>; -} - -export class IssueFilterStore implements IIssueFilterStore { - // observables - layoutOptions: TIssueLayoutOptions = { - list: true, - kanban: false, - calendar: false, - gantt: false, - spreadsheet: false, - }; - filters: { [anchor: string]: TIssueFilters } | undefined = undefined; - - constructor(private store: RootStore) { - makeObservable(this, { - // observables - layoutOptions: observable, - filters: observable, - // actions - updateLayoutOptions: action, - initIssueFilters: action, - updateIssueFilters: action, - }); - } - - // helper methods - computedFilter = (filters: TIssueQueryFilters, filteredParams: TIssueFilterKeys[]) => { - const computedFilters: TIssueQueryFiltersParams = {}; - - Object.keys(filters).map((key) => { - const currentFilterKey = key as TIssueFilterKeys; - const filterValue = filters[currentFilterKey] as any; - - if (filterValue !== undefined && filteredParams.includes(currentFilterKey)) { - if (Array.isArray(filterValue)) computedFilters[currentFilterKey] = filterValue.join(","); - else if (typeof filterValue === "string" || typeof filterValue === "boolean") - computedFilters[currentFilterKey] = filterValue.toString(); - } - }); - - return computedFilters; - }; - - // computed - getIssueFilters = computedFn((anchor: string) => { - const currentFilters = this.filters?.[anchor]; - return currentFilters; - }); - - getAppliedFilters = computedFn((anchor: string) => { - const issueFilters = this.getIssueFilters(anchor); - if (!issueFilters) return undefined; - - const currentLayout = issueFilters?.display_filters?.layout; - if (!currentLayout) return undefined; - - const currentFilters: TIssueQueryFilters = { - priority: issueFilters?.filters?.priority || undefined, - state: issueFilters?.filters?.state || undefined, - labels: issueFilters?.filters?.labels || undefined, - }; - const filteredParams = ISSUE_DISPLAY_FILTERS_BY_LAYOUT?.[currentLayout]?.filters || []; - const currentFilterQueryParams: TIssueQueryFiltersParams = this.computedFilter(currentFilters, filteredParams); - - return currentFilterQueryParams; - }); - - isIssueFiltersUpdated = computedFn((anchor: string, userFilters: TIssueFilters) => { - const issueFilters = this.getIssueFilters(anchor); - if (!issueFilters) return false; - const currentUserFilters = cloneDeep(userFilters?.filters || {}); - const currentIssueFilters = cloneDeep(issueFilters?.filters || {}); - return isEqual(currentUserFilters, currentIssueFilters); - }); - - // actions - updateLayoutOptions = (options: TIssueLayoutOptions) => set(this, ["layoutOptions"], options); - - initIssueFilters = async (anchor: string, initFilters: TIssueFilters, shouldFetchIssues: boolean = false) => { - if (this.filters === undefined) runInAction(() => (this.filters = {})); - if (this.filters && initFilters) set(this.filters, [anchor], initFilters); - - if (shouldFetchIssues) await this.store.issue.fetchPublicIssuesWithExistingPagination(anchor, "mutation"); - }; - - getFilterParams = computedFn( - ( - options: IssuePaginationOptions, - anchor: string, - cursor: string | undefined, - groupId: string | undefined, - subGroupId: string | undefined - ) => { - const filterParams = this.getAppliedFilters(anchor); - const paginationParams = getPaginationParams(filterParams, options, cursor, groupId, subGroupId); - return paginationParams; - } - ); - - updateIssueFilters = async ( - anchor: string, - filterKind: K, - filterKey: keyof TIssueFilters[K], - filterValue: TIssueFilters[K][typeof filterKey] - ) => { - if (!filterKind || !filterKey || !filterValue) return; - if (this.filters === undefined) runInAction(() => (this.filters = {})); - - runInAction(() => { - if (this.filters) set(this.filters, [anchor, filterKind, filterKey], filterValue); - }); - - if (filterKey !== "layout") await this.store.issue.fetchPublicIssuesWithExistingPagination(anchor, "mutation"); - }; -} diff --git a/apps/space/store/issue.store.ts b/apps/space/store/issue.store.ts deleted file mode 100644 index 93556bc80..000000000 --- a/apps/space/store/issue.store.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { action, makeObservable, runInAction } from "mobx"; -// plane imports -import { SitesIssueService } from "@plane/services"; -import type { IssuePaginationOptions, TLoader } from "@plane/types"; -// store -import type { RootStore } from "@/store/root.store"; -// types -import { BaseIssuesStore } from "./helpers/base-issues.store"; -import type { IBaseIssuesStore } from "./helpers/base-issues.store"; - -export interface IIssueStore extends IBaseIssuesStore { - // actions - fetchPublicIssues: ( - anchor: string, - loadType: TLoader, - options: IssuePaginationOptions, - isExistingPaginationOptions?: boolean - ) => Promise; - fetchNextPublicIssues: (anchor: string, groupId?: string, subGroupId?: string) => Promise; - fetchPublicIssuesWithExistingPagination: (anchor: string, loadType?: TLoader) => Promise; -} - -export class IssueStore extends BaseIssuesStore implements IIssueStore { - // root store - rootStore: RootStore; - // services - issueService: SitesIssueService; - - constructor(_rootStore: RootStore) { - super(_rootStore); - makeObservable(this, { - // actions - fetchPublicIssues: action, - fetchNextPublicIssues: action, - fetchPublicIssuesWithExistingPagination: action, - }); - - this.rootStore = _rootStore; - this.issueService = new SitesIssueService(); - } - - /** - * @description fetch issues, states and labels - * @param {string} anchor - * @param params - */ - fetchPublicIssues = async ( - anchor: string, - loadType: TLoader = "init-loader", - options: IssuePaginationOptions, - isExistingPaginationOptions: boolean = false - ) => { - try { - // set loader and clear store - runInAction(() => { - this.setLoader(loadType); - }); - this.clear(!isExistingPaginationOptions); - - const params = this.rootStore.issueFilter.getFilterParams(options, anchor, undefined, undefined, undefined); - - const response = await this.issueService.list(anchor, params); - - // after fetching issues, call the base method to process the response further - this.onfetchIssues(response, options); - } catch (error) { - this.setLoader(undefined); - throw error; - } - }; - - fetchNextPublicIssues = async (anchor: string, groupId?: string, subGroupId?: string) => { - const cursorObject = this.getPaginationData(groupId, subGroupId); - // if there are no pagination options and the next page results do not exist the return - if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return; - try { - // set Loader - this.setLoader("pagination", groupId, subGroupId); - - // get params from stored pagination options - const params = this.rootStore.issueFilter.getFilterParams( - this.paginationOptions, - anchor, - cursorObject?.nextCursor, - groupId, - subGroupId - ); - // call the fetch issues API with the params for next page in issues - const response = await this.issueService.list(anchor, params); - - // after the next page of issues are fetched, call the base method to process the response - this.onfetchNexIssues(response, groupId, subGroupId); - } catch (error) { - // set Loader as undefined if errored out - this.setLoader(undefined, groupId, subGroupId); - throw error; - } - }; - - /** - * This Method exists to fetch the first page of the issues with the existing stored pagination - * This is useful for refetching when filters, groupBy, orderBy etc changes - * @param workspaceSlug - * @param projectId - * @param loadType - * @returns - */ - fetchPublicIssuesWithExistingPagination = async (anchor: string, loadType: TLoader = "mutation") => { - if (!this.paginationOptions) return; - return await this.fetchPublicIssues(anchor, loadType, this.paginationOptions, true); - }; -} diff --git a/apps/space/store/label.store.ts b/apps/space/store/label.store.ts deleted file mode 100644 index b1c229f0a..000000000 --- a/apps/space/store/label.store.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { set } from "lodash-es"; -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -// plane imports -import { SitesLabelService } from "@plane/services"; -import type { IIssueLabel } from "@plane/types"; -// store -import type { RootStore } from "./root.store"; - -export interface IIssueLabelStore { - // observables - labels: IIssueLabel[] | undefined; - // computed actions - getLabelById: (labelId: string | undefined) => IIssueLabel | undefined; - getLabelsByIds: (labelIds: string[]) => IIssueLabel[]; - // fetch actions - fetchLabels: (anchor: string) => Promise; -} - -export class LabelStore implements IIssueLabelStore { - labelMap: Record = {}; - labelService: SitesLabelService; - rootStore: RootStore; - - constructor(_rootStore: RootStore) { - makeObservable(this, { - // observables - labelMap: observable, - // computed - labels: computed, - // fetch action - fetchLabels: action, - }); - this.labelService = new SitesLabelService(); - this.rootStore = _rootStore; - } - - get labels() { - return Object.values(this.labelMap); - } - - getLabelById = (labelId: string | undefined) => (labelId ? this.labelMap[labelId] : undefined); - - getLabelsByIds = (labelIds: string[]) => { - const currLabels = []; - for (const labelId of labelIds) { - const label = this.getLabelById(labelId); - if (label) { - currLabels.push(label); - } - } - - return currLabels; - }; - - fetchLabels = async (anchor: string) => { - const labelsResponse = await this.labelService.list(anchor); - runInAction(() => { - this.labelMap = {}; - for (const label of labelsResponse) { - set(this.labelMap, [label.id], label); - } - }); - return labelsResponse; - }; -} diff --git a/apps/space/store/members.store.ts b/apps/space/store/members.store.ts deleted file mode 100644 index a06d00550..000000000 --- a/apps/space/store/members.store.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { set } from "lodash-es"; -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -// plane imports -import { SitesMemberService } from "@plane/services"; -import type { TPublicMember } from "@/types/member"; -import type { RootStore } from "./root.store"; - -export interface IIssueMemberStore { - // observables - members: TPublicMember[] | undefined; - // computed actions - getMemberById: (memberId: string | undefined) => TPublicMember | undefined; - getMembersByIds: (memberIds: string[]) => TPublicMember[]; - // fetch actions - fetchMembers: (anchor: string) => Promise; -} - -export class MemberStore implements IIssueMemberStore { - memberMap: Record = {}; - memberService: SitesMemberService; - rootStore: RootStore; - - constructor(_rootStore: RootStore) { - makeObservable(this, { - // observables - memberMap: observable, - // computed - members: computed, - // fetch action - fetchMembers: action, - }); - this.memberService = new SitesMemberService(); - this.rootStore = _rootStore; - } - - get members() { - return Object.values(this.memberMap); - } - - getMemberById = (memberId: string | undefined) => (memberId ? this.memberMap[memberId] : undefined); - - getMembersByIds = (memberIds: string[]) => { - const currMembers = []; - for (const memberId of memberIds) { - const member = this.getMemberById(memberId); - if (member) { - currMembers.push(member); - } - } - - return currMembers; - }; - - fetchMembers = async (anchor: string) => { - try { - const membersResponse = await this.memberService.list(anchor); - runInAction(() => { - this.memberMap = {}; - for (const member of membersResponse) { - set(this.memberMap, [member.member], member); - } - }); - return membersResponse; - } catch (error) { - console.error("Failed to fetch members:", error); - return []; - } - }; -} diff --git a/apps/space/store/module.store.ts b/apps/space/store/module.store.ts deleted file mode 100644 index c539846ef..000000000 --- a/apps/space/store/module.store.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { set } from "lodash-es"; -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -// plane imports -import { SitesModuleService } from "@plane/services"; -// types -import type { TPublicModule } from "@/types/modules"; -// root store -import type { RootStore } from "./root.store"; - -export interface IIssueModuleStore { - // observables - modules: TPublicModule[] | undefined; - // computed actions - getModuleById: (moduleId: string | undefined) => TPublicModule | undefined; - getModulesByIds: (moduleIds: string[]) => TPublicModule[]; - // fetch actions - fetchModules: (anchor: string) => Promise; -} - -export class ModuleStore implements IIssueModuleStore { - moduleMap: Record = {}; - moduleService: SitesModuleService; - rootStore: RootStore; - - constructor(_rootStore: RootStore) { - makeObservable(this, { - // observables - moduleMap: observable, - // computed - modules: computed, - // fetch action - fetchModules: action, - }); - this.moduleService = new SitesModuleService(); - this.rootStore = _rootStore; - } - - get modules() { - return Object.values(this.moduleMap); - } - - getModuleById = (moduleId: string | undefined) => (moduleId ? this.moduleMap[moduleId] : undefined); - - getModulesByIds = (moduleIds: string[]) => { - const currModules = []; - for (const moduleId of moduleIds) { - const issueModule = this.getModuleById(moduleId); - if (issueModule) { - currModules.push(issueModule); - } - } - - return currModules; - }; - - fetchModules = async (anchor: string) => { - try { - const modulesResponse = await this.moduleService.list(anchor); - runInAction(() => { - this.moduleMap = {}; - for (const issueModule of modulesResponse) { - set(this.moduleMap, [issueModule.id], issueModule); - } - }); - return modulesResponse; - } catch (error) { - console.error("Failed to fetch members:", error); - return []; - } - }; -} diff --git a/apps/space/store/profile.store.ts b/apps/space/store/profile.store.ts deleted file mode 100644 index d62d1d37a..000000000 --- a/apps/space/store/profile.store.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { set } from "lodash-es"; -import { action, makeObservable, observable, runInAction } from "mobx"; -// plane imports -import { UserService } from "@plane/services"; -import type { TUserProfile } from "@plane/types"; -import { EStartOfTheWeek } from "@plane/types"; -// store -import type { RootStore } from "@/store/root.store"; - -type TError = { - status: string; - message: string; -}; - -export interface IProfileStore { - // observables - isLoading: boolean; - error: TError | undefined; - data: TUserProfile; - // actions - fetchUserProfile: () => Promise; - updateUserProfile: (data: Partial) => Promise; -} - -export class ProfileStore implements IProfileStore { - isLoading: boolean = false; - error: TError | undefined = undefined; - data: TUserProfile = { - id: undefined, - user: undefined, - role: undefined, - last_workspace_id: undefined, - theme: { - theme: undefined, - primary: undefined, - background: undefined, - darkPalette: undefined, - }, - onboarding_step: { - workspace_join: false, - profile_complete: false, - workspace_create: false, - workspace_invite: false, - }, - is_onboarded: false, - is_tour_completed: false, - use_case: undefined, - billing_address_country: undefined, - billing_address: undefined, - has_billing_address: false, - has_marketing_email_consent: false, - created_at: "", - updated_at: "", - language: "", - start_of_the_week: EStartOfTheWeek.SUNDAY, - }; - - // services - userService: UserService; - - constructor(public store: RootStore) { - makeObservable(this, { - // observables - isLoading: observable.ref, - error: observable, - data: observable, - // actions - fetchUserProfile: action, - updateUserProfile: action, - }); - // services - this.userService = new UserService(); - } - - // actions - /** - * @description fetches user profile information - * @returns {Promise} - */ - fetchUserProfile = async () => { - try { - runInAction(() => { - this.isLoading = true; - this.error = undefined; - }); - const userProfile = await this.userService.profile(); - runInAction(() => { - this.isLoading = false; - this.data = userProfile; - }); - return userProfile; - } catch (_error) { - runInAction(() => { - this.isLoading = false; - this.error = { - status: "user-profile-fetch-error", - message: "Failed to fetch user profile", - }; - }); - } - }; - - /** - * @description updated the user profile information - * @param {Partial} data - * @returns {Promise} - */ - updateUserProfile = async (data: Partial) => { - const currentUserProfileData = this.data; - try { - if (currentUserProfileData) { - Object.keys(data).forEach((key: string) => { - const userKey: keyof TUserProfile = key as keyof TUserProfile; - if (this.data) set(this.data, userKey, data[userKey]); - }); - } - const userProfile = await this.userService.updateProfile(data); - return userProfile; - } catch (_error) { - if (currentUserProfileData) { - Object.keys(currentUserProfileData).forEach((key: string) => { - const userKey: keyof TUserProfile = key as keyof TUserProfile; - if (this.data) set(this.data, userKey, currentUserProfileData[userKey]); - }); - } - runInAction(() => { - this.error = { - status: "user-profile-update-error", - message: "Failed to update user profile", - }; - }); - } - }; -} diff --git a/apps/space/store/publish/publish.store.ts b/apps/space/store/publish/publish.store.ts deleted file mode 100644 index 973ea86e5..000000000 --- a/apps/space/store/publish/publish.store.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observable, makeObservable, computed } from "mobx"; -// types -import type { - IWorkspaceLite, - TProjectDetails, - TPublishEntityType, - TProjectPublishSettings, - TProjectPublishViewProps, -} from "@plane/types"; -// store -import type { RootStore } from "../root.store"; - -export interface IPublishStore extends TProjectPublishSettings { - // computed - workspaceSlug: string | undefined; - canComment: boolean; - canReact: boolean; - canVote: boolean; -} - -export class PublishStore implements IPublishStore { - // observables - anchor: string | undefined; - is_comments_enabled: boolean; - created_at: string | undefined; - created_by: string | undefined; - entity_identifier: string | undefined; - entity_name: TPublishEntityType | undefined; - id: string | undefined; - inbox: unknown; - project: string | undefined; - project_details: TProjectDetails | undefined; - is_reactions_enabled: boolean; - updated_at: string | undefined; - updated_by: string | undefined; - view_props: TProjectPublishViewProps | undefined; - is_votes_enabled: boolean; - workspace: string | undefined; - workspace_detail: IWorkspaceLite | undefined; - - constructor( - private store: RootStore, - publishSettings: TProjectPublishSettings - ) { - this.anchor = publishSettings.anchor; - this.is_comments_enabled = publishSettings.is_comments_enabled; - this.created_at = publishSettings.created_at; - this.created_by = publishSettings.created_by; - this.entity_identifier = publishSettings.entity_identifier; - this.entity_name = publishSettings.entity_name; - this.id = publishSettings.id; - this.inbox = publishSettings.inbox; - this.project = publishSettings.project; - this.project_details = publishSettings.project_details; - this.is_reactions_enabled = publishSettings.is_reactions_enabled; - this.updated_at = publishSettings.updated_at; - this.updated_by = publishSettings.updated_by; - this.view_props = publishSettings.view_props; - this.is_votes_enabled = publishSettings.is_votes_enabled; - this.workspace = publishSettings.workspace; - this.workspace_detail = publishSettings.workspace_detail; - - makeObservable(this, { - // observables - anchor: observable.ref, - is_comments_enabled: observable.ref, - created_at: observable.ref, - created_by: observable.ref, - entity_identifier: observable.ref, - entity_name: observable.ref, - id: observable.ref, - inbox: observable, - project: observable.ref, - project_details: observable, - is_reactions_enabled: observable.ref, - updated_at: observable.ref, - updated_by: observable.ref, - view_props: observable, - is_votes_enabled: observable.ref, - workspace: observable.ref, - workspace_detail: observable, - // computed - workspaceSlug: computed, - canComment: computed, - canReact: computed, - canVote: computed, - }); - } - - /** - * @description returns the workspace slug from the workspace details - */ - get workspaceSlug() { - return this?.workspace_detail?.slug ?? undefined; - } - - /** - * @description returns whether commenting is enabled or not - */ - get canComment() { - return !!this.is_comments_enabled; - } - - /** - * @description returns whether reacting is enabled or not - */ - get canReact() { - return !!this.is_reactions_enabled; - } - - /** - * @description returns whether voting is enabled or not - */ - get canVote() { - return !!this.is_votes_enabled; - } -} diff --git a/apps/space/store/publish/publish_list.store.ts b/apps/space/store/publish/publish_list.store.ts deleted file mode 100644 index eeebe97e7..000000000 --- a/apps/space/store/publish/publish_list.store.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { set } from "lodash-es"; -import { makeObservable, observable, runInAction, action } from "mobx"; -// plane imports -import { SitesProjectPublishService } from "@plane/services"; -import type { TProjectPublishSettings } from "@plane/types"; -// store -import { PublishStore } from "@/store/publish/publish.store"; -import type { RootStore } from "@/store/root.store"; - -export interface IPublishListStore { - // observables - publishMap: Record; // anchor => PublishStore - // actions - fetchPublishSettings: (pageId: string) => Promise; -} - -export class PublishListStore implements IPublishListStore { - // observables - publishMap: Record = {}; // anchor => PublishStore - // service - publishService; - - constructor(private rootStore: RootStore) { - makeObservable(this, { - // observables - publishMap: observable, - // actions - fetchPublishSettings: action, - }); - // services - this.publishService = new SitesProjectPublishService(); - } - - /** - * @description fetch publish settings - * @param {string} anchor - */ - fetchPublishSettings = async (anchor: string) => { - const response = await this.publishService.retrieveSettingsByAnchor(anchor); - runInAction(() => { - if (response.anchor) { - set(this.publishMap, [response.anchor], new PublishStore(this.rootStore, response)); - } - }); - return response; - }; -} diff --git a/apps/space/store/root.store.ts b/apps/space/store/root.store.ts deleted file mode 100644 index 6b5261884..000000000 --- a/apps/space/store/root.store.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * 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"; -// store imports -import type { IInstanceStore } from "@/store/instance.store"; -import { InstanceStore } from "@/store/instance.store"; -import type { IIssueDetailStore } from "@/store/issue-detail.store"; -import { IssueDetailStore } from "@/store/issue-detail.store"; -import type { IIssueStore } from "@/store/issue.store"; -import { IssueStore } from "@/store/issue.store"; -import type { IUserStore } from "@/store/user.store"; -import { UserStore } from "@/store/user.store"; -import type { ICycleStore } from "./cycle.store"; -import { CycleStore } from "./cycle.store"; -import type { IIssueFilterStore } from "./issue-filters.store"; -import { IssueFilterStore } from "./issue-filters.store"; -import type { IIssueLabelStore } from "./label.store"; -import { LabelStore } from "./label.store"; -import type { IIssueMemberStore } from "./members.store"; -import { MemberStore } from "./members.store"; -import type { IIssueModuleStore } from "./module.store"; -import { ModuleStore } from "./module.store"; -import type { IPublishListStore } from "./publish/publish_list.store"; -import { PublishListStore } from "./publish/publish_list.store"; -import type { IStateStore } from "./state.store"; -import { StateStore } from "./state.store"; - -enableStaticRendering(typeof window === "undefined"); - -export class RootStore { - instance: IInstanceStore; - user: IUserStore; - issue: IIssueStore; - issueDetail: IIssueDetailStore; - state: IStateStore; - label: IIssueLabelStore; - module: IIssueModuleStore; - member: IIssueMemberStore; - cycle: ICycleStore; - issueFilter: IIssueFilterStore; - publishList: IPublishListStore; - - constructor() { - this.instance = new InstanceStore(this); - this.user = new UserStore(this); - this.issue = new IssueStore(this); - this.issueDetail = new IssueDetailStore(this); - this.state = new StateStore(this); - this.label = new LabelStore(this); - this.module = new ModuleStore(this); - this.member = new MemberStore(this); - this.cycle = new CycleStore(this); - this.issueFilter = new IssueFilterStore(this); - this.publishList = new PublishListStore(this); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - hydrate = (data: any) => { - if (!data) return; - this.instance.hydrate(data?.instance || undefined); - this.user.hydrate(data?.user || undefined); - }; - - reset() { - localStorage.setItem("theme", "system"); - this.instance = new InstanceStore(this); - this.user = new UserStore(this); - this.issue = new IssueStore(this); - this.issueDetail = new IssueDetailStore(this); - this.state = new StateStore(this); - this.label = new LabelStore(this); - this.module = new ModuleStore(this); - this.member = new MemberStore(this); - this.cycle = new CycleStore(this); - this.issueFilter = new IssueFilterStore(this); - this.publishList = new PublishListStore(this); - } -} diff --git a/apps/space/store/state.store.ts b/apps/space/store/state.store.ts deleted file mode 100644 index 3ecf78d15..000000000 --- a/apps/space/store/state.store.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { clone } from "lodash-es"; -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -// plane imports -import { SitesStateService } from "@plane/services"; -import type { IState } from "@plane/types"; -// helpers -import { sortStates } from "@/helpers/state.helper"; -// store -import type { RootStore } from "./root.store"; - -export interface IStateStore { - // observables - states: IState[] | undefined; - //computed - sortedStates: IState[] | undefined; - // computed actions - getStateById: (stateId: string | undefined) => IState | undefined; - // fetch actions - fetchStates: (anchor: string) => Promise; -} - -export class StateStore implements IStateStore { - states: IState[] | undefined = undefined; - stateService: SitesStateService; - rootStore: RootStore; - - constructor(_rootStore: RootStore) { - makeObservable(this, { - // observables - states: observable, - // computed - sortedStates: computed, - // fetch action - fetchStates: action, - }); - this.stateService = new SitesStateService(); - this.rootStore = _rootStore; - } - - get sortedStates() { - if (!this.states) return; - return sortStates(clone(this.states)); - } - - getStateById = (stateId: string | undefined) => this.states?.find((state) => state.id === stateId); - - fetchStates = async (anchor: string) => { - const statesResponse = await this.stateService.list(anchor); - runInAction(() => { - this.states = statesResponse; - }); - return statesResponse; - }; -} diff --git a/apps/space/store/user.store.ts b/apps/space/store/user.store.ts deleted file mode 100644 index f2a3c7dbc..000000000 --- a/apps/space/store/user.store.ts +++ /dev/null @@ -1,190 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { AxiosError } from "axios"; -import { set } from "lodash-es"; -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -// plane imports -import { UserService } from "@plane/services"; -import type { ActorDetail, IUser } from "@plane/types"; -// store types -import type { IProfileStore } from "@/store/profile.store"; -import { ProfileStore } from "@/store/profile.store"; -// store -import type { RootStore } from "@/store/root.store"; - -type TUserErrorStatus = { - status: string; - message: string; -}; - -export interface IUserStore { - // observables - isAuthenticated: boolean; - isInitializing: boolean; - error: TUserErrorStatus | undefined; - data: IUser | undefined; - // store observables - profile: IProfileStore; - // computed - currentActor: ActorDetail; - // actions - fetchCurrentUser: () => Promise; - updateCurrentUser: (data: Partial) => Promise; - hydrate: (data: IUser | undefined) => void; - reset: () => void; - signOut: () => Promise; -} - -export class UserStore implements IUserStore { - // observables - isAuthenticated: boolean = false; - isInitializing: boolean = true; - error: TUserErrorStatus | undefined = undefined; - data: IUser | undefined = undefined; - // store observables - profile: IProfileStore; - // service - userService: UserService; - - constructor(private store: RootStore) { - // stores - this.profile = new ProfileStore(store); - // service - this.userService = new UserService(); - // observables - makeObservable(this, { - // observables - isAuthenticated: observable.ref, - isInitializing: observable.ref, - error: observable, - // model observables - data: observable, - profile: observable, - // computed - currentActor: computed, - // actions - fetchCurrentUser: action, - updateCurrentUser: action, - reset: action, - signOut: action, - }); - } - - // computed - get currentActor(): ActorDetail { - return { - id: this.data?.id, - first_name: this.data?.first_name, - last_name: this.data?.last_name, - display_name: this.data?.display_name, - avatar_url: this.data?.avatar_url || undefined, - is_bot: false, - }; - } - - // actions - /** - * @description fetches the current user - * @returns {Promise} - */ - fetchCurrentUser = async (): Promise => { - try { - runInAction(() => { - if (this.data === undefined && !this.error) this.isInitializing = true; - this.error = undefined; - }); - const user = await this.userService.me(); - if (user && user?.id) { - await this.profile.fetchUserProfile(); - runInAction(() => { - this.data = user; - this.isInitializing = false; - this.isAuthenticated = true; - }); - } else - runInAction(() => { - this.data = user; - this.isInitializing = false; - this.isAuthenticated = false; - }); - return user; - } catch (error) { - runInAction(() => { - this.isInitializing = false; - this.isAuthenticated = false; - this.error = { - status: "user-fetch-error", - message: "Failed to fetch current user", - }; - if (error instanceof AxiosError && error.status === 401) { - this.data = undefined; - } - }); - throw error; - } - }; - - /** - * @description updates the current user - * @param data - * @returns {Promise} - */ - updateCurrentUser = async (data: Partial): Promise => { - const currentUserData = this.data; - try { - if (currentUserData) { - Object.keys(data).forEach((key: string) => { - const userKey: keyof IUser = key as keyof IUser; - if (this.data) set(this.data, userKey, data[userKey]); - }); - } - const user = await this.userService.update(data); - return user; - } catch (error) { - if (currentUserData) { - Object.keys(currentUserData).forEach((key: string) => { - const userKey: keyof IUser = key as keyof IUser; - if (this.data) set(this.data, userKey, currentUserData[userKey]); - }); - } - runInAction(() => { - this.error = { - status: "user-update-error", - message: "Failed to update current user", - }; - }); - throw error; - } - }; - - hydrate = (data: IUser | undefined): void => { - if (!data) return; - this.data = { ...this.data, ...data }; - }; - - /** - * @description resets the user store - * @returns {void} - */ - reset = (): void => { - runInAction(() => { - this.isAuthenticated = false; - this.isInitializing = false; - this.error = undefined; - this.data = undefined; - this.profile = new ProfileStore(this.store); - }); - }; - - /** - * @description signs out the current user - * @returns {Promise} - */ - signOut = async (): Promise => { - this.store.reset(); - }; -} diff --git a/apps/space/styles/globals.css b/apps/space/styles/globals.css deleted file mode 100644 index 4cddab587..000000000 --- a/apps/space/styles/globals.css +++ /dev/null @@ -1,48 +0,0 @@ -@import "@plane/tailwind-config/index.css"; -@import "@plane/editor/styles"; -@import "@plane/propel/styles/react-day-picker.css"; -@plugin "@tailwindcss/typography"; - -/* stickies and editor colors - * Text colors and the themed light/dark background ramps come from - * @plane/editor/styles (imported above), which is their single source of truth. - * Only the un-themed :root fallback below is kept: the editor package declares - * background colors solely under [data-theme*="light"] / [data-theme*="dark"], - * so without this they are undefined until a theme lands on the element. */ -:root { - --editor-colors-gray-background: #d6d6d8; - --editor-colors-peach-background: #ffd5d7; - --editor-colors-pink-background: #fdd4e3; - --editor-colors-orange-background: #ffe3cd; - --editor-colors-green-background: #c3f0de; - --editor-colors-light-blue-background: #c5eff9; - --editor-colors-dark-blue-background: #c9dafb; - --editor-colors-purple-background: #e3d8fd; -} - -/* Progress Bar Styles */ -:root { - --bprogress-color: var(--background-color-accent-primary); - --bprogress-height: 2.5px !important; -} - -.bprogress { - pointer-events: none; -} - -.bprogress .bar { - background: linear-gradient( - 90deg, - --alpha(var(--background-color-accent-primary) / 80%) 0%, - --alpha(var(--background-color-accent-primary) / 100%) 100% - ) !important; - will-change: width, opacity; -} - -.bprogress .peg { - display: block; - box-shadow: - 0 0 8px --alpha(var(--background-color-accent-primary) / 60%), - 0 0 4px --alpha(var(--background-color-accent-primary) / 40%) !important; - will-change: transform, opacity; -} diff --git a/apps/space/tsconfig.json b/apps/space/tsconfig.json deleted file mode 100644 index a340381bb..000000000 --- a/apps/space/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "@plane/typescript-config/react-router.json", - "compilerOptions": { - "rootDirs": [".", "./.react-router/types"], - "types": ["vite/client"], - "paths": { - "@/*": ["./*"] - }, - "strictNullChecks": true, - "noImplicitOverride": false, - "noUnusedLocals": false, - "noImplicitReturns": false, - "exactOptionalPropertyTypes": false, - "noUnusedParameters": false - }, - "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*", "additional.d.ts"], - "exclude": ["node_modules"] -} diff --git a/apps/space/types/auth.ts b/apps/space/types/auth.ts deleted file mode 100644 index 72e12cd46..000000000 --- a/apps/space/types/auth.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export enum EAuthModes { - SIGN_IN = "SIGN_IN", - SIGN_UP = "SIGN_UP", -} - -export enum EAuthSteps { - EMAIL = "EMAIL", - PASSWORD = "PASSWORD", - UNIQUE_CODE = "UNIQUE_CODE", -} - -export interface ICsrfTokenData { - csrf_token: string; -} - -// email check types starts -export interface IEmailCheckData { - email: string; -} - -export interface IEmailCheckResponse { - status: "MAGIC_CODE" | "CREDENTIAL"; - is_password_autoset: boolean; - existing: boolean; -} -// email check types ends diff --git a/apps/space/types/cycle.d.ts b/apps/space/types/cycle.d.ts deleted file mode 100644 index edf8f31a8..000000000 --- a/apps/space/types/cycle.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type TPublicCycle = { - id: string; - name: string; - status: string; -}; diff --git a/apps/space/types/intake.d.ts b/apps/space/types/intake.d.ts deleted file mode 100644 index 9cf2934f6..000000000 --- a/apps/space/types/intake.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type TIntakeIssueForm = { - name: string; - email: string; - username: string; - description_html: string; -}; diff --git a/apps/space/types/issue.d.ts b/apps/space/types/issue.d.ts deleted file mode 100644 index 299aada24..000000000 --- a/apps/space/types/issue.d.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { ActorDetail, TIssue, TIssuePriorities, TStateGroups, TIssuePublicComment } from "@plane/types"; - -export type TIssueLayout = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt"; -export type TIssueLayoutOptions = { - [key in TIssueLayout]: boolean; -}; - -export type TIssueFilterPriorityObject = { - key: TIssuePriorities; - title: string; - className: string; - icon: string; -}; - -export type TIssueFilterKeys = "priority" | "state" | "labels"; - -export type TDisplayFilters = { - layout: TIssueLayout; -}; - -export type TFilters = { - state: TStateGroups[]; - priority: TIssuePriorities[]; - labels: string[]; -}; - -export type TIssueFilters = { - display_filters: TDisplayFilters; - filters: TFilters; -}; - -export type TIssueQueryFilters = Partial; - -export type TIssueQueryFiltersParams = Partial>; - -export interface IIssue extends Pick< - TIssue, - | "description_html" - | "created_at" - | "updated_at" - | "created_by" - | "id" - | "name" - | "priority" - | "state_id" - | "project_id" - | "sequence_id" - | "sort_order" - | "start_date" - | "target_date" - | "cycle_id" - | "module_ids" - | "label_ids" - | "assignee_ids" - | "attachment_count" - | "sub_issues_count" - | "link_count" - | "estimate_point" -> { - comments: TIssuePublicComment[]; - reaction_items: IIssueReaction[]; - vote_items: IVote[]; -} - -export type IPeekMode = "side" | "modal" | "full"; - -type TIssueResponseResults = - | IIssue[] - | { - [key: string]: { - results: - | IIssue[] - | { - [key: string]: { - results: IIssue[]; - total_results: number; - }; - }; - total_results: number; - }; - }; - -export type TIssuesResponse = { - grouped_by: string; - next_cursor: string; - prev_cursor: string; - next_page_results: boolean; - prev_page_results: boolean; - total_count: number; - count: number; - total_pages: number; - extra_stats: null; - results: TIssueResponseResults; -}; - -export interface IIssueLabel { - id: string; - name: string; - color: string; - parent: string | null; -} - -export interface IVote { - vote: -1 | 1; - actor_details: ActorDetail; -} - -export interface IIssueReaction { - actor_details: ActorDetail; - reaction: string; -} - -export interface IIssueFilterOptions { - state?: string[] | null; - labels?: string[] | null; - priority?: string[] | null; -} diff --git a/apps/space/types/member.d.ts b/apps/space/types/member.d.ts deleted file mode 100644 index 34c95daf6..000000000 --- a/apps/space/types/member.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type TPublicMember = { - id: string; - member: string; - member__display_name: string; - member__avatar: string; -}; diff --git a/apps/space/types/modules.d.ts b/apps/space/types/modules.d.ts deleted file mode 100644 index 8bc35ce6f..000000000 --- a/apps/space/types/modules.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type TPublicModule = { - id: string; - name: string; -}; diff --git a/apps/space/vite.config.ts b/apps/space/vite.config.ts deleted file mode 100644 index 1767562ce..000000000 --- a/apps/space/vite.config.ts +++ /dev/null @@ -1,39 +0,0 @@ -import path from "node:path"; -import * as dotenv from "dotenv"; -import { reactRouter } from "@react-router/dev/vite"; -import { defineConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; -import { joinUrlPath } from "@plane/utils"; - -dotenv.config({ path: path.resolve(__dirname, ".env") }); - -// Expose only vars starting with VITE_ -const viteEnv = Object.keys(process.env) - .filter((k) => k.startsWith("VITE_")) - .reduce>((a, k) => { - a[k] = process.env[k] ?? ""; - return a; - }, {}); - -const basePath = joinUrlPath(process.env.VITE_SPACE_BASE_PATH ?? "", "/") ?? "/"; - -export default defineConfig(() => ({ - base: basePath, - define: { - "process.env": JSON.stringify(viteEnv), - }, - build: { - assetsInlineLimit: 0, - }, - plugins: [reactRouter(), tsconfigPaths({ projects: [path.resolve(__dirname, "tsconfig.json")] })], - resolve: { - alias: { - // Next.js compatibility shims used within space - "next/navigation": path.resolve(__dirname, "app/compat/next/navigation.ts"), - }, - dedupe: ["react", "react-dom"], - }, - server: { - host: "127.0.0.1", - }, -})); diff --git a/apps/web/.dockerignore b/apps/web/.dockerignore deleted file mode 100644 index 90b7c4387..000000000 --- a/apps/web/.dockerignore +++ /dev/null @@ -1,11 +0,0 @@ -node_modules -.next -.react-router -.vite -.turbo -build -dist -*.log -.env* -!.env.example - diff --git a/apps/web/.env.example b/apps/web/.env.example deleted file mode 100644 index 59663aa67..000000000 --- a/apps/web/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -VITE_API_BASE_URL="http://localhost:8000" - -VITE_WEB_BASE_URL="http://localhost:3000" - -VITE_ADMIN_BASE_URL="http://localhost:3001" -VITE_ADMIN_BASE_PATH="/god-mode" - -VITE_SPACE_BASE_URL="http://localhost:3002" -VITE_SPACE_BASE_PATH="/spaces" - -VITE_LIVE_BASE_URL="http://localhost:3100" -VITE_LIVE_BASE_PATH="/live" diff --git a/apps/web/.gitignore b/apps/web/.gitignore deleted file mode 100644 index 7d7c7a5f2..000000000 --- a/apps/web/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ - -# Sentry Config File -.env.sentry-build-plugin diff --git a/apps/web/.prettierignore b/apps/web/.prettierignore deleted file mode 100644 index b0b8bc622..000000000 --- a/apps/web/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -.next/ -.react-router/ -.turbo/ -.vite/ -build/ -dist/ -node_modules/ -out/ -pnpm-lock.yaml -storybook-static/ diff --git a/apps/web/Dockerfile.dev b/apps/web/Dockerfile.dev deleted file mode 100644 index d914fd81f..000000000 --- a/apps/web/Dockerfile.dev +++ /dev/null @@ -1,13 +0,0 @@ -FROM node:22-alpine - -RUN apk add --no-cache libc6-compat -# Set working directory -WORKDIR /app - -COPY . . -RUN corepack enable pnpm && pnpm add -g turbo -RUN pnpm install - -EXPOSE 3000 -VOLUME [ "/app/node_modules", "/app/web/node_modules" ] -CMD ["pnpm", "dev", "--filter=web"] diff --git a/apps/web/Dockerfile.web b/apps/web/Dockerfile.web deleted file mode 100644 index ee9a54f9e..000000000 --- a/apps/web/Dockerfile.web +++ /dev/null @@ -1,126 +0,0 @@ -# syntax=docker/dockerfile:1.7 -FROM node:22-alpine AS base - -# Setup pnpm package manager with corepack and configure global bin directory for caching -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PNPM_HOME/bin:$PATH" -RUN corepack enable - -# ***************************************************************************** -# STAGE 1: Build the project -# ***************************************************************************** -FROM base AS builder -RUN apk add --no-cache libc6-compat -# Set working directory -WORKDIR /app - -ARG TURBO_VERSION=2.9.18 -RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION} -COPY . . - -RUN turbo prune --scope=web --docker - -# ***************************************************************************** -# STAGE 2: Install dependencies & build the project -# ***************************************************************************** -# Add lockfile and package.json's of isolated subworkspace -FROM base AS installer - -RUN apk add --no-cache libc6-compat -WORKDIR /app - -# First install the dependencies (as they change less often) -COPY .gitignore .gitignore -COPY --from=builder /app/out/json/ . -COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml -RUN corepack enable pnpm - -# Copy full directory structure before fetch to ensure all package.json files are available -COPY --from=builder /app/out/full/ . -COPY turbo.json turbo.json - -# Fetch dependencies to cache store, then install offline with dev deps -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store - -ARG VITE_API_BASE_URL="" -ENV VITE_API_BASE_URL=$VITE_API_BASE_URL - -ARG VITE_ADMIN_BASE_URL="" -ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL - -ARG VITE_ADMIN_BASE_PATH="/god-mode" -ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH - -ARG VITE_LIVE_BASE_URL="" -ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL - -ARG VITE_LIVE_BASE_PATH="/live" -ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH - -ARG VITE_SPACE_BASE_URL="" -ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL - -ARG VITE_SPACE_BASE_PATH="/spaces" -ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH - -ARG VITE_WEB_BASE_URL="" -ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL - -ENV NEXT_TELEMETRY_DISABLED=1 -ENV TURBO_TELEMETRY_DISABLED=1 - -RUN pnpm turbo run build --filter=web - -# ***************************************************************************** -# STAGE 3: Serve with Caddy -# ***************************************************************************** -FROM caddy:2.11.4-builder-alpine AS caddy-builder - -RUN XCADDY_SKIP_CLEANUP=1 xcaddy build \ - --with github.com/mholt/caddy-ratelimit - -# Caddy's own go.mod pins x/crypto, x/net and x/text at versions with open CVEs -# (CVE-2026-56854 CRITICAL in x/crypto, CVE-2026-46600 in x/net, -# CVE-2026-56852 in x/text), so every xcaddy build re-ships them. xcaddy -# preserved its build dir above, so upgrade them there and rebuild. -# x/crypto v0.55.0 requires x/net v0.57.0 and x/text v0.41.0, hence those floors. -# grpc and otel move together: grpc >= v1.83.0 requires otel v1.44.0, so bump -# the otel pair in lockstep or go walks grpc backwards to satisfy the older one. -RUN cd "$(find /tmp -maxdepth 1 -name 'buildenv_*' -type d | head -1)" && \ - go get golang.org/x/crypto@v0.55.0 && \ - go get golang.org/x/net@v0.57.0 && \ - go get golang.org/x/text@v0.41.0 && \ - go get google.golang.org/grpc@v1.83.1 && \ - go get go.opentelemetry.io/otel@v1.44.0 && \ - go get go.opentelemetry.io/otel/sdk@v1.44.0 && \ - go mod tidy && \ - go build -o /usr/bin/caddy -ldflags "-w -s" -trimpath . - -# `go get` resolves a module backwards rather than failing when pins conflict, -# so assert the binary really carries the pinned grpc: a silent downgrade must -# fail the build instead of shipping a caddy that only looks patched. -RUN go version -m /usr/bin/caddy | grep -qE 'google\.golang\.org/grpc[[:space:]]v1\.83\.1[[:space:]]' \ - || { echo "caddy was not built against the pinned grpc -- check the go get floors" >&2; exit 1; } - -FROM caddy:2.11.4-alpine AS production - -# curl is required by the HEALTHCHECK below and is not present in the base image. -# OS security updates too: --available forces reinstall from the current repos; -# bump APK_SECURITY_PATCH to bust buildx's cached layer (keyed on this command -# string), otherwise a rebuild silently re-ships the old packages. -ARG APK_SECURITY_PATCH=2026-09-07 -RUN echo "apk-security-patch ${APK_SECURITY_PATCH}" && apk update && apk upgrade --no-cache --available && apk add --no-cache curl && rm -rf /var/cache/apk/* - -COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy - -COPY apps/web/caddy/Caddyfile /etc/caddy/Caddyfile - -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD curl -fsS http://127.0.0.1:3000/ >/dev/null || exit 1 - -COPY --from=installer /app/apps/web/build/client /usr/share/caddy/html - -EXPOSE 3000 - -CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"] \ No newline at end of file diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/_sidebar.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/_sidebar.tsx deleted file mode 100644 index 173b1fb0e..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/_sidebar.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/** - * 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"; -// plane imports -import { useParams, usePathname } from "next/navigation"; -import { SIDEBAR_WIDTH } from "@plane/constants"; -import { useLocalStorage } from "@plane/hooks"; -// components -import { ResizableSidebar } from "@/components/sidebar/resizable-sidebar"; -// hooks -import { useAppTheme } from "@/hooks/store/use-app-theme"; -// local imports -import { ExtendedAppSidebar } from "./extended-sidebar"; -import { AppSidebar } from "./sidebar"; - -export const ProjectAppSidebar = observer(function ProjectAppSidebar() { - // store hooks - const { - sidebarCollapsed, - toggleSidebar, - sidebarPeek, - toggleSidebarPeek, - isExtendedSidebarOpened, - isAnySidebarDropdownOpen, - } = useAppTheme(); - const { storedValue, setValue } = useLocalStorage("sidebarWidth", SIDEBAR_WIDTH); - // states - const [sidebarWidth, setSidebarWidth] = useState(storedValue ?? SIDEBAR_WIDTH); - // routes - const { workspaceSlug } = useParams(); - const pathname = usePathname(); - // derived values - const isAnyExtendedSidebarOpen = isExtendedSidebarOpened; - - const isNotificationsPath = pathname.includes(`/${workspaceSlug}/notifications`); - - // handlers - const handleWidthChange = (width: number) => setValue(width); - - if (isNotificationsPath) return null; - - return ( - <> - - - - } - isAnyExtendedSidebarExpanded={isAnyExtendedSidebarOpen} - isAnySidebarDropdownOpen={isAnySidebarDropdownOpen} - > - - - - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/header.tsx deleted file mode 100644 index bcc00835f..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/header.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useTranslation } from "@plane/i18n"; -// ui -import { CyclesOutline } from "@makeplane/propel/icons"; -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; - -export const WorkspaceActiveCycleHeader = observer(function WorkspaceActiveCycleHeader() { - const { t } = useTranslation(); - return ( -
- - - } - /> - } - /> - - -
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/layout.tsx deleted file mode 100644 index fb000228c..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// components -import { Outlet } from "react-router"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -// local imports -import { WorkspaceActiveCycleHeader } from "./header"; - -export default function WorkspaceActiveCycleLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/page.tsx deleted file mode 100644 index e7e1e07c8..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// components -import { PageHead } from "@/components/core/page-title"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -// local imports -import { WorkspaceActiveCyclesList } from "@/components/active-cycles/workspace-active-cycles-list"; - -function WorkspaceActiveCyclesPage() { - const { currentWorkspace } = useWorkspace(); - // derived values - const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Active Cycles` : undefined; - - return ( - <> - - - - ); -} - -export default observer(WorkspaceActiveCyclesPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/header.tsx deleted file mode 100644 index 992476e6d..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/header.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useTranslation } from "@plane/i18n"; -import { AnalyticsOutline } from "@makeplane/propel/icons"; -// plane imports -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; - -export const WorkspaceAnalyticsHeader = observer(function WorkspaceAnalyticsHeader() { - const { t } = useTranslation(); - return ( -
- - - } - /> - } - /> - - -
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/layout.tsx deleted file mode 100644 index e12366070..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// components -import { Outlet } from "react-router"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { WorkspaceAnalyticsHeader } from "./header"; - -export default function WorkspaceAnalyticsTabLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx deleted file mode 100644 index e1e985e1d..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useState, useEffect } from "react"; -import { observer } from "mobx-react"; -import { useRouter } from "next/navigation"; -// plane package imports -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { EmptyStateDetailed } from "@plane/propel/empty-state"; -import { Tab, Tabs, TabsList, TabsPanel } from "@makeplane/propel/components/tabs"; -// components -import { cn } from "@plane/utils"; -import AnalyticsFilterActions from "@/components/analytics/analytics-filter-actions"; -import { PageHead } from "@/components/core/page-title"; -// hooks -import { useCommandPalette } from "@/hooks/store/use-command-palette"; -import { useProject } from "@/hooks/store/use-project"; -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useAnalyticsTabs } from "@/components/analytics/use-analytics-tabs"; -import type { Route } from "./+types/page"; - -function AnalyticsPage({ params }: Route.ComponentProps) { - const { tabId } = params; - - // hooks - const router = useRouter(); - - // plane imports - const { t } = useTranslation(); - - // store hooks - const { toggleCreateProjectModal } = useCommandPalette(); - const { workspaceProjectIds, loader } = useProject(); - const { currentWorkspace } = useWorkspace(); - const { allowPermissions } = useUserPermissions(); - - const pageTitle = currentWorkspace?.name - ? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name }) - : undefined; - - // permissions - const canPerformEmptyStateActions = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - const workspaceSlug = params.workspaceSlug; - const ANALYTICS_TABS = useAnalyticsTabs(workspaceSlug.toString()); - - const [selectedTab, setSelectedTab] = useState(tabId || ANALYTICS_TABS[0]?.key); - - useEffect(() => { - if (tabId) { - setSelectedTab(tabId); - } - }, [tabId]); - - // Handle tab change - const handleTabChange = (value: string) => { - setSelectedTab(value); - router.push(`/${currentWorkspace?.slug}/analytics/${value}`); - }; - - return ( - <> - - {workspaceProjectIds && ( - <> - {workspaceProjectIds.length > 0 || loader === "init-loader" ? ( -
- -
-
- - {ANALYTICS_TABS.map((tab) => ( - - ))} - - -
- -
-
- {/* Grid wrapper: Propel's TabsPanel omits className, so the single mounted panel - gets its fill height from a one-row grid instead. */} -
- {ANALYTICS_TABS.map((tab) => ( - - - - ))} -
-
-
-
- ) : ( - { - toggleCreateProjectModal(true); - }, - disabled: !canPerformEmptyStateActions, - }, - ]} - /> - )} - - )} - - ); -} - -export default observer(AnalyticsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx deleted file mode 100644 index 88df26053..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { Header, Row } from "@plane/ui"; -import { cn } from "@plane/utils"; -// components -import { AppHeader } from "@/components/core/app-header"; -import { TabNavigationRoot } from "@/components/navigation"; -import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-button"; -// hooks -import { useAppTheme } from "@/hooks/store/use-app-theme"; -import { useIssueDetail } from "@/hooks/store/use-issue-detail"; -import { useProjectNavigationPreferences } from "@/hooks/use-navigation-preferences"; -// local components -import { WorkItemDetailsHeader } from "./work-item-header"; - -export const ProjectWorkItemDetailsHeader = observer(function ProjectWorkItemDetailsHeader() { - // router - const { workspaceSlug, workItem } = useParams(); - // store hooks - const { sidebarCollapsed } = useAppTheme(); - const { - issue: { getIssueById, getIssueIdByIdentifier }, - } = useIssueDetail(); - // derived values - const issueId = getIssueIdByIdentifier(workItem?.toString()); - const issueDetails = issueId ? getIssueById(issueId?.toString()) : undefined; - // preferences - const { preferences: projectPreferences } = useProjectNavigationPreferences(); - - return ( - <> - {projectPreferences.navigationMode === "TABBED" && ( -
- -
-
- {sidebarCollapsed && ( -
- -
- )} -
- - - -
-
-
-
-
- )} - } /> - - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx deleted file mode 100644 index b8b8a58e6..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// components -import { Outlet } from "react-router"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ProjectWorkItemDetailsHeader } from "./header"; - -export default function ProjectIssueDetailsLayout() { - return ( - <> - - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx deleted file mode 100644 index a644c40b6..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import { useTheme } from "next-themes"; -import useSWR from "swr"; -// plane imports -import { useTranslation } from "@plane/i18n"; -import type { TIssue } from "@plane/types"; -import { EIssueServiceType } from "@plane/types"; -import { Loader } from "@plane/ui"; -// assets -import emptyIssueDark from "@/app/assets/empty-state/search/issues-dark.webp?url"; -import emptyIssueLight from "@/app/assets/empty-state/search/issues-light.webp?url"; -// components -import { EmptyState } from "@/components/common/empty-state"; -import { PageHead } from "@/components/core/page-title"; -// hooks -import { useAppTheme } from "@/hooks/store/use-app-theme"; -import { useIssueDetail } from "@/hooks/store/use-issue-detail"; -import { useProject } from "@/hooks/store/use-project"; -import { useAppRouter } from "@/hooks/use-app-router"; -// layouts -import { ProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper"; -// plane web imports -import { useWorkItemProperties } from "@/hooks/use-issue-properties"; -import { WorkItemDetailRoot } from "@/components/browse/workItem-detail"; - -import type { Route } from "./+types/page"; - -export const IssueDetailsPage = observer(function IssueDetailsPage({ params }: Route.ComponentProps) { - // router - const router = useAppRouter(); - const { workspaceSlug, workItem } = params; - // hooks - const { resolvedTheme } = useTheme(); - // store hooks - const { t } = useTranslation(); - const { - fetchIssueWithIdentifier, - issue: { getIssueById }, - } = useIssueDetail(); - const { getProjectById, getProjectByIdentifier } = useProject(); - const { toggleIssueDetailSidebar, issueDetailSidebarCollapsed } = useAppTheme(); - - const [projectIdentifier, sequence_id] = workItem.split("-"); - - // fetching issue details - const { data, isLoading, error } = useSWR( - `ISSUE_DETAIL_${workspaceSlug}_${projectIdentifier}_${sequence_id}`, - () => fetchIssueWithIdentifier(workspaceSlug.toString(), projectIdentifier, sequence_id) - ); - - // derived values - const projectDetails = getProjectByIdentifier(projectIdentifier); - const issueId = data?.id; - const projectId = data?.project_id ?? projectDetails?.id ?? ""; - const issue = getIssueById(issueId?.toString() || "") || undefined; - const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined; - const issueLoader = !issue || isLoading; - const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined; - - useWorkItemProperties( - projectId, - workspaceSlug.toString(), - issueId, - issue?.is_epic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES - ); - - useEffect(() => { - const handleToggleIssueDetailSidebar = () => { - if (window && window.innerWidth < 768) { - toggleIssueDetailSidebar(true); - } - if (window && issueDetailSidebarCollapsed && window.innerWidth >= 768) { - toggleIssueDetailSidebar(false); - } - }; - window.addEventListener("resize", handleToggleIssueDetailSidebar); - handleToggleIssueDetailSidebar(); - return () => window.removeEventListener("resize", handleToggleIssueDetailSidebar); - }, [issueDetailSidebarCollapsed, toggleIssueDetailSidebar]); - - useEffect(() => { - if (data?.is_intake) { - router.push(`/${workspaceSlug}/projects/${data.project_id}/intake/?currentTab=open&inboxIssueId=${data?.id}`); - } - }, [workspaceSlug, data, router]); - - if (error && !isLoading) { - return ( - router.push(`/${workspaceSlug}/workspace-views/all-issues/`), - }} - /> - ); - } - - if (issueLoader) { - return ( - -
- - - - -
-
- - - - -
-
- ); - } - - return ( - <> - - {workspaceSlug && projectId && issueId && ( - - - - )} - - ); -}); - -export default IssueDetailsPage; diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/work-item-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/work-item-header.tsx deleted file mode 100644 index 4b7111e97..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/work-item-header.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane ui -import { WorkItemsOutline } from "@makeplane/propel/icons"; -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { IssueDetailQuickActions } from "@/components/issues/issue-detail/issue-detail-quick-actions"; -// hooks -import { useIssueDetail } from "@/hooks/store/use-issue-detail"; -import { useProject } from "@/hooks/store/use-project"; -import { useAppRouter } from "@/hooks/use-app-router"; -// plane web imports -import { CommonProjectBreadcrumbs } from "@/components/breadcrumbs/common"; - -export const WorkItemDetailsHeader = observer(function WorkItemDetailsHeader() { - // router - const router = useAppRouter(); - const { workspaceSlug, workItem } = useParams(); - // store hooks - const { getProjectById, loader } = useProject(); - const { - issue: { getIssueById, getIssueIdByIdentifier }, - } = useIssueDetail(); - // derived values - const issueId = getIssueIdByIdentifier(workItem?.toString()); - const issueDetails = issueId ? getIssueById(issueId.toString()) : undefined; - const projectId = issueDetails ? issueDetails?.project_id : undefined; - const projectDetails = projectId ? getProjectById(projectId?.toString()) : undefined; - - if (!workspaceSlug || !projectId || !issueId) return null; - return ( -
- - - - } - /> - } - /> - - } - /> - - - - {projectId && issueId && ( - - )} - -
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/drafts/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/drafts/header.tsx deleted file mode 100644 index 027b8edd7..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/drafts/header.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/** - * 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 { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -// ui -import { Button } from "@plane/propel/button"; -import { DraftsOutline } from "@makeplane/propel/icons"; -import { EIssuesStoreType } from "@plane/types"; -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { CountChip } from "@/components/common/count-chip"; -import { CreateUpdateIssueModal } from "@/components/issues/issue-modal/modal"; - -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useWorkspaceDraftIssues } from "@/hooks/store/workspace-draft"; - -export const WorkspaceDraftHeader = observer(function WorkspaceDraftHeader() { - // state - const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); - // store hooks - const { allowPermissions } = useUserPermissions(); - const { paginationInfo } = useWorkspaceDraftIssues(); - const { joinedProjectIds } = useProject(); - - const { t } = useTranslation(); - // check if user is authorized to create draft work item - const isAuthorizedUser = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - return ( - <> - setIsDraftIssueModalOpen(false)} - isDraft - /> -
- -
- - } /> - } - /> - - {paginationInfo?.total_count && paginationInfo?.total_count > 0 ? ( - - ) : ( - <> - )} -
-
- - - {joinedProjectIds && joinedProjectIds.length > 0 && ( - - )} - -
- - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/drafts/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/drafts/layout.tsx deleted file mode 100644 index d7df53f90..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/drafts/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// components -import { Outlet } from "react-router"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -// local imports -import { WorkspaceDraftHeader } from "./header"; - -export default function WorkspaceDraftLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/drafts/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/drafts/page.tsx deleted file mode 100644 index 300fbce17..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/drafts/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// components -import { PageHead } from "@/components/core/page-title"; -import { WorkspaceDraftIssuesRoot } from "@/components/issues/workspace-draft"; -import type { Route } from "./+types/page"; - -function WorkspaceDraftPage({ params }: Route.ComponentProps) { - const { workspaceSlug } = params; - const pageTitle = "Workspace Draft"; - - return ( - <> - -
- -
- - ); -} - -export default WorkspaceDraftPage; diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx deleted file mode 100644 index 5d7c69f08..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useRef, useState } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { EmptyStateCompact } from "@plane/propel/empty-state"; -import { AddOutline, SearchOutline } from "@makeplane/propel/icons"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { Tooltip } from "@makeplane/propel/components/tooltip"; -import { copyUrlToClipboard, orderJoinedProjects } from "@plane/utils"; -// components -import { CreateProjectModal } from "@/components/project/create-project-modal"; -import { SidebarProjectsListItem } from "@/components/workspace/sidebar/projects-list-item"; -// hooks -import { useAppTheme } from "@/hooks/store/use-app-theme"; -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -import type { TProject } from "@plane/types"; -import { ExtendedSidebarWrapper } from "./extended-sidebar-wrapper"; - -export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar() { - // refs - const extendedProjectSidebarRef = useRef(null); - const [searchQuery, setSearchQuery] = useState(""); - // states - const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); - // routers - const { workspaceSlug } = useParams(); - // store hooks - const { t } = useTranslation(); - const { isExtendedProjectSidebarOpened, toggleExtendedProjectSidebar } = useAppTheme(); - const { getPartialProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject(); - const { allowPermissions } = useUserPermissions(); - - const handleOnProjectDrop = ( - sourceId: string | undefined, - destinationId: string | undefined, - shouldDropAtEnd: boolean - ) => { - if (!sourceId || !destinationId || !workspaceSlug) return; - if (sourceId === destinationId) return; - - const joinedProjectsList: TProject[] = []; - joinedProjects.map((projectId) => { - const projectDetails = getPartialProjectById(projectId); - if (projectDetails) joinedProjectsList.push(projectDetails); - }); - - const sourceIndex = joinedProjects.indexOf(sourceId); - const destinationIndex = shouldDropAtEnd ? joinedProjects.length : joinedProjects.indexOf(destinationId); - - if (joinedProjectsList.length <= 0) return; - - const updatedSortOrder = orderJoinedProjects(sourceIndex, destinationIndex, sourceId, joinedProjectsList); - if (updatedSortOrder != undefined) - updateProjectView(workspaceSlug.toString(), sourceId, { sort_order: updatedSortOrder }).catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: t("error"), - message: t("something_went_wrong"), - }); - }); - }; - - // filter projects based on search query - const filteredProjects = joinedProjects.filter((projectId) => { - const project = getPartialProjectById(projectId); - if (!project) return false; - return project.name.toLowerCase().includes(searchQuery.toLowerCase()) || project.identifier.includes(searchQuery); - }); - - // auth - const isAuthorizedUser = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - const handleClose = useCallback(() => toggleExtendedProjectSidebar(false), [toggleExtendedProjectSidebar]); - - const handleCopyText = (projectId: string) => { - copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: t("link_copied"), - message: t("project_link_copied_to_clipboard"), - }); - }); - }; - return ( - <> - {workspaceSlug && ( - setIsProjectModalOpen(false)} - setToFavorite={false} - workspaceSlug={workspaceSlug.toString()} - /> - )} - -
-
- Projects - {isAuthorizedUser && ( - - - - )} -
-
- - setSearchQuery(e.target.value)} - /> -
-
- {filteredProjects.length === 0 ? ( -
- -
- ) : ( -
- {filteredProjects.map((projectId, index) => ( - handleCopyText(projectId)} - projectListType={"JOINED"} - disableDrag={false} - disableDrop={false} - isLastChild={index === filteredProjects.length - 1} - handleOnProjectDrop={handleOnProjectDrop} - renderInExtendedSidebar - /> - ))} -
- )} -
- - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar-wrapper.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar-wrapper.tsx deleted file mode 100644 index 90030a341..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar-wrapper.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useEffect } from "react"; -import { observer } from "mobx-react"; -// plane imports -import { EXTENDED_SIDEBAR_WIDTH, SIDEBAR_WIDTH } from "@plane/constants"; -import { useLocalStorage } from "@plane/hooks"; -import { cn } from "@plane/utils"; -// hooks -import { useAppTheme } from "@/hooks/store/use-app-theme"; -// hooks -import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click"; - -type Props = { - className?: string; - children: React.ReactNode; - extendedSidebarRef: React.RefObject; - isExtendedSidebarOpened: boolean; - handleClose: () => void; - excludedElementId: string; -}; - -export const ExtendedSidebarWrapper = observer(function ExtendedSidebarWrapper(props: Props) { - const { className, children, extendedSidebarRef, isExtendedSidebarOpened, handleClose, excludedElementId } = props; - // store hooks - const { sidebarCollapsed } = useAppTheme(); - // local storage - const { storedValue } = useLocalStorage("sidebarWidth", SIDEBAR_WIDTH); - - useExtendedSidebarOutsideClickDetector(extendedSidebarRef, handleClose, excludedElementId); - - useEffect(() => { - if (sidebarCollapsed) { - handleClose(); - } - }, [sidebarCollapsed, handleClose]); - - return ( -
- {children} -
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar.tsx deleted file mode 100644 index c8f5eb82f..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useMemo, useRef } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS, EUserPermissionsLevel } from "@plane/constants"; -import type { EUserWorkspaceRoles } from "@plane/types"; -// hooks -import { useAppTheme } from "@/hooks/store/use-app-theme"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useWorkspaceNavigationPreferences } from "@/hooks/use-navigation-preferences"; -// components -import { ExtendedSidebarItem } from "@/components/workspace/sidebar/extended-sidebar-item"; -import { ExtendedSidebarWrapper } from "./extended-sidebar-wrapper"; - -export const ExtendedAppSidebar = observer(function ExtendedAppSidebar() { - // refs - const extendedSidebarRef = useRef(null); - // routers - const { workspaceSlug } = useParams(); - // store hooks - const { isExtendedSidebarOpened, toggleExtendedSidebar } = useAppTheme(); - const { allowPermissions } = useUserPermissions(); - const { preferences: workspacePreferences, updateWorkspaceItemSortOrder } = useWorkspaceNavigationPreferences(); - - // derived values - const currentWorkspaceNavigationPreferences = workspacePreferences.items; - - const sortedNavigationItems = useMemo(() => { - const slug = workspaceSlug.toString(); - - return ( - WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.filter((item) => { - // Permission check - const hasPermission = allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, slug); - - return hasPermission; - }) - // oxlint-disable-next-line oxc/no-map-spread - .map((item) => { - const preference = currentWorkspaceNavigationPreferences?.[item.key]; - return { - ...item, - sort_order: preference?.sort_order ?? 0, - is_pinned: preference?.is_pinned ?? false, - }; - }) - // oxlint-disable-next-line unicorn/no-array-sort - .sort((a, b) => { - // First sort by pinned status (pinned items first) - if (a.is_pinned !== b.is_pinned) { - return b.is_pinned ? 1 : -1; - } - // Then sort by sort_order within each group - return a.sort_order - b.sort_order; - }) - ); - }, [workspaceSlug, currentWorkspaceNavigationPreferences, allowPermissions]); - - const sortedNavigationItemsKeys = sortedNavigationItems.map((item) => item.key); - - // oxlint-disable-next-line unicorn/consistent-function-scoping - const orderNavigationItem = ( - sourceIndex: number, - destinationIndex: number, - navigationList: { - sort_order: number; - key: string; - labelTranslationKey: string; - href: string; - access: EUserWorkspaceRoles[]; - }[] - ): number | undefined => { - if (sourceIndex < 0 || destinationIndex < 0 || navigationList.length <= 0) return undefined; - - let updatedSortOrder: number | undefined = undefined; - const sortOrderDefaultValue = 10000; - - if (destinationIndex === 0) { - // updating project at the top of the project - const currentSortOrder = navigationList[destinationIndex].sort_order || 0; - updatedSortOrder = currentSortOrder - sortOrderDefaultValue; - } else if (destinationIndex === navigationList.length) { - // updating project at the bottom of the project - const currentSortOrder = navigationList[destinationIndex - 1].sort_order || 0; - updatedSortOrder = currentSortOrder + sortOrderDefaultValue; - } else { - // updating project in the middle of the project - const destinationTopProjectSortOrder = navigationList[destinationIndex - 1].sort_order || 0; - const destinationBottomProjectSortOrder = navigationList[destinationIndex].sort_order || 0; - const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2; - updatedSortOrder = updatedValue; - } - - return updatedSortOrder; - }; - - const handleOnNavigationItemDrop = ( - sourceId: string | undefined, - destinationId: string | undefined, - shouldDropAtEnd: boolean - ) => { - if (!sourceId || !destinationId || !workspaceSlug) return; - if (sourceId === destinationId) return; - - const sourceIndex = sortedNavigationItemsKeys.indexOf(sourceId); - const destinationIndex = shouldDropAtEnd - ? sortedNavigationItemsKeys.length - : sortedNavigationItemsKeys.indexOf(destinationId); - - const updatedSortOrder = orderNavigationItem(sourceIndex, destinationIndex, sortedNavigationItems); - - if (updatedSortOrder != undefined) updateWorkspaceItemSortOrder(sourceId, updatedSortOrder); - }; - - const handleClose = useCallback(() => toggleExtendedSidebar(false), [toggleExtendedSidebar]); - - return ( - - {sortedNavigationItems.map((item, index) => ( - - ))} - - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/header.tsx deleted file mode 100644 index ce7845246..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/header.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { HomeOutline, WidgetOutline } from "@makeplane/propel/icons"; -// plane imports -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -// hooks -import { useHome } from "@/hooks/store/use-home"; - -export const WorkspaceDashboardHeader = observer(function WorkspaceDashboardHeader() { - // plane hooks - const { t } = useTranslation(); - // hooks - const { toggleWidgetSettings } = useHome(); - - return ( - <> -
- -
- - } /> - } - /> - -
-
- - - -
- - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx deleted file mode 100644 index 090eefbf6..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { Outlet } from "react-router"; -import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider"; -// plane web components -import { ProjectAppSidebar } from "./_sidebar"; -import { ExtendedProjectSidebar } from "./extended-project-sidebar"; - -function WorkspaceLayout() { - return ( - <> - -
-
-
- - -
- -
-
-
- - ); -} - -export default observer(WorkspaceLayout); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/notifications/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/notifications/layout.tsx deleted file mode 100644 index 5188dfd41..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/notifications/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { NotificationsSidebarRoot } from "@/components/workspace-notifications/sidebar"; - -export default function ProjectInboxIssuesLayout() { - return ( -
- -
- -
-
- ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx deleted file mode 100644 index ad99b55d3..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// components -import { PageHead } from "@/components/core/page-title"; -import { NotificationsRoot } from "@/components/workspace-notifications"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -import type { Route } from "./+types/page"; - -function WorkspaceDashboardPage({ params }: Route.ComponentProps) { - const { workspaceSlug } = params; - // plane hooks - const { t } = useTranslation(); - // hooks - const { currentWorkspace } = useWorkspace(); - // derived values - const pageTitle = currentWorkspace?.name - ? t("notification.page_label", { workspace: currentWorkspace?.name }) - : undefined; - - return ( - <> - - - - ); -} - -export default observer(WorkspaceDashboardPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/page.tsx deleted file mode 100644 index 4d4547ae2..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// components -import { useTranslation } from "@plane/i18n"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { PageHead } from "@/components/core/page-title"; -import { WorkspaceHomeView } from "@/components/home"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -// local components -import { WorkspaceDashboardHeader } from "./header"; - -function WorkspaceDashboardPage() { - const { currentWorkspace } = useWorkspace(); - const { t } = useTranslation(); - // derived values - const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - ${t("home.title")}` : undefined; - - return ( - <> - } /> - - - - - - ); -} - -export default observer(WorkspaceDashboardPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/[profileViewId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/[profileViewId]/page.tsx deleted file mode 100644 index 1f1fe4ce8..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/[profileViewId]/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -// components -import { PageHead } from "@/components/core/page-title"; -import { ProfileIssuesPage } from "@/components/profile/profile-issues"; -import type { Route } from "./+types/page"; - -const ProfilePageHeader = { - assigned: "Profile - Assigned", - created: "Profile - Created", - subscribed: "Profile - Subscribed", -}; - -function isValidProfileViewId(viewId: string): viewId is keyof typeof ProfilePageHeader { - return viewId in ProfilePageHeader; -} - -function ProfileIssuesTypePage({ params }: Route.ComponentProps) { - const { profileViewId } = params; - - if (!isValidProfileViewId(profileViewId)) return null; - - const header = ProfilePageHeader[profileViewId]; - - return ( - <> - - - - ); -} - -export default ProfileIssuesTypePage; diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx deleted file mode 100644 index 7bd93646c..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/** - * 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"; -// plane imports -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -// components -import { PageHead } from "@/components/core/page-title"; -import { DownloadActivityButton } from "@/components/profile/activity/download-button"; -import { WorkspaceActivityListPage } from "@/components/profile/activity/workspace-activity-list"; -// hooks -import { useUserPermissions } from "@/hooks/store/user"; - -const PER_PAGE = 100; - -function ProfileActivityPage() { - // states - const [pageCount, setPageCount] = useState(1); - const [totalPages, setTotalPages] = useState(0); - const [resultsCount, setResultsCount] = useState(0); - // router - const { allowPermissions } = useUserPermissions(); - //hooks - const { t } = useTranslation(); - - const updateTotalPages = (count: number) => setTotalPages(count); - - const updateResultsCount = (count: number) => setResultsCount(count); - - const handleLoadMore = () => setPageCount((prev) => prev + 1); - - const activityPages: React.ReactNode[] = []; - for (let i = 0; i < pageCount; i++) - activityPages.push( - - ); - - const canDownloadActivity = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - return ( - <> - -
-
-

{t("profile.stats.recent_activity.title")}

- {canDownloadActivity && } -
-
- {activityPages} - {pageCount < totalPages && resultsCount !== 0 && ( -
- -
- )} -
-
- - ); -} - -export default observer(ProfileActivityPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx deleted file mode 100644 index 28f415bdd..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// ui -import { observer } from "mobx-react"; -import { useParams, useRouter } from "next/navigation"; -import { ChevronDownOutline, RightSidePaneOutline, YourWorkOutline } from "@makeplane/propel/icons"; -import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import type { IUserProfileProjectSegregation } from "@plane/types"; -import { Breadcrumbs, Header, CustomMenu } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { ProfileIssuesFilter } from "@/components/profile/profile-issues-filter"; -// hooks -import { useAppTheme } from "@/hooks/store/use-app-theme"; -import { useUser, useUserPermissions } from "@/hooks/store/user"; -import { Button } from "@plane/propel/button"; - -type TUserProfileHeader = { - userProjectsData: IUserProfileProjectSegregation | undefined; - type?: string | undefined; - showProfileIssuesFilter?: boolean; -}; - -export const UserProfileHeader = observer(function UserProfileHeader(props: TUserProfileHeader) { - const { userProjectsData, type = undefined, showProfileIssuesFilter } = props; - // router - const { workspaceSlug, userId } = useParams(); - const router = useRouter(); - // store hooks - const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme(); - const { data: currentUser } = useUser(); - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { t } = useTranslation(); - // derived values - const isAuthorized = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - if (!workspaceUserInfo) return null; - - const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB; - - const userName = `${userProjectsData?.user_data?.first_name} ${userProjectsData?.user_data?.last_name}`; - - const isCurrentUser = currentUser?.id === userId; - - const breadcrumbLabel = isCurrentUser ? t("profile.page_label") : `${userName} ${t("profile.work")}`; - - return ( -
- - - } - /> - } - /> - - - -
{showProfileIssuesFilter && }
-
- - {type} - -
- } - customButtonClassName="flex flex-grow justify-center text-secondary text-13" - closeOnSelect - > - <> - {tabsList.map((tab) => ( - router.push(`/${workspaceSlug}/profile/${userId}/${tab.route}`)} - > - {t(tab.i18n_label)} - - ))} - -
- -
-
- - - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx deleted file mode 100644 index f5e2d6663..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { usePathname } from "next/navigation"; -import { Outlet } from "react-router"; -import useSWR from "swr"; -// components -import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ProfileSidebar } from "@/components/profile/sidebar"; -// constants -import { USER_PROFILE_PROJECT_SEGREGATION } from "@plane/constants"; -// hooks -import { useUserPermissions } from "@/hooks/store/user"; -import useSize from "@/hooks/use-window-size"; -// local components -import { UserService } from "@/services/user.service"; -import type { Route } from "./+types/layout"; -import { UserProfileHeader } from "./header"; -import { ProfileIssuesMobileHeader } from "./mobile-header"; -import { ProfileNavbar } from "./navbar"; - -const userService = new UserService(); - -function UseProfileLayout({ params }: Route.ComponentProps) { - // router - const { workspaceSlug, userId } = params; - const pathname = usePathname(); - // store hooks - const { allowPermissions } = useUserPermissions(); - const { t } = useTranslation(); - // derived values - const isAuthorized = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - const windowSize = useSize(); - const isSmallerScreen = windowSize[0] >= 768; - - const { data: userProjectsData } = useSWR(USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug, userId), () => - userService.getUserProfileProjectsSegregation(workspaceSlug, userId) - ); - // derived values - const isAuthorizedPath = - pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed"); - const isIssuesTab = pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed"); - - const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB; - const currentTab = tabsList.find((tab) => pathname === `/${workspaceSlug}/profile/${userId}${tab.selected}`); - - return ( - <> - {/* Passing the type prop from the current route value as we need the header as top most component. - TODO: We are depending on the route path to handle the mobile header type. If the path changes, this logic will break. */} -
-
- - } - mobileHeader={isIssuesTab && } - /> - -
-
- - {isAuthorized || !isAuthorizedPath ? ( -
- -
- ) : ( -
- {t("you_do_not_have_the_permission_to_access_this_page")} -
- )} -
- {!isSmallerScreen && } -
-
-
- {isSmallerScreen && } -
- - ); -} - -export default observer(UseProfileLayout); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx deleted file mode 100644 index fa8b73dbb..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane constants -import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; -// plane i18n -import { useTranslation } from "@plane/i18n"; -// icons -import { ChevronDownOutline } from "@makeplane/propel/icons"; -// types -import type { - IIssueDisplayFilterOptions, - IIssueDisplayProperties, - TIssueLayouts, - EIssueLayoutTypes, -} from "@plane/types"; -import { EIssuesStoreType } from "@plane/types"; -// ui -import { CustomMenu } from "@plane/ui"; -// components -import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters"; -import { IssueLayoutIcon } from "@/components/issues/issue-layouts/layout-icon"; -// hooks -import { useIssues } from "@/hooks/store/use-issues"; - -export const ProfileIssuesMobileHeader = observer(function ProfileIssuesMobileHeader() { - // plane i18n - const { t } = useTranslation(); - // router - const { workspaceSlug, userId } = useParams(); - // store hook - const { - issuesFilter: { issueFilters, updateFilters }, - } = useIssues(EIssuesStoreType.PROFILE); - // derived values - const activeLayout = issueFilters?.displayFilters?.layout; - - const handleLayoutChange = useCallback( - (layout: TIssueLayouts) => { - if (!workspaceSlug || !userId) return; - updateFilters( - workspaceSlug.toString(), - undefined, - EIssueFilterType.DISPLAY_FILTERS, - { layout: layout as EIssueLayoutTypes | undefined }, - userId.toString() - ); - }, - [workspaceSlug, updateFilters, userId] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !userId) return; - updateFilters( - workspaceSlug.toString(), - undefined, - EIssueFilterType.DISPLAY_FILTERS, - updatedDisplayFilter, - userId.toString() - ); - }, - [workspaceSlug, updateFilters, userId] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !userId) return; - updateFilters( - workspaceSlug.toString(), - undefined, - EIssueFilterType.DISPLAY_PROPERTIES, - property, - userId.toString() - ); - }, - [workspaceSlug, updateFilters, userId] - ); - - return ( -
- - {t("common.layout")} - -
- } - customButtonClassName="flex flex-center text-secondary text-13" - closeOnSelect - > - {ISSUE_LAYOUTS.map((layout, index) => { - if (layout.key === "spreadsheet" || layout.key === "gantt_chart" || layout.key === "calendar") return; - return ( - { - handleLayoutChange(ISSUE_LAYOUTS[index].key); - }} - className="flex items-center gap-2" - > - -
{t(layout.i18n_title)}
-
- ); - })} - -
- - {t("common.display")} - -
- } - > - - -
-
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx deleted file mode 100644 index 4d99983dd..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import Link from "next/link"; -import { useParams, usePathname } from "next/navigation"; -// plane imports -import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Header, EHeaderVariant } from "@plane/ui"; -import { cn } from "@plane/utils"; - -type Props = { - isAuthorized: boolean; -}; - -export function ProfileNavbar(props: Props) { - const { isAuthorized } = props; - const { t } = useTranslation(); - const { workspaceSlug, userId } = useParams(); - const pathname = usePathname(); - - const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB; - - return ( -
-
- {tabsList.map((tab) => ( - - - {t(tab.i18n_label)} - - - ))} -
-
- ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/page.tsx deleted file mode 100644 index 6118a867a..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import useSWR from "swr"; -// plane imports -import { GROUP_CHOICES } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import type { IUserStateDistribution, TStateGroups } from "@plane/types"; -import { ContentWrapper } from "@plane/ui"; -// components -import { PageHead } from "@/components/core/page-title"; -import { ProfileActivity } from "@/components/profile/overview/activity"; -import { ProfilePriorityDistribution } from "@/components/profile/overview/priority-distribution"; -import { ProfileStateDistribution } from "@/components/profile/overview/state-distribution"; -import { ProfileStats } from "@/components/profile/overview/stats"; -import { ProfileWorkload } from "@/components/profile/overview/workload"; -// constants -import { USER_PROFILE_DATA } from "@plane/constants"; -// services -import { UserService } from "@/services/user.service"; -import type { Route } from "./+types/page"; -const userService = new UserService(); - -export default function ProfileOverviewPage({ params }: Route.ComponentProps) { - const { workspaceSlug, userId } = params; - - const { t } = useTranslation(); - const { data: userProfile } = useSWR(USER_PROFILE_DATA(workspaceSlug, userId), () => - userService.getUserProfileData(workspaceSlug, userId) - ); - - const stateDistribution: IUserStateDistribution[] = Object.keys(GROUP_CHOICES).map((key) => { - const group = userProfile?.state_distribution.find((g) => g.state_group === key); - - if (group) return group; - else return { state_group: key as TStateGroups, state_count: 0 }; - }); - - return ( - <> - - - - -
- - -
- -
- - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx deleted file mode 100644 index 1b3fc1ef3..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ProjectArchivesHeader } from "../header"; - -export default function ProjectArchiveCyclesLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx deleted file mode 100644 index d25a11bf8..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// components -import { PageHead } from "@/components/core/page-title"; -import { ArchivedCycleLayoutRoot } from "@/components/cycles/archived-cycles"; -import { ArchivedCyclesHeader } from "@/components/cycles/archived-cycles/header"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import type { Route } from "./+types/page"; - -function ProjectArchivedCyclesPage({ params }: Route.ComponentProps) { - // router - const { projectId } = params; - // store hooks - const { getProjectById } = useProject(); - // derived values - const project = getProjectById(projectId); - const pageTitle = project?.name && `${project?.name} - Archived cycles`; - - return ( - <> - -
- - -
- - ); -} - -export default observer(ProjectArchivedCyclesPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx deleted file mode 100644 index fcfdfe3b7..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import { ArchiveOutline, CyclesOutline, ModuleOutline, WorkItemsOutline } from "@makeplane/propel/icons"; -import { Tooltip } from "@makeplane/propel/components/tooltip"; -import { EIssuesStoreType } from "@plane/types"; -// ui -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -// hooks -import { useIssues } from "@/hooks/store/use-issues"; -import { useProject } from "@/hooks/store/use-project"; -import { useAppRouter } from "@/hooks/use-app-router"; -import { usePlatformOS } from "@/hooks/use-platform-os"; -// plane web imports -import { CommonProjectBreadcrumbs } from "@/components/breadcrumbs/common"; - -type TProps = { - activeTab: "issues" | "cycles" | "modules"; -}; - -const PROJECT_ARCHIVES_BREADCRUMB_LIST: { - [key: string]: { - label: string; - href: string; - icon: React.FC & { className?: string }>; - }; -} = { - issues: { - label: "Work items", - href: "/issues", - icon: WorkItemsOutline, - }, - cycles: { - label: "Cycles", - href: "/cycles", - icon: CyclesOutline, - }, - modules: { - label: "Modules", - href: "/modules", - icon: ModuleOutline, - }, -}; - -export const ProjectArchivesHeader = observer(function ProjectArchivesHeader(props: TProps) { - const { activeTab } = props; - // router - const router = useAppRouter(); - const { workspaceSlug, projectId } = useParams(); - // store hooks - const { - issues: { getGroupIssueCount }, - } = useIssues(EIssuesStoreType.ARCHIVED); - const { loader } = useProject(); - // hooks - const { isMobile } = usePlatformOS(); - - const issueCount = getGroupIssueCount(undefined, undefined, false); - - const activeTabBreadcrumbDetail = - PROJECT_ARCHIVES_BREADCRUMB_LIST[activeTab as keyof typeof PROJECT_ARCHIVES_BREADCRUMB_LIST]; - - return ( -
- -
- - - } - /> - } - /> - {activeTabBreadcrumbDetail && ( - } - /> - } - /> - )} - - {activeTab === "issues" && issueCount && issueCount > 0 ? ( - 1 ? "work items" : "work item"} in project's archived`} - layout="stacked" - side="bottom" - disabled={isMobile} - > - - {issueCount} - - - ) : null} -
-
-
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx deleted file mode 100644 index 135167d96..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useRouter } from "next/navigation"; -import useSWR from "swr"; -// ui -import { Banner } from "@makeplane/propel/components/banner"; -import { ArchiveOutline } from "@makeplane/propel/icons"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { Loader } from "@plane/ui"; -// components -import { PageHead } from "@/components/core/page-title"; -import { IssueDetailRoot } from "@/components/issues/issue-detail"; -// constants -// hooks -import { useIssueDetail } from "@/hooks/store/use-issue-detail"; -import { useProject } from "@/hooks/store/use-project"; -import type { Route } from "./+types/page"; - -function ArchivedIssueDetailsPage({ params }: Route.ComponentProps) { - // router - const { workspaceSlug, projectId, archivedIssueId } = params; - const router = useRouter(); - // states - // hooks - const { t } = useTranslation(); - const { - fetchIssue, - issue: { getIssueById }, - } = useIssueDetail(); - - const { getProjectById } = useProject(); - - const { isLoading } = useSWR(`ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}`, () => - fetchIssue(workspaceSlug, projectId, archivedIssueId) - ); - - // derived values - const issue = getIssueById(archivedIssueId); - const project = issue ? getProjectById(issue?.project_id ?? "") : undefined; - const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined; - - if (!issue) return <>; - - const issueLoader = !issue || isLoading; - - return ( - <> - - {issueLoader ? ( - -
- - - - -
-
- - - - -
-
- ) : ( - <> - } - actions={ - - } - /> -
-
- -
-
- - )} - - ); -} - -export default observer(ArchivedIssueDetailsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx deleted file mode 100644 index dca11117a..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import useSWR from "swr"; -// ui -import { ArchiveOutline, WorkItemsOutline } from "@makeplane/propel/icons"; -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { IssueDetailQuickActions } from "@/components/issues/issue-detail/issue-detail-quick-actions"; -// constants -import { ISSUE_DETAILS } from "@plane/constants"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -// plane web -import { ProjectBreadcrumb } from "@/components/breadcrumbs/project"; -// services -import { IssueService } from "@/services/issue"; - -const issueService = new IssueService(); - -export const ProjectArchivedIssueDetailsHeader = observer(function ProjectArchivedIssueDetailsHeader() { - // router - const { workspaceSlug, projectId, archivedIssueId } = useParams(); - // store hooks - const { currentProjectDetails, loader } = useProject(); - - const { data: issueDetails } = useSWR( - workspaceSlug && projectId && archivedIssueId ? ISSUE_DETAILS(archivedIssueId.toString()) : null, - workspaceSlug && projectId && archivedIssueId - ? () => issueService.retrieve(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString()) - : null - ); - - return ( -
- - - - } - /> - } - /> - } - /> - } - /> - - } - /> - - - - - -
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx deleted file mode 100644 index 1d99a7320..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ProjectArchivedIssueDetailsHeader } from "./header"; - -export default function ProjectArchivedIssueDetailLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx deleted file mode 100644 index 6385a3a1c..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ProjectArchivesHeader } from "../../header"; - -export default function ProjectArchiveIssuesLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx deleted file mode 100644 index 0a493cb3d..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// components -import { PageHead } from "@/components/core/page-title"; -import { ArchivedIssuesHeader } from "@/components/issues/archived-issues-header"; -import { ArchivedIssueLayoutRoot } from "@/components/issues/issue-layouts/roots/archived-issue-layout-root"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import type { Route } from "./+types/page"; - -function ProjectArchivedIssuesPage({ params }: Route.ComponentProps) { - // router - const { projectId } = params; - // store hooks - const { getProjectById } = useProject(); - // derived values - const project = getProjectById(projectId); - const pageTitle = project?.name && `${project?.name} - Archived work items`; - - return ( - <> - -
- - -
- - ); -} - -export default observer(ProjectArchivedIssuesPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx deleted file mode 100644 index 0101dcf22..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ProjectArchivesHeader } from "../header"; - -export default function ProjectArchiveModulesLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx deleted file mode 100644 index 39a6a16a6..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// components -import { PageHead } from "@/components/core/page-title"; -import { ArchivedModuleLayoutRoot, ArchivedModulesHeader } from "@/components/modules"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import type { Route } from "./+types/page"; - -function ProjectArchivedModulesPage({ params }: Route.ComponentProps) { - // router - const { projectId } = params; - // store hooks - const { getProjectById } = useProject(); - // derived values - const project = getProjectById(projectId); - const pageTitle = project?.name && `${project?.name} - Archived modules`; - - return ( - <> - -
- - -
- - ); -} - -export default observer(ProjectArchivedModulesPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx deleted file mode 100644 index 414215248..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { cn } from "@plane/utils"; -// assets -import emptyCycle from "@/app/assets/empty-state/cycle.svg?url"; -// components -import { EmptyState } from "@/components/common/empty-state"; -import { PageHead } from "@/components/core/page-title"; -import useCyclesDetails from "@/components/cycles/active-cycle/use-cycles-details"; -import { CycleDetailsSidebar } from "@/components/cycles/analytics-sidebar"; -import { CycleLayoutRoot } from "@/components/issues/issue-layouts/roots/cycle-layout-root"; -// hooks -import { useCycle } from "@/hooks/store/use-cycle"; -import { useProject } from "@/hooks/store/use-project"; -import { useAppRouter } from "@/hooks/use-app-router"; -import useLocalStorage from "@/hooks/use-local-storage"; -import type { Route } from "./+types/page"; - -function CycleDetailPage({ params }: Route.ComponentProps) { - // router - const router = useAppRouter(); - const { workspaceSlug, projectId, cycleId } = params; - // store hooks - const { getCycleById, loader } = useCycle(); - const { getProjectById } = useProject(); - // const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE); - // hooks - const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", false); - - useCyclesDetails({ - workspaceSlug, - projectId, - cycleId, - }); - // derived values - const isSidebarCollapsed = storedValue ? (storedValue === true ? true : false) : false; - const cycle = getCycleById(cycleId); - const project = getProjectById(projectId); - const pageTitle = project?.name && cycle?.name ? `${project?.name} - ${cycle?.name}` : undefined; - - /** - * Toggles the sidebar - */ - const toggleSidebar = () => setValue(!isSidebarCollapsed); - - // const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - return ( - <> - - {!cycle && !loader ? ( - router.push(`/${workspaceSlug}/projects/${projectId}/cycles`), - }} - /> - ) : ( - <> -
-
- -
- {!isSidebarCollapsed && ( -
- -
- )} -
- - )} - - ); -} - -export default observer(CycleDetailPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx deleted file mode 100644 index cb5425b95..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx +++ /dev/null @@ -1,273 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useRef, useState } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// icons -import { BarOutline, CyclesOutline, PreferencesOutline, RightSidePaneOutline } from "@makeplane/propel/icons"; -// plane imports -import { - EIssueFilterType, - EUserPermissions, - EUserPermissionsLevel, - ISSUE_DISPLAY_FILTERS_BY_PAGE, -} from "@plane/constants"; -import { usePlatformOS } from "@plane/hooks"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { IconButton } from "@plane/propel/icon-button"; -import { Tooltip } from "@makeplane/propel/components/tooltip"; -import type { ICustomSearchSelectOption, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; -import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types"; -import { Breadcrumbs, BreadcrumbNavigationSearchDropdown, Header } from "@plane/ui"; -import { cn } from "@plane/utils"; -// components -import { WorkItemsModal } from "@/components/analytics/work-items/modal"; -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SwitcherLabel } from "@/components/common/switcher-label"; -import { CycleQuickActions } from "@/components/cycles/quick-actions"; -import { - DisplayFiltersSelection, - FiltersDropdown, - LayoutSelection, - MobileLayoutSelection, -} from "@/components/issues/issue-layouts/filters"; -import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle"; -// hooks -import { useCommandPalette } from "@/hooks/store/use-command-palette"; -import { useCycle } from "@/hooks/store/use-cycle"; -import { useIssues } from "@/hooks/store/use-issues"; -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -import useLocalStorage from "@/hooks/use-local-storage"; -// plane web imports -import { CommonProjectBreadcrumbs } from "@/components/breadcrumbs/common"; - -export const CycleIssuesHeader = observer(function CycleIssuesHeader() { - // refs - const parentRef = useRef(null); - // states - const [analyticsModal, setAnalyticsModal] = useState(false); - // router - const router = useAppRouter(); - const { workspaceSlug, projectId, cycleId } = useParams(); - // i18n - const { t } = useTranslation(); - // store hooks - const { - issuesFilter: { issueFilters, updateFilters }, - issues: { getGroupIssueCount }, - } = useIssues(EIssuesStoreType.CYCLE); - const { currentProjectCycleIds, getCycleById } = useCycle(); - const { toggleCreateIssueModal } = useCommandPalette(); - const { currentProjectDetails, loader } = useProject(); - const { isMobile } = usePlatformOS(); - const { allowPermissions } = useUserPermissions(); - - const activeLayout = issueFilters?.displayFilters?.layout; - - const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", false); - - const isSidebarCollapsed = storedValue === true; - const toggleSidebar = () => { - setValue(!isSidebarCollapsed); - }; - - const handleLayoutChange = useCallback( - (layout: EIssueLayoutTypes) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId); - }, - [workspaceSlug, projectId, cycleId, updateFilters] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId); - }, - [workspaceSlug, projectId, cycleId, updateFilters] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, cycleId); - }, - [workspaceSlug, projectId, cycleId, updateFilters] - ); - - // derived values - const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; - const isCompletedCycle = cycleDetails?.status?.toLocaleLowerCase() === "completed"; - const canUserCreateIssue = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT - ); - - const switcherOptions = currentProjectCycleIds - ?.map((id) => { - const _cycle = id === cycleId ? cycleDetails : getCycleById(id); - if (!_cycle) return; - return { - value: _cycle.id, - query: _cycle.name, - content: , - }; - }) - .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; - - const workItemsCount = getGroupIssueCount(undefined, undefined, false); - - return ( - <> - setAnalyticsModal(false)} - cycleDetails={cycleDetails ?? undefined} - /> -
- -
- - - } - /> - } - /> - { - router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${value}`); - }} - title={cycleDetails?.name} - icon={ - - - - } - isLast - /> - } - isLast - /> - - {workItemsCount && workItemsCount > 0 ? ( - 1 ? "work items" : "work item"} in this cycle`} - layout="stacked" - side="bottom" - disabled={isMobile} - > - - {workItemsCount} - - - ) : null} -
-
- -
-
- handleLayoutChange(layout)} - selectedLayout={activeLayout} - /> -
-
- handleLayoutChange(layout)} - activeLayout={activeLayout} - /> -
- - } - > - - - - {canUserCreateIssue && ( - <> - - {!isCompletedCycle && ( - - )} - - )} - - -
-
-
- - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx deleted file mode 100644 index 2d92265f4..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { CycleIssuesHeader } from "./header"; -import { CycleIssuesMobileHeader } from "./mobile-header"; - -export default function ProjectCycleIssuesLayout() { - return ( - <> - } mobileHeader={} /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx deleted file mode 100644 index 4c0051471..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useState } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { BoardOutline, CalendarOutline, ChevronDownOutline, ListOutline } from "@makeplane/propel/icons"; -import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types"; -import { EIssuesStoreType } from "@plane/types"; -import { CustomMenu } from "@plane/ui"; -// components -import { WorkItemsModal } from "@/components/analytics/work-items/modal"; -import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters"; -import { IssueLayoutIcon } from "@/components/issues/issue-layouts/layout-icon"; -// hooks -import { useCycle } from "@/hooks/store/use-cycle"; -import { useIssues } from "@/hooks/store/use-issues"; -import { useProject } from "@/hooks/store/use-project"; - -const SUPPORTED_LAYOUTS = [ - { key: "list", titleTranslationKey: "issue.layouts.list", icon: ListOutline }, - { key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: BoardOutline }, - { key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: CalendarOutline }, -]; - -export const CycleIssuesMobileHeader = observer(function CycleIssuesMobileHeader() { - // router - const { workspaceSlug, projectId, cycleId } = useParams(); - // states - const [analyticsModal, setAnalyticsModal] = useState(false); - // plane hooks - const { t } = useTranslation(); - // store hooks - const { currentProjectDetails } = useProject(); - const { getCycleById } = useCycle(); - const { - issuesFilter: { issueFilters, updateFilters }, - } = useIssues(EIssuesStoreType.CYCLE); - // derived values - const activeLayout = issueFilters?.displayFilters?.layout; - const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; - - const handleLayoutChange = useCallback( - (layout: EIssueLayoutTypes) => { - if (!workspaceSlug || !projectId || !cycleId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - { layout: layout }, - cycleId.toString() - ); - }, - [workspaceSlug, projectId, cycleId, updateFilters] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId || !cycleId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - updatedDisplayFilter, - cycleId.toString() - ); - }, - [workspaceSlug, projectId, cycleId, updateFilters] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !projectId || !cycleId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_PROPERTIES, - property, - cycleId.toString() - ); - }, - [workspaceSlug, projectId, cycleId, updateFilters] - ); - - return ( - <> - setAnalyticsModal(false)} - cycleDetails={cycleDetails ?? undefined} - /> -
- {t("common.layout")} - } - customButtonClassName="flex flex-grow justify-center text-secondary text-13" - closeOnSelect - > - {SUPPORTED_LAYOUTS.map((layout, index) => ( - { - handleLayoutChange(ISSUE_LAYOUTS[index].key); - }} - className="flex items-center gap-2" - > - -
{t(layout.titleTranslationKey)}
-
- ))} -
-
- - {t("common.display")} - - - } - > - - -
- - setAnalyticsModal(true)} - className="flex flex-grow justify-center border-l border-subtle text-13 text-secondary" - > - {t("common.analytics")} - -
- - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx deleted file mode 100644 index 2f871518f..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// ui -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { CyclesOutline } from "@makeplane/propel/icons"; -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { CyclesViewHeader } from "@/components/cycles/cycles-view-header"; -// hooks -import { useCommandPalette } from "@/hooks/store/use-command-palette"; -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -// plane web imports -import { CommonProjectBreadcrumbs } from "@/components/breadcrumbs/common"; - -export const CyclesListHeader = observer(function CyclesListHeader() { - // router - const router = useAppRouter(); - const { workspaceSlug, projectId } = useParams(); - - // store hooks - const { toggleCreateCycleModal } = useCommandPalette(); - const { allowPermissions } = useUserPermissions(); - const { currentProjectDetails, loader } = useProject(); - const { t } = useTranslation(); - - const canUserCreateCycle = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT - ); - - return ( -
- - - - } - isLast - /> - } - isLast - /> - - - {canUserCreateCycle && currentProjectDetails ? ( - - - - - ) : ( - <> - )} -
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx deleted file mode 100644 index c1b003348..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { CyclesListHeader } from "./header"; -import { CyclesListMobileHeader } from "./mobile-header"; - -export default function ProjectCyclesListLayout() { - return ( - <> - } mobileHeader={} /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx deleted file mode 100644 index 678445877..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type React from "react"; -import { observer } from "mobx-react"; -// ui -import type { ISvgIcons } from "@plane/propel/icons"; -import { GridOutline, ListOutline, TimelineOutline } from "@makeplane/propel/icons"; -// plane package imports -import type { TCycleLayoutOptions } from "@plane/types"; -import { CustomMenu } from "@plane/ui"; -// hooks -import { useCycleFilter } from "@/hooks/store/use-cycle-filter"; -import { useProject } from "@/hooks/store/use-project"; - -const CYCLE_VIEW_LAYOUTS: { - key: TCycleLayoutOptions; - icon: React.FC; - title: string; -}[] = [ - { - key: "list", - icon: ListOutline, - title: "List layout", - }, - { - key: "board", - icon: GridOutline, - title: "Gallery layout", - }, - { - key: "gantt", - icon: TimelineOutline, - title: "Timeline layout", - }, -]; - -export const CyclesListMobileHeader = observer(function CyclesListMobileHeader() { - const { currentProjectDetails } = useProject(); - // hooks - const { updateDisplayFilters } = useCycleFilter(); - return ( -
- - - Layout - - } - customButtonClassName="flex flex-grow justify-center items-center text-secondary text-13" - closeOnSelect - > - {CYCLE_VIEW_LAYOUTS.map((layout) => { - if (layout.key == "gantt") return; - return ( - { - updateDisplayFilters(currentProjectDetails!.id, { - layout: layout.key, - }); - }} - className="flex items-center gap-2" - > - -
{layout.title}
-
- ); - })} -
-
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx deleted file mode 100644 index c430e1622..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/** - * 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"; -// plane imports -import { useTheme } from "next-themes"; -import { EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { EmptyStateDetailed } from "@plane/propel/empty-state"; -import type { TCycleFilters } from "@plane/types"; -import { EUserProjectRoles } from "@plane/types"; -// components -import { Header, EHeaderVariant } from "@plane/ui"; -import { calculateTotalFilters } from "@plane/utils"; -// assets -import darkEmptyState from "@/app/assets/empty-state/disabled-feature/cycles-dark.webp?url"; -import lightEmptyState from "@/app/assets/empty-state/disabled-feature/cycles-light.webp?url"; -// components -import { PageHead } from "@/components/core/page-title"; -import { CycleAppliedFiltersList } from "@/components/cycles/applied-filters"; -import { CyclesView } from "@/components/cycles/cycles-view"; -import { CycleCreateUpdateModal } from "@/components/cycles/modal"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; -import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader"; -// hooks -import { useCycle } from "@/hooks/store/use-cycle"; -import { useCycleFilter } from "@/hooks/store/use-cycle-filter"; -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -import type { Route } from "./+types/page"; - -function ProjectCyclesPage({ params }: Route.ComponentProps) { - // states - const [createModal, setCreateModal] = useState(false); - // store hooks - const { currentProjectCycleIds, loader } = useCycle(); - const { getProjectById, currentProjectDetails } = useProject(); - // router - const router = useAppRouter(); - const { workspaceSlug, projectId } = params; - // theme hook - const { resolvedTheme } = useTheme(); - // plane hooks - const { t } = useTranslation(); - // cycle filters hook - const { clearAllFilters, currentProjectFilters, updateFilters } = useCycleFilter(); - const { allowPermissions } = useUserPermissions(); - // derived values - const resolvedEmptyState = resolvedTheme === "light" ? lightEmptyState : darkEmptyState; - const totalCycles = currentProjectCycleIds?.length ?? 0; - const project = getProjectById(projectId); - const pageTitle = project?.name ? `${project?.name} - ${t("common.cycles", { count: 2 })}` : undefined; - const hasAdminLevelPermission = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); - const hasMemberLevelPermission = allowPermissions( - [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], - EUserPermissionsLevel.PROJECT - ); - - const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => { - let newValues = currentProjectFilters?.[key] ?? []; - - if (!value) newValues = []; - else newValues = newValues.filter((val) => val !== value); - - updateFilters(projectId, { [key]: newValues }); - }; - - // No access to cycle - if (currentProjectDetails?.cycle_view === false) - return ( -
- { - router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`); - }, - disabled: !hasAdminLevelPermission, - }} - /> -
- ); - - if (loader) return ; - - return ( - <> - -
- setCreateModal(false)} - /> - {totalCycles === 0 ? ( -
- setCreateModal(true), - variant: "primary", - disabled: !hasMemberLevelPermission, - }, - ]} - /> -
- ) : ( - <> - {calculateTotalFilters(currentProjectFilters ?? {}) !== 0 && ( -
- clearAllFilters(projectId)} - handleRemoveFilter={handleRemoveFilter} - /> -
- )} - - - - )} -
- - ); -} - -export default observer(ProjectCyclesPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx deleted file mode 100644 index 99c88f98e..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ProjectInboxHeader } from "@/components/projects/settings/intake/header"; - -export default function ProjectInboxIssuesLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx deleted file mode 100644 index 64739d60b..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -import { useTheme } from "next-themes"; -// plane imports -import { EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { EUserProjectRoles, EInboxIssueCurrentTab } from "@plane/types"; -// assets -import darkIntakeAsset from "@/app/assets/empty-state/disabled-feature/intake-dark.webp?url"; -import lightIntakeAsset from "@/app/assets/empty-state/disabled-feature/intake-light.webp?url"; -// components -import { PageHead } from "@/components/core/page-title"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; -import { InboxIssueRoot } from "@/components/inbox"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -import type { Route } from "./+types/page"; - -function ProjectInboxPage({ params }: Route.ComponentProps) { - /// router - const router = useAppRouter(); - const { workspaceSlug, projectId } = params; - const searchParams = useSearchParams(); - const navigationTab = searchParams.get("currentTab"); - const inboxIssueId = searchParams.get("inboxIssueId"); - // theme hook - const { resolvedTheme } = useTheme(); - // plane hooks - const { t } = useTranslation(); - // hooks - const { currentProjectDetails } = useProject(); - const { allowPermissions } = useUserPermissions(); - // derived values - const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); - const resolvedPath = resolvedTheme === "light" ? lightIntakeAsset : darkIntakeAsset; - - // No access to inbox - if (currentProjectDetails?.inbox_view === false) - return ( -
- { - router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`); - }, - disabled: !canPerformEmptyStateActions, - }} - /> -
- ); - - // derived values - const pageTitle = currentProjectDetails?.name - ? t("inbox_issue.page_label", { - workspace: currentProjectDetails?.name, - }) - : t("inbox_issue.page_label", { - workspace: "Plane", - }); - - const currentNavigationTab = navigationTab - ? navigationTab === "open" - ? EInboxIssueCurrentTab.OPEN - : EInboxIssueCurrentTab.CLOSED - : undefined; - - return ( -
- -
- -
-
- ); -} - -export default observer(ProjectInboxPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx deleted file mode 100644 index 1fab5aa63..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useTheme } from "next-themes"; -import { redirect } from "react-router"; -import { useTranslation } from "@plane/i18n"; -// assets -import emptyIssueDark from "@/app/assets/empty-state/search/issues-dark.webp?url"; -import emptyIssueLight from "@/app/assets/empty-state/search/issues-light.webp?url"; -// components -import { EmptyState } from "@/components/common/empty-state"; -import { LogoSpinner } from "@/components/common/logo-spinner"; -// hooks -import { useAppRouter } from "@/hooks/use-app-router"; -// services -import { IssueService } from "@/services/issue/issue.service"; -// types -import type { Route } from "./+types/page"; - -const issueService = new IssueService(); - -export async function clientLoader({ params }: Route.ClientLoaderArgs) { - const { workspaceSlug, projectId, issueId } = params; - - try { - const data = await issueService.getIssueMetaFromURL(workspaceSlug, projectId, issueId); - - if (data) { - throw redirect(`/${workspaceSlug}/browse/${data.project_identifier}-${data.sequence_id}`); - } - - return { error: true, workspaceSlug }; - } catch (error) { - // If it's a redirect, rethrow it - if (error instanceof Response) { - throw error; - } - // Otherwise return error state - return { error: true, workspaceSlug }; - } -} - -export default function IssueDetailsPage({ loaderData }: Route.ComponentProps) { - const router = useAppRouter(); - const { t } = useTranslation(); - const { resolvedTheme } = useTheme(); - - if (loaderData.error) { - return ( -
- router.push(`/${loaderData.workspaceSlug}/workspace-views/all-issues/`), - }} - /> -
- ); - } - - return ( -
- -
- ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/header.tsx deleted file mode 100644 index f350dbf87..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/header.tsx +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { IssuesHeader } from "@/components/issues/header"; - -export function ProjectIssuesHeader() { - return ; -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx deleted file mode 100644 index c8ac59571..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// components -import { Outlet } from "react-router"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ProjectIssuesHeader } from "./header"; -import { ProjectIssuesMobileHeader } from "./mobile-header"; - -export default function ProjectIssuesLayout() { - return ( - <> - } mobileHeader={} /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx deleted file mode 100644 index 75916ac86..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useState } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { ChevronDownOutline } from "@makeplane/propel/icons"; -import type { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; -import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types"; -// components -import { WorkItemsModal } from "@/components/analytics/work-items/modal"; -import { - DisplayFiltersSelection, - FiltersDropdown, - MobileLayoutSelection, -} from "@/components/issues/issue-layouts/filters"; -// hooks -import { useIssues } from "@/hooks/store/use-issues"; -import { useProject } from "@/hooks/store/use-project"; - -export const ProjectIssuesMobileHeader = observer(function ProjectIssuesMobileHeader() { - // i18n - const { t } = useTranslation(); - const [analyticsModal, setAnalyticsModal] = useState(false); - const { workspaceSlug, projectId } = useParams(); - const { currentProjectDetails } = useProject(); - - // store hooks - const { - issuesFilter: { issueFilters, updateFilters }, - } = useIssues(EIssuesStoreType.PROJECT); - const activeLayout = issueFilters?.displayFilters?.layout; - - const handleLayoutChange = useCallback( - (layout: EIssueLayoutTypes) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); - }, - [workspaceSlug, projectId, updateFilters] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter); - }, - [workspaceSlug, projectId, updateFilters] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property); - }, - [workspaceSlug, projectId, updateFilters] - ); - - return ( - <> - setAnalyticsModal(false)} - projectDetails={currentProjectDetails ?? undefined} - /> -
- -
- - {t("common.display")} - - - } - > - - -
- - -
- - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx deleted file mode 100644 index 15ebb8bbb..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// i18n -import { useTranslation } from "@plane/i18n"; -// components -import { PageHead } from "@/components/core/page-title"; -import { ProjectLayoutRoot } from "@/components/issues/issue-layouts/roots/project-layout-root"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import type { Route } from "./+types/page"; - -function ProjectIssuesPage({ params }: Route.ComponentProps) { - const { projectId } = params; - // i18n - const { t } = useTranslation(); - // store - const { getProjectById } = useProject(); - - // derived values - const project = getProjectById(projectId); - const pageTitle = project?.name ? `${project?.name} - ${t("issue.label", { count: 2 })}` : undefined; // Count is for pluralization - - return ( - <> - -
- -
- - ); -} - -export default observer(ProjectIssuesPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx deleted file mode 100644 index e43b6eb92..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { Outlet } from "react-router"; -// plane imports -import { Header, Row } from "@plane/ui"; -import { cn } from "@plane/utils"; -// components -import { TabNavigationRoot } from "@/components/navigation/tab-navigation-root"; -import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-button"; -// hooks -import { useAppTheme } from "@/hooks/store/use-app-theme"; -import { useProjectNavigationPreferences } from "@/hooks/use-navigation-preferences"; -// layouts -import { ProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper"; -// local imports -import type { Route } from "./+types/layout"; - -function ProjectLayout({ params }: Route.ComponentProps) { - // router - const { workspaceSlug, projectId } = params; - // store hooks - const { sidebarCollapsed } = useAppTheme(); - // preferences - const { preferences: projectPreferences } = useProjectNavigationPreferences(); - - return ( - <> - {projectPreferences.navigationMode === "TABBED" && ( -
- -
-
- {sidebarCollapsed && ( -
- -
- )} -
- - - -
-
-
-
-
- )} - - - - - ); -} - -export default observer(ProjectLayout); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx deleted file mode 100644 index 3a958965c..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import useSWR from "swr"; -// plane imports -import { cn } from "@plane/utils"; -// assets -import emptyModule from "@/app/assets/empty-state/module.svg?url"; -// components -import { EmptyState } from "@/components/common/empty-state"; -import { PageHead } from "@/components/core/page-title"; -import { ModuleLayoutRoot } from "@/components/issues/issue-layouts/roots/module-layout-root"; -import { ModuleAnalyticsSidebar } from "@/components/modules"; -// hooks -import { useModule } from "@/hooks/store/use-module"; -import { useProject } from "@/hooks/store/use-project"; -import { useAppRouter } from "@/hooks/use-app-router"; -import useLocalStorage from "@/hooks/use-local-storage"; -import type { Route } from "./+types/page"; - -function ModuleIssuesPage({ params }: Route.ComponentProps) { - // router - const router = useAppRouter(); - const { workspaceSlug, projectId, moduleId } = params; - // store hooks - const { fetchModuleDetails, getModuleById } = useModule(); - const { getProjectById } = useProject(); - // const { issuesFilter } = useIssues(EIssuesStoreType.MODULE); - // local storage - const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); - const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; - // fetching module details - const { error } = useSWR(`CURRENT_MODULE_DETAILS_${moduleId}`, () => - fetchModuleDetails(workspaceSlug, projectId, moduleId) - ); - // derived values - const projectModule = getModuleById(moduleId); - const project = getProjectById(projectId); - const pageTitle = project?.name && projectModule?.name ? `${project?.name} - ${projectModule?.name}` : undefined; - - const toggleSidebar = () => { - setValue(`${!isSidebarCollapsed}`); - }; - - // const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - return ( - <> - - {error ? ( - router.push(`/${workspaceSlug}/projects/${projectId}/modules`), - }} - /> - ) : ( -
-
- -
- {!isSidebarCollapsed && ( -
- -
- )} -
- )} - - ); -} - -export default observer(ModuleIssuesPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx deleted file mode 100644 index 2a0e462c9..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx +++ /dev/null @@ -1,270 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useRef, useState } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// icons -import { BarOutline, ModuleOutline, PreferencesOutline, RightSidePaneOutline } from "@makeplane/propel/icons"; -// plane imports -import { - EIssueFilterType, - ISSUE_DISPLAY_FILTERS_BY_PAGE, - EUserPermissions, - EUserPermissionsLevel, -} from "@plane/constants"; -import { Button } from "@plane/propel/button"; -import { Tooltip } from "@makeplane/propel/components/tooltip"; -import type { ICustomSearchSelectOption, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; -import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types"; -import { Breadcrumbs, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui"; -import { cn } from "@plane/utils"; -// components -import { WorkItemsModal } from "@/components/analytics/work-items/modal"; -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SwitcherLabel } from "@/components/common/switcher-label"; -import { - DisplayFiltersSelection, - FiltersDropdown, - LayoutSelection, - MobileLayoutSelection, -} from "@/components/issues/issue-layouts/filters"; -import { ModuleQuickActions } from "@/components/modules"; -import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle"; -// hooks -import { useCommandPalette } from "@/hooks/store/use-command-palette"; -import { useIssues } from "@/hooks/store/use-issues"; -import { useModule } from "@/hooks/store/use-module"; -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -import { useIssuesActions } from "@/hooks/use-issues-actions"; -import useLocalStorage from "@/hooks/use-local-storage"; -import { usePlatformOS } from "@/hooks/use-platform-os"; -// plane web imports -import { CommonProjectBreadcrumbs } from "@/components/breadcrumbs/common"; -import { IconButton } from "@plane/propel/icon-button"; - -export const ModuleIssuesHeader = observer(function ModuleIssuesHeader() { - // refs - const parentRef = useRef(null); - // states - const [analyticsModal, setAnalyticsModal] = useState(false); - // router - const router = useAppRouter(); - const { workspaceSlug, projectId, moduleId: routerModuleId } = useParams(); - const moduleId = routerModuleId ? routerModuleId.toString() : undefined; - // hooks - const { isMobile } = usePlatformOS(); - // store hooks - const { - issuesFilter: { issueFilters }, - issues: { getGroupIssueCount }, - } = useIssues(EIssuesStoreType.MODULE); - const { updateFilters } = useIssuesActions(EIssuesStoreType.MODULE); - const { projectModuleIds, getModuleById } = useModule(); - const { toggleCreateIssueModal } = useCommandPalette(); - const { allowPermissions } = useUserPermissions(); - const { currentProjectDetails, loader } = useProject(); - // local storage - const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); - // derived values - const isSidebarCollapsed = storedValue ? storedValue === "true" : false; - const activeLayout = issueFilters?.displayFilters?.layout; - const moduleDetails = moduleId ? getModuleById(moduleId) : undefined; - const canUserCreateIssue = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT - ); - const workItemsCount = getGroupIssueCount(undefined, undefined, false); - - const toggleSidebar = () => { - setValue(`${!isSidebarCollapsed}`); - }; - - const handleLayoutChange = useCallback( - (layout: EIssueLayoutTypes) => { - if (!projectId) return; - updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); - }, - [projectId, updateFilters] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!projectId) return; - updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter); - }, - [projectId, updateFilters] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!projectId) return; - updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property); - }, - [projectId, updateFilters] - ); - - const switcherOptions = projectModuleIds - ?.map((id) => { - const _module = id === moduleId ? moduleDetails : getModuleById(id); - if (!_module) return; - return { - value: _module.id, - query: _module.name, - content: , - }; - }) - .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; - - return ( - <> - setAnalyticsModal(false)} - moduleDetails={moduleDetails ?? undefined} - projectDetails={currentProjectDetails} - /> -
- -
- - - } - isLast - /> - } - isLast - /> - { - router.push(`/${workspaceSlug}/projects/${projectId}/modules/${value}`); - }} - title={moduleDetails?.name} - icon={} - isLast - /> - } - /> - - {workItemsCount && workItemsCount > 0 ? ( - 1 ? "work items" : "work item"} in this module`} - layout="stacked" - side="bottom" - disabled={isMobile} - > - - {workItemsCount} - - - ) : null} -
-
- -
-
- handleLayoutChange(layout)} - selectedLayout={activeLayout} - /> -
-
- handleLayoutChange(layout)} - activeLayout={activeLayout} - /> -
- {moduleId && } - } - > - - -
- - {canUserCreateIssue ? ( - <> - - - - ) : ( - <> - )} - - {moduleId && ( - - )} -
-
- - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx deleted file mode 100644 index f11b9068b..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ModuleIssuesHeader } from "./header"; -import { ModuleIssuesMobileHeader } from "./mobile-header"; - -export default function ProjectModuleIssuesLayout() { - return ( - <> - } mobileHeader={} /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx deleted file mode 100644 index b6a5c670f..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useState } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { BoardOutline, CalendarOutline, ChevronDownOutline, ListOutline } from "@makeplane/propel/icons"; -import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types"; -import { EIssuesStoreType } from "@plane/types"; -import { CustomMenu } from "@plane/ui"; -// components -import { WorkItemsModal } from "@/components/analytics/work-items/modal"; -import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters"; -import { IssueLayoutIcon } from "@/components/issues/issue-layouts/layout-icon"; -// hooks -import { useIssues } from "@/hooks/store/use-issues"; -import { useModule } from "@/hooks/store/use-module"; -import { useProject } from "@/hooks/store/use-project"; - -const SUPPORTED_LAYOUTS = [ - { key: "list", i18n_title: "issue.layouts.list", icon: ListOutline }, - { key: "kanban", i18n_title: "issue.layouts.kanban", icon: BoardOutline }, - { key: "calendar", i18n_title: "issue.layouts.calendar", icon: CalendarOutline }, -]; - -export const ModuleIssuesMobileHeader = observer(function ModuleIssuesMobileHeader() { - // router - const { workspaceSlug, projectId, moduleId } = useParams(); - // states - const [analyticsModal, setAnalyticsModal] = useState(false); - // plane hooks - const { t } = useTranslation(); - // store hooks - const { currentProjectDetails } = useProject(); - const { getModuleById } = useModule(); - const { - issuesFilter: { issueFilters, updateFilters }, - } = useIssues(EIssuesStoreType.MODULE); - // derived values - const activeLayout = issueFilters?.displayFilters?.layout; - const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; - - const handleLayoutChange = useCallback( - (layout: EIssueLayoutTypes) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); - - return ( -
- setAnalyticsModal(false)} - moduleDetails={moduleDetails ?? undefined} - projectDetails={currentProjectDetails} - /> -
- Layout} - customButtonClassName="flex flex-grow justify-center text-secondary text-13" - closeOnSelect - > - {SUPPORTED_LAYOUTS.map((layout, index) => ( - { - handleLayoutChange(ISSUE_LAYOUTS[index].key); - }} - className="flex items-center gap-2" - > - -
{t(layout.i18n_title)}
-
- ))} -
-
- - Display - - - } - > - - -
- - -
-
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx deleted file mode 100644 index 41d166695..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -// ui -import { Button } from "@plane/propel/button"; -import { ModuleOutline } from "@makeplane/propel/icons"; -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { ModuleViewHeader } from "@/components/modules"; -// hooks -import { useCommandPalette } from "@/hooks/store/use-command-palette"; -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -// plane web imports -import { CommonProjectBreadcrumbs } from "@/components/breadcrumbs/common"; - -export const ModulesListHeader = observer(function ModulesListHeader() { - // router - const router = useAppRouter(); - const { workspaceSlug, projectId } = useParams(); - // store hooks - const { toggleCreateModuleModal } = useCommandPalette(); - const { allowPermissions } = useUserPermissions(); - - const { loader } = useProject(); - - const { t } = useTranslation(); - - // auth - const canUserCreateModule = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT - ); - - return ( -
- -
- - - } - isLast - /> - } - isLast - /> - -
-
- - - {canUserCreateModule ? ( - - ) : ( - <> - )} - -
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx deleted file mode 100644 index 46eb9f101..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ModulesListHeader } from "./header"; -import { ModulesListMobileHeader } from "./mobile-header"; - -export default function ProjectModulesListLayout() { - return ( - <> - } mobileHeader={} /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx deleted file mode 100644 index 414579284..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { MODULE_VIEW_LAYOUTS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { ChevronDownOutline } from "@makeplane/propel/icons"; -import { CustomMenu, Row } from "@plane/ui"; -import { ModuleLayoutIcon } from "@/components/modules"; -import { useModuleFilter } from "@/hooks/store/use-module-filter"; -import { useProject } from "@/hooks/store/use-project"; - -export const ModulesListMobileHeader = observer(function ModulesListMobileHeader() { - const { currentProjectDetails } = useProject(); - const { updateDisplayFilters } = useModuleFilter(); - const { t } = useTranslation(); - - return ( -
- - Layout - - } - customButtonClassName="flex flex-grow justify-center items-center text-secondary text-13" - closeOnSelect - > - {MODULE_VIEW_LAYOUTS.map((layout) => { - if (layout.key == "gantt") return; - return ( - { - updateDisplayFilters(currentProjectDetails!.id.toString(), { layout: layout.key }); - }} - className="flex items-center gap-2" - > - -
{t(layout.i18n_title)}
-
- ); - })} -
-
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx deleted file mode 100644 index 12e0be1ec..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback } from "react"; -import { observer } from "mobx-react"; -import { useTheme } from "next-themes"; -// plane imports -import { EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import type { TModuleFilters } from "@plane/types"; -import { EUserProjectRoles } from "@plane/types"; -import { calculateTotalFilters } from "@plane/utils"; -// assets -import darkModulesAsset from "@/app/assets/empty-state/disabled-feature/modules-dark.webp?url"; -import lightModulesAsset from "@/app/assets/empty-state/disabled-feature/modules-light.webp?url"; -// components -import { PageHead } from "@/components/core/page-title"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; -import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules"; -// hooks -import { useModuleFilter } from "@/hooks/store/use-module-filter"; -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -import type { Route } from "./+types/page"; - -function ProjectModulesPage({ params }: Route.ComponentProps) { - // router - const router = useAppRouter(); - const { workspaceSlug, projectId } = params; - // theme hook - const { resolvedTheme } = useTheme(); - // plane hooks - const { t } = useTranslation(); - // store - const { getProjectById, currentProjectDetails } = useProject(); - const { - currentProjectFilters = {}, - currentProjectDisplayFilters, - clearAllFilters, - updateFilters, - updateDisplayFilters, - } = useModuleFilter(); - const { allowPermissions } = useUserPermissions(); - // derived values - const project = getProjectById(projectId); - const pageTitle = project?.name ? `${project?.name} - Modules` : undefined; - const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); - const resolvedPath = resolvedTheme === "light" ? lightModulesAsset : darkModulesAsset; - - const handleRemoveFilter = useCallback( - (key: keyof TModuleFilters, value: string | null) => { - let newValues = currentProjectFilters[key] ?? []; - - if (!value) newValues = []; - else newValues = newValues.filter((val) => val !== value); - - updateFilters(projectId, { [key]: newValues }); - }, - [currentProjectFilters, projectId, updateFilters] - ); - - // No access to - if (currentProjectDetails?.module_view === false) - return ( -
- { - router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`); - }, - disabled: !canPerformEmptyStateActions, - }} - /> -
- ); - - return ( - <> - -
- {(calculateTotalFilters(currentProjectFilters) !== 0 || currentProjectDisplayFilters?.favorites) && ( - clearAllFilters(projectId)} - handleRemoveFilter={handleRemoveFilter} - handleDisplayFiltersUpdate={(val) => updateDisplayFilters(projectId, val)} - alwaysAllowEditing - /> - )} - -
- - ); -} - -export default observer(ProjectModulesPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx deleted file mode 100644 index fc1f76aa5..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useEffect, useMemo } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import useSWR from "swr"; -// plane types -import { getButtonStyling } from "@plane/propel/button"; -import type { TSearchEntityRequestPayload, TWebhookConnectionQueryParams } from "@plane/types"; -import { EFileAssetType } from "@plane/types"; -// plane ui -// plane utils -import { cn } from "@plane/utils"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { PageHead } from "@/components/core/page-title"; -import { IssuePeekOverview } from "@/components/issues/peek-overview"; -import type { TPageRootConfig, TPageRootHandlers } from "@/components/pages/editor/page-root"; -import { PageRoot } from "@/components/pages/editor/page-root"; -// hooks -import { useEditorConfig } from "@/hooks/editor"; -import { useEditorAsset } from "@/hooks/store/use-editor-asset"; -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useAppRouter } from "@/hooks/use-app-router"; -// plane web hooks -import { EPageStoreType, usePage, usePageStore } from "@/hooks/store"; -// plane web services -import { WorkspaceService } from "@/services/workspace.service"; -// services -import { ProjectPageService, ProjectPageVersionService } from "@/services/page"; -import type { Route } from "./+types/page"; -const workspaceService = new WorkspaceService(); -const projectPageService = new ProjectPageService(); -const projectPageVersionService = new ProjectPageVersionService(); - -const storeType = EPageStoreType.PROJECT; - -function PageDetailsPage({ params }: Route.ComponentProps) { - // router - const router = useAppRouter(); - const { workspaceSlug, projectId, pageId } = params; - // store hooks - const { createPage, fetchPageDetails } = usePageStore(storeType); - const page = usePage({ - pageId, - storeType, - }); - const { getWorkspaceBySlug } = useWorkspace(); - const { uploadEditorAsset, duplicateEditorAsset } = useEditorAsset(); - // derived values - const workspaceId = workspaceSlug ? (getWorkspaceBySlug(workspaceSlug)?.id ?? "") : ""; - const { canCurrentUserAccessPage, id, name, updateDescription } = page ?? {}; - // entity search handler - const fetchEntityCallback = useCallback( - async (payload: TSearchEntityRequestPayload) => - await workspaceService.searchEntity(workspaceSlug, { - ...payload, - project_id: projectId, - }), - [projectId, workspaceSlug] - ); - // editor config - const { getEditorFileHandlers } = useEditorConfig(); - // fetch page details - const { error: pageDetailsError } = useSWR( - `PAGE_DETAILS_${pageId}`, - () => fetchPageDetails(workspaceSlug, projectId, pageId), - { - revalidateIfStale: true, - revalidateOnFocus: true, - revalidateOnReconnect: true, - } - ); - // page root handlers - const pageRootHandlers: TPageRootHandlers = useMemo( - () => ({ - create: createPage, - fetchAllVersions: async (pageId) => - await projectPageVersionService.fetchAllVersions(workspaceSlug, projectId, pageId), - fetchDescriptionBinary: async () => { - if (!id) return; - return await projectPageService.fetchDescriptionBinary(workspaceSlug, projectId, id); - }, - fetchEntity: fetchEntityCallback, - fetchVersionDetails: async (pageId, versionId) => - await projectPageVersionService.fetchVersionById(workspaceSlug, projectId, pageId, versionId), - restoreVersion: async (pageId, versionId) => - await projectPageVersionService.restoreVersion(workspaceSlug, projectId, pageId, versionId), - getRedirectionLink: (pageId) => { - if (pageId) { - return `/${workspaceSlug}/projects/${projectId}/pages/${pageId}`; - } else { - return `/${workspaceSlug}/projects/${projectId}/pages`; - } - }, - updateDescription: updateDescription ?? (async () => {}), - }), - [createPage, fetchEntityCallback, id, updateDescription, workspaceSlug, projectId] - ); - // page root config - const pageRootConfig: TPageRootConfig = useMemo( - () => ({ - fileHandler: getEditorFileHandlers({ - projectId, - uploadFile: async (blockId, file) => { - const { asset_id } = await uploadEditorAsset({ - blockId, - data: { - entity_identifier: id ?? "", - entity_type: EFileAssetType.PAGE_DESCRIPTION, - }, - file, - projectId, - workspaceSlug, - }); - return asset_id; - }, - duplicateFile: async (assetId: string) => { - const { asset_id } = await duplicateEditorAsset({ - assetId, - entityId: id, - entityType: EFileAssetType.PAGE_DESCRIPTION, - projectId, - workspaceSlug, - }); - return asset_id; - }, - workspaceId, - workspaceSlug, - }), - }), - [getEditorFileHandlers, projectId, workspaceId, workspaceSlug, uploadEditorAsset, id, duplicateEditorAsset] - ); - - const webhookConnectionParams: TWebhookConnectionQueryParams = useMemo( - () => ({ - documentType: "project_page", - projectId, - workspaceSlug, - }), - [projectId, workspaceSlug] - ); - - useEffect(() => { - if (page?.deleted_at && page?.id) { - router.push(pageRootHandlers.getRedirectionLink()); - } - }, [page?.deleted_at, page?.id, router, pageRootHandlers]); - - if ((!page || !id) && !pageDetailsError) - return ( -
- -
- ); - - if (pageDetailsError || !canCurrentUserAccessPage) - return ( -
-

Page not found

-

- The page you are trying to access doesn{"'"}t exist or you don{"'"}t have permission to view it. -

- - View other Pages - -
- ); - - if (!page) return null; - - return ( - <> - -
-
- - -
-
- - ); -} - -export default observer(PageDetailsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx deleted file mode 100644 index d005cfb00..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { PagesOutline } from "@makeplane/propel/icons"; -import type { ICustomSearchSelectOption } from "@plane/types"; -import { Breadcrumbs, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui"; -import { getPageName } from "@plane/utils"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { PageAccessIcon } from "@/components/common/page-access-icon"; -import { SwitcherIcon, SwitcherLabel } from "@/components/common/switcher-label"; -import { PageHeaderActions } from "@/components/pages/header/actions"; -import { PageSyncingBadge } from "@/components/pages/header/syncing-badge"; -import { CommonProjectBreadcrumbs } from "@/components/breadcrumbs/common"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useAppRouter } from "@/hooks/use-app-router"; -import { EPageStoreType, usePage, usePageStore } from "@/hooks/store"; - -export interface IPagesHeaderProps { - showButton?: boolean; -} - -const storeType = EPageStoreType.PROJECT; - -export const PageDetailsHeader = observer(function PageDetailsHeader() { - // router - const router = useAppRouter(); - const { workspaceSlug, pageId, projectId } = useParams(); - // store hooks - const { loader } = useProject(); - const { getPageById, getCurrentProjectPageIds } = usePageStore(storeType); - const page = usePage({ - pageId: pageId?.toString() ?? "", - storeType, - }); - // derived values - const projectPageIds = getCurrentProjectPageIds(projectId?.toString()); - - const switcherOptions = projectPageIds - .map((id) => { - const _page = id === pageId ? page : getPageById(id); - if (!_page) return; - return { - value: _page.id, - query: _page.name, - content: ( -
- - -
- ), - }; - }) - .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; - - if (!page) return null; - - return ( -
- -
- - - } - /> - } - /> - - { - router.push(`/${workspaceSlug}/projects/${projectId}/pages/${value}`); - }} - title={getPageName(page?.name)} - icon={ - - - - } - isLast - /> - } - /> - -
-
- - - - -
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx deleted file mode 100644 index b8f408091..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// component -import { Outlet } from "react-router"; -import useSWR from "swr"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -// plane web hooks -import { EPageStoreType, usePageStore } from "@/hooks/store"; -// local components -import type { Route } from "./+types/layout"; -import { PageDetailsHeader } from "./header"; - -export default function ProjectPageDetailsLayout({ params }: Route.ComponentProps) { - const { workspaceSlug, projectId } = params; - const { fetchPagesList } = usePageStore(EPageStoreType.PROJECT); - // fetching pages list - useSWR(`PROJECT_PAGES_${projectId}`, () => fetchPagesList(workspaceSlug, projectId)); - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx deleted file mode 100644 index 7105d1cd7..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/** - * 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 { useParams, useRouter, useSearchParams } from "next/navigation"; -// constants -import { EPageAccess } from "@plane/constants"; -// plane types -import { Button } from "@plane/propel/button"; -import { PagesOutline } from "@makeplane/propel/icons"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import type { TPage } from "@plane/types"; -// plane ui -import { Breadcrumbs, Header } from "@plane/ui"; -// helpers -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -// plane web imports -import { CommonProjectBreadcrumbs } from "@/components/breadcrumbs/common"; -import { EPageStoreType, usePageStore } from "@/hooks/store"; - -export const PagesListHeader = observer(function PagesListHeader() { - // states - const [isCreatingPage, setIsCreatingPage] = useState(false); - // router - const router = useRouter(); - const { workspaceSlug, projectId } = useParams(); - const searchParams = useSearchParams(); - const pageType = searchParams.get("type"); - // store hooks - const { currentProjectDetails, loader } = useProject(); - const { canCurrentUserCreatePage, createPage } = usePageStore(EPageStoreType.PROJECT); - // handle page create - const handleCreatePage = async () => { - setIsCreatingPage(true); - - const payload: Partial = { - access: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC, - }; - - await createPage(payload) - // oxlint-disable-next-line promise/always-return - .then((res) => { - const pageId = `/${workspaceSlug}/projects/${currentProjectDetails?.id}/pages/${res?.id}`; - router.push(pageId); - }) - .catch((err) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: err?.data?.error || "Page could not be created. Please try again.", - }); - }) - .finally(() => setIsCreatingPage(false)); - }; - - return ( -
- - - - } - isLast - /> - } - isLast - /> - - - {canCurrentUserCreatePage && ( - - - - )} -
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx deleted file mode 100644 index b803854bd..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// components -import { Outlet } from "react-router"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -// local components -import { PagesListHeader } from "./header"; - -export default function ProjectPagesListLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx deleted file mode 100644 index 540e87cc1..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -import { useTheme } from "next-themes"; -// plane imports -import { EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import type { TPageNavigationTabs } from "@plane/types"; -import { EUserProjectRoles } from "@plane/types"; -// assets -import darkPagesAsset from "@/app/assets/empty-state/disabled-feature/pages-dark.webp?url"; -import lightPagesAsset from "@/app/assets/empty-state/disabled-feature/pages-light.webp?url"; -// components -import { PageHead } from "@/components/core/page-title"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; -import { PagesListRoot } from "@/components/pages/list/root"; -import { PagesListView } from "@/components/pages/pages-list-view"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -// plane web hooks -import { EPageStoreType } from "@/hooks/store"; -import type { Route } from "./+types/page"; - -const getPageType = (pageType?: string | null): TPageNavigationTabs => { - if (pageType === "private") return "private"; - if (pageType === "archived") return "archived"; - return "public"; -}; - -function ProjectPagesPage({ params }: Route.ComponentProps) { - // router - const router = useAppRouter(); - const searchParams = useSearchParams(); - const type = searchParams.get("type"); - const { workspaceSlug, projectId } = params; - // theme hook - const { resolvedTheme } = useTheme(); - // plane hooks - const { t } = useTranslation(); - // store hooks - const { getProjectById, currentProjectDetails } = useProject(); - const { allowPermissions } = useUserPermissions(); - // derived values - const project = getProjectById(projectId); - const pageTitle = project?.name ? `${project?.name} - Pages` : undefined; - const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); - const resolvedPath = resolvedTheme === "light" ? lightPagesAsset : darkPagesAsset; - const pageType = getPageType(type); - - // No access to cycle - if (currentProjectDetails?.page_view === false) - return ( -
- { - router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`); - }, - disabled: !canPerformEmptyStateActions, - }} - /> -
- ); - return ( - <> - - - - - - ); -} - -export default observer(ProjectPagesPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx deleted file mode 100644 index 5db2b2fcc..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx +++ /dev/null @@ -1,221 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useRef } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; - -// plane imports -import { - EIssueFilterType, - ISSUE_DISPLAY_FILTERS_BY_PAGE, - EUserPermissions, - EUserPermissionsLevel, -} from "@plane/constants"; -import { Button } from "@plane/propel/button"; -import { LockOutline, ViewsOutline } from "@makeplane/propel/icons"; -import { Tooltip } from "@makeplane/propel/components/tooltip"; -import type { ICustomSearchSelectOption, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; -import { EIssuesStoreType, EViewAccess, EIssueLayoutTypes } from "@plane/types"; -import { Breadcrumbs, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SwitcherIcon, SwitcherLabel } from "@/components/common/switcher-label"; -import { DisplayFiltersSelection, FiltersDropdown, LayoutSelection } from "@/components/issues/issue-layouts/filters"; -import { ViewQuickActions } from "@/components/views/quick-actions"; -import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle"; -// hooks -import { useCommandPalette } from "@/hooks/store/use-command-palette"; -import { useIssues } from "@/hooks/store/use-issues"; -import { useProject } from "@/hooks/store/use-project"; -import { useProjectView } from "@/hooks/store/use-project-view"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -// plane web imports -import { CommonProjectBreadcrumbs } from "@/components/breadcrumbs/common"; - -export const ProjectViewIssuesHeader = observer(function ProjectViewIssuesHeader() { - // refs - const parentRef = useRef(null); - // router - const router = useAppRouter(); - const { workspaceSlug, projectId, viewId: routerViewId } = useParams(); - const viewId = routerViewId ? routerViewId.toString() : undefined; - // store hooks - const { - issuesFilter: { issueFilters, updateFilters }, - } = useIssues(EIssuesStoreType.PROJECT_VIEW); - const { toggleCreateIssueModal } = useCommandPalette(); - const { allowPermissions } = useUserPermissions(); - - const { currentProjectDetails, loader } = useProject(); - const { projectViewIds, getViewById } = useProjectView(); - - const activeLayout = issueFilters?.displayFilters?.layout; - - const handleLayoutChange = useCallback( - (layout: EIssueLayoutTypes) => { - if (!workspaceSlug || !projectId || !viewId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - { layout: layout }, - viewId.toString() - ); - }, - [workspaceSlug, projectId, viewId, updateFilters] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId || !viewId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - updatedDisplayFilter, - viewId.toString() - ); - }, - [workspaceSlug, projectId, viewId, updateFilters] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !projectId || !viewId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_PROPERTIES, - property, - viewId.toString() - ); - }, - [workspaceSlug, projectId, viewId, updateFilters] - ); - - const viewDetails = viewId ? getViewById(viewId.toString()) : null; - - const canUserCreateIssue = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT - ); - - if (!viewDetails) return; - - const switcherOptions = projectViewIds - ?.map((id) => { - const _view = id === viewId ? viewDetails : getViewById(id); - if (!_view) return; - return { - value: _view.id, - query: _view.name, - content: , - }; - }) - .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; - - return ( -
- - - - } - /> - } - /> - { - router.push(`/${workspaceSlug}/projects/${projectId}/views/${value}`); - }} - title={viewDetails?.name} - icon={ - - - - } - isLast - /> - } - /> - - - {viewDetails?.access === EViewAccess.PRIVATE ? ( -
- - - -
- ) : ( - <> - )} -
- - <> - {!viewDetails.is_locked && ( - handleLayoutChange(layout)} - selectedLayout={activeLayout} - /> - )} - {viewId && } - {!viewDetails.is_locked && ( - - - - )} - - {canUserCreateIssue && ( - - )} -
- -
-
-
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx deleted file mode 100644 index c4c03eb49..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import useSWR from "swr"; -// assets -import emptyView from "@/app/assets/empty-state/view.svg?url"; -// components -import { EmptyState } from "@/components/common/empty-state"; -import { PageHead } from "@/components/core/page-title"; -import { ProjectViewLayoutRoot } from "@/components/issues/issue-layouts/roots/project-view-layout-root"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useProjectView } from "@/hooks/store/use-project-view"; -import { useAppRouter } from "@/hooks/use-app-router"; -import type { Route } from "./+types/page"; - -function ProjectViewIssuesPage({ params }: Route.ComponentProps) { - // router - const router = useAppRouter(); - const { workspaceSlug, projectId, viewId } = params; - // store hooks - const { fetchViewDetails, getViewById } = useProjectView(); - const { getProjectById } = useProject(); - // derived values - const projectView = getViewById(viewId); - const project = getProjectById(projectId); - const pageTitle = project?.name && projectView?.name ? `${project?.name} - ${projectView?.name}` : undefined; - - const { error } = useSWR(`VIEW_DETAILS_${viewId}`, () => fetchViewDetails(workspaceSlug, projectId, viewId)); - - if (error) { - return ( - router.push(`/${workspaceSlug}/projects/${projectId}/views`), - }} - /> - ); - } - - return ( - <> - - - - ); -} - -export default observer(ProjectViewIssuesPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx deleted file mode 100644 index b33381b93..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -// local components -import { ProjectViewIssuesHeader } from "./[viewId]/header"; - -export default function ProjectViewIssuesLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx deleted file mode 100644 index a4304aab6..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// ui -import { Button } from "@plane/propel/button"; -import { ViewsOutline } from "@makeplane/propel/icons"; -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { ViewListHeader } from "@/components/views/view-list-header"; -// hooks -import { useCommandPalette } from "@/hooks/store/use-command-palette"; -import { useProject } from "@/hooks/store/use-project"; -// plane web imports -import { CommonProjectBreadcrumbs } from "@/components/breadcrumbs/common"; - -export const ProjectViewsHeader = observer(function ProjectViewsHeader() { - const { workspaceSlug, projectId } = useParams(); - // store hooks - const { toggleCreateViewModal } = useCommandPalette(); - const { loader } = useProject(); - - return ( - <> -
- - - - } - isLast - /> - } - isLast - /> - - - - -
- -
-
-
- - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx deleted file mode 100644 index 0b6dda9dc..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -// local components -import { ProjectViewsHeader } from "./header"; -import { ViewMobileHeader } from "./mobile-header"; - -export default function ProjectViewsListLayout() { - return ( - <> - } mobileHeader={} /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx deleted file mode 100644 index bbe4d131a..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// icons -import { ChevronDownOutline, FilterOutline } from "@makeplane/propel/icons"; -// components -import { Row } from "@plane/ui"; -import { FiltersDropdown } from "@/components/issues/issue-layouts/filters"; -import { ViewFiltersSelection } from "@/components/views/filters/filter-selection"; -import { ViewOrderByDropdown } from "@/components/views/filters/order-by"; -// hooks -import { useMember } from "@/hooks/store/use-member"; -import { useProjectView } from "@/hooks/store/use-project-view"; - -export const ViewMobileHeader = observer(function ViewMobileHeader() { - // store hooks - const { filters, updateFilters } = useProjectView(); - const { - project: { projectMemberIds }, - } = useMember(); - - return ( - <> -
- - { - if (val.key) updateFilters("sortKey", val.key); - if (val.order) updateFilters("sortBy", val.order); - }} - isMobile - /> - -
- } - title="Filters" - placement="bottom-end" - isFiltersApplied={false} - menuButton={ - - Filters - - - } - > - - -
-
- - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx deleted file mode 100644 index b8f7f3688..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback } from "react"; -import { observer } from "mobx-react"; -import { useTheme } from "next-themes"; -// plane imports -import { EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import type { EViewAccess, TViewFilterProps } from "@plane/types"; -import { EUserProjectRoles } from "@plane/types"; -import { Header, EHeaderVariant } from "@plane/ui"; -import { calculateTotalFilters } from "@plane/utils"; -// assets -import darkViewsAsset from "@/app/assets/empty-state/disabled-feature/views-dark.webp?url"; -import lightViewsAsset from "@/app/assets/empty-state/disabled-feature/views-light.webp?url"; -// components -import { PageHead } from "@/components/core/page-title"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; -import { ViewAppliedFiltersList } from "@/components/views/applied-filters"; -import { ProjectViewsList } from "@/components/views/views-list"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useProjectView } from "@/hooks/store/use-project-view"; -import { useUserPermissions } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -import type { Route } from "./+types/page"; - -function ProjectViewsPage({ params }: Route.ComponentProps) { - // router - const router = useAppRouter(); - const { workspaceSlug, projectId } = params; - // theme hook - const { resolvedTheme } = useTheme(); - // plane hooks - const { t } = useTranslation(); - // store - const { getProjectById, currentProjectDetails } = useProject(); - const { filters, updateFilters, clearAllFilters } = useProjectView(); - const { allowPermissions } = useUserPermissions(); - // derived values - const project = getProjectById(projectId); - const pageTitle = project?.name ? `${project?.name} - Views` : undefined; - const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); - const resolvedPath = resolvedTheme === "light" ? lightViewsAsset : darkViewsAsset; - - const handleRemoveFilter = useCallback( - (key: keyof TViewFilterProps, value: string | EViewAccess | null) => { - let newValues = filters.filters?.[key]; - - if (key === "favorites") { - newValues = !!value; - } - if (Array.isArray(newValues)) { - if (!value) newValues = []; - else newValues = newValues.filter((val) => val !== value) as string[]; - } - - updateFilters("filters", { [key]: newValues }); - }, - [filters.filters, updateFilters] - ); - - const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0; - - // No access to - if (currentProjectDetails?.issue_views_view === false) - return ( -
- { - router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`); - }, - disabled: !canPerformEmptyStateActions, - }} - /> -
- ); - - return ( - <> - - {isFiltersApplied && ( -
- -
- )} - - - ); -} - -export default observer(ProjectViewsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx deleted file mode 100644 index 219d9dbc0..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// components -import { Outlet } from "react-router"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ProjectsListMobileHeader } from "@/components/projects/mobile-header"; -import { ProjectsBaseHeader } from "@/components/project/header"; - -export default function ProjectListLayout() { - return ( - <> - } mobileHeader={} /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx deleted file mode 100644 index 70a471fad..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { ProjectPageRoot } from "@/components/projects/page"; - -function ProjectsPage() { - return ; -} - -export default ProjectsPage; diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(list)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(list)/layout.tsx deleted file mode 100644 index 0f39520b2..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(list)/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ProjectsListMobileHeader } from "@/components/projects/mobile-header"; -import { ProjectsBaseHeader } from "@/components/project/header"; - -export default function ProjectListLayout() { - return ( - <> - } mobileHeader={} /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(list)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(list)/page.tsx deleted file mode 100644 index 70a471fad..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(list)/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { ProjectPageRoot } from "@/components/projects/page"; - -function ProjectsPage() { - return ; -} - -export default ProjectsPage; diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx deleted file mode 100644 index c94b1090c..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { isEmpty } from "lodash-es"; -import { observer } from "mobx-react"; -// plane helpers -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -// components -import { SidebarWrapper } from "@/components/sidebar/sidebar-wrapper"; -import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu"; -import { SidebarProjectsList } from "@/components/workspace/sidebar/projects-list"; -import { SidebarQuickActions } from "@/components/workspace/sidebar/quick-actions"; -import { SidebarMenuItems } from "@/components/workspace/sidebar/sidebar-menu-items"; -// hooks -import { useFavorite } from "@/hooks/store/use-favorite"; -import { useUserPermissions } from "@/hooks/store/user"; - -export const AppSidebar = observer(function AppSidebar() { - // store hooks - const { allowPermissions } = useUserPermissions(); - const { groupedFavorites } = useFavorite(); - - // derived values - const canPerformWorkspaceMemberActions = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - const isFavoriteEmpty = isEmpty(groupedFavorites); - - return ( - }> - - {/* Favorites Menu */} - {canPerformWorkspaceMemberActions && !isFavoriteEmpty && } - {/* Projects List */} - - - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/star-us-link.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/star-us-link.tsx deleted file mode 100644 index c4039f7ab..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/star-us-link.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useTheme } from "next-themes"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// assets -import githubBlackImage from "@/app/assets/logos/github-black.png?url"; -import githubWhiteImage from "@/app/assets/logos/github-white.png?url"; - -export function StarUsOnGitHubLink() { - // plane hooks - const { t } = useTranslation(); - // hooks - const { resolvedTheme } = useTheme(); - const imageSrc = resolvedTheme === "dark" ? githubWhiteImage : githubBlackImage; - - return ( - - - {t("home.star_us_on_github")} - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/stickies/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/stickies/header.tsx deleted file mode 100644 index aef45ee76..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/stickies/header.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { Button } from "@plane/propel/button"; -import { MultipleStickyOutline } from "@makeplane/propel/icons"; -import { Breadcrumbs, Header } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { StickySearch } from "@/components/stickies/modal/search"; -import { useStickyOperations } from "@/components/stickies/sticky/use-operations"; -// hooks -import { useSticky } from "@/hooks/use-stickies"; - -export const WorkspaceStickyHeader = observer(function WorkspaceStickyHeader() { - const { workspaceSlug } = useParams(); - // hooks - const { creatingSticky, toggleShowNewSticky } = useSticky(); - const { stickyOperations } = useStickyOperations({ workspaceSlug: workspaceSlug?.toString() }); - - return ( - <> -
- -
- - } - /> - } - /> - -
-
- - - - - -
- - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/stickies/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/stickies/layout.tsx deleted file mode 100644 index 14be62a14..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/stickies/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { WorkspaceStickyHeader } from "./header"; - -export default function WorkspaceStickiesLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/stickies/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/stickies/page.tsx deleted file mode 100644 index d925b2ce4..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/stickies/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// components -import { PageHead } from "@/components/core/page-title"; -import { StickiesInfinite } from "@/components/stickies/layout/stickies-infinite"; - -export default function WorkspaceStickiesPage() { - return ( - <> - -
- -
- - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/[globalViewId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/[globalViewId]/page.tsx deleted file mode 100644 index 3d62aded3..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/[globalViewId]/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * 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"; -// plane imports -import { DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants"; -// components -import { PageHead } from "@/components/core/page-title"; -import { AllIssueLayoutRoot } from "@/components/issues/issue-layouts/roots/all-issue-layout-root"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -import type { Route } from "./+types/page"; - -function GlobalViewIssuesPage({ params }: Route.ComponentProps) { - // router - const { globalViewId } = params; - // store hooks - const { currentWorkspace } = useWorkspace(); - // states - const [isLoading, setIsLoading] = useState(false); - - // derived values - const defaultView = DEFAULT_GLOBAL_VIEWS_LIST.find((view) => view.key === globalViewId); - const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - All Views` : undefined; - - // handlers - const toggleLoading = (value: boolean) => setIsLoading(value); - return ( - <> - - - - ); -} - -export default observer(GlobalViewIssuesPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx deleted file mode 100644 index c852b4d53..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useCallback, useMemo, useState } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE, DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { ViewsOutline } from "@makeplane/propel/icons"; -import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, ICustomSearchSelectOption } from "@plane/types"; -import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types"; -import { Breadcrumbs, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SwitcherLabel } from "@/components/common/switcher-label"; -import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters"; -import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle"; -import { DefaultWorkspaceViewQuickActions } from "@/components/workspace/views/default-view-quick-action"; -import { CreateUpdateWorkspaceViewModal } from "@/components/workspace/views/modal"; -import { WorkspaceViewQuickActions } from "@/components/workspace/views/quick-action"; -// hooks -import { useGlobalView } from "@/hooks/store/use-global-view"; -import { useIssues } from "@/hooks/store/use-issues"; -import { useAppRouter } from "@/hooks/use-app-router"; - -export const GlobalIssuesHeader = observer(function GlobalIssuesHeader() { - // states - const [createViewModal, setCreateViewModal] = useState(false); - // router - const router = useAppRouter(); - const { workspaceSlug, globalViewId: routerGlobalViewId } = useParams(); - const globalViewId = routerGlobalViewId ? routerGlobalViewId.toString() : undefined; - // store hooks - const { - issuesFilter: { filters, updateFilters }, - } = useIssues(EIssuesStoreType.GLOBAL); - const { getViewDetailsById, currentWorkspaceViews } = useGlobalView(); - const { t } = useTranslation(); - - const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined; - - const activeLayout = issueFilters?.displayFilters?.layout; - const viewDetails = globalViewId ? getViewDetailsById(globalViewId) : undefined; - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !globalViewId) return; - updateFilters( - workspaceSlug.toString(), - undefined, - EIssueFilterType.DISPLAY_FILTERS, - updatedDisplayFilter, - globalViewId - ); - }, - [workspaceSlug, updateFilters, globalViewId] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !globalViewId) return; - updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.DISPLAY_PROPERTIES, property, globalViewId); - }, - [workspaceSlug, updateFilters, globalViewId] - ); - - const isLocked = viewDetails?.is_locked; - - const isDefaultView = DEFAULT_GLOBAL_VIEWS_LIST.find((view) => view.key === globalViewId); - - const defaultViewDetails = DEFAULT_GLOBAL_VIEWS_LIST.find((view) => view.key === globalViewId); - - const defaultOptions = DEFAULT_GLOBAL_VIEWS_LIST.map((view) => ({ - value: view.key, - query: view.key, - content: , - })); - - const workspaceOptions = (currentWorkspaceViews || []).map((view) => { - const _view = getViewDetailsById(view); - if (!_view) return; - return { - value: _view.id, - query: _view.name, - content: , - }; - }); - - const switcherOptions = [...defaultOptions, ...workspaceOptions].filter( - (option) => option !== undefined - ) as ICustomSearchSelectOption[]; - const currentLayoutFilters = useMemo(() => { - const layout = activeLayout ?? EIssueLayoutTypes.SPREADSHEET; - return ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.layoutOptions[layout]; - }, [activeLayout]); - - return ( - <> - setCreateViewModal(false)} /> -
- - - } /> - } - /> - { - router.push(`/${workspaceSlug}/workspace-views/${value}`); - }} - title={viewDetails?.name ?? t(defaultViewDetails?.i18n_label ?? "")} - icon={ - - - - } - isLast - /> - } - isLast - /> - - - - - {globalViewId && } - {!isLocked && ( - - - - )} - -
- {viewDetails && } - {isDefaultView && defaultViewDetails && ( - - )} -
-
-
- - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/layout.tsx deleted file mode 100644 index 60f6e5aa4..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import { AppHeader } from "@/components/core/app-header"; -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { GlobalIssuesHeader } from "./header"; - -export default function GlobalIssuesLayout() { - return ( - <> - } /> - - - - - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/page.tsx deleted file mode 100644 index eeaee1399..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/page.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useState } from "react"; -import { observer } from "mobx-react"; -// plane imports -import { Input, InputGroup } from "@makeplane/propel/components/input"; -import { DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { SearchOutline } from "@makeplane/propel/icons"; - -// components -import { PageHead } from "@/components/core/page-title"; -import { GlobalDefaultViewListItem } from "@/components/workspace/views/default-view-list-item"; -import { GlobalViewsList } from "@/components/workspace/views/views-list"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; - -function WorkspaceViewsPage() { - const [query, setQuery] = useState(""); - // store - const { currentWorkspace } = useWorkspace(); - const { t } = useTranslation(); - // derived values - const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - All Views` : undefined; - - return ( - <> - -
-
- - - setQuery(e.target.value)} - placeholder="Search" - aria-label="Search" - /> - -
-
- {DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => t(v.i18n_label).toLowerCase().includes(query.toLowerCase())).map( - (option) => ( - - ) - )} - -
-
- - ); -} - -export default observer(WorkspaceViewsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx deleted file mode 100644 index a8d2c4b0b..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { ContentWrapper } from "@/components/core/content-wrapper"; -import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider"; - -export default function SettingsLayout() { - return ( - <> - -
-
- {/* Content */} - -
- -
-
-
-
- - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/header.tsx deleted file mode 100644 index 617256780..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { WORKSPACE_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; - -export const BillingWorkspaceSettingsHeader = observer(function BillingWorkspaceSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = WORKSPACE_SETTINGS["billing-and-plans"]; - const Icon = WORKSPACE_SETTINGS_ICONS["billing-and-plans"]; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx deleted file mode 100644 index 3f9383f96..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// component -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import { BillingWorkspaceSettingsHeader } from "./header"; -import { BillingRoot } from "@/components/workspace/billing"; - -function BillingSettingsPage() { - // store hooks - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { currentWorkspace } = useWorkspace(); - // derived values - const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined; - - if (workspaceUserInfo && !canPerformWorkspaceAdminActions) { - return ; - } - - return ( - } hugging> - - - - ); -} - -export default observer(BillingSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/header.tsx deleted file mode 100644 index 2cc08a4bf..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { WORKSPACE_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; - -export const ExportsWorkspaceSettingsHeader = observer(function ExportsWorkspaceSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = WORKSPACE_SETTINGS.export; - const Icon = WORKSPACE_SETTINGS_ICONS.export; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx deleted file mode 100644 index af6141be0..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { cn } from "@plane/utils"; -// components -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { ExportGuide } from "@/components/exporter/guide"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { SettingsHeading } from "@/components/settings/heading"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import { ExportsWorkspaceSettingsHeader } from "./header"; - -function ExportsPage() { - // store hooks - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { currentWorkspace } = useWorkspace(); - const { t } = useTranslation(); - - // derived values - const canPerformWorkspaceMemberActions = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - const pageTitle = currentWorkspace?.name - ? `${currentWorkspace.name} - ${t("workspace_settings.settings.exports.title")}` - : undefined; - - // if user is not authorized to view this page - if (workspaceUserInfo && !canPerformWorkspaceMemberActions) { - return ; - } - - return ( - } hugging> - -
- - -
-
- ); -} - -export default observer(ExportsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/header.tsx deleted file mode 100644 index 7a69bb295..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { WORKSPACE_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; - -export const GeneralWorkspaceSettingsHeader = observer(function GeneralWorkspaceSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = WORKSPACE_SETTINGS.general; - const Icon = WORKSPACE_SETTINGS_ICONS.general; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/integrations/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/integrations/page.tsx deleted file mode 100644 index 0344f8a56..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/integrations/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import useSWR from "swr"; -// components -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { SingleIntegrationCard } from "@/components/integration/single-integration-card"; -import { IntegrationsSettingsLoader } from "@/components/ui/loader/settings/integration"; -// constants -import { APP_INTEGRATIONS } from "@plane/constants"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserPermissions } from "@/hooks/store/user"; -// services -import { IntegrationService } from "@/services/integrations"; - -const integrationService = new IntegrationService(); - -function WorkspaceIntegrationsPage() { - // translation - const { t } = useTranslation(); - // store hooks - const { currentWorkspace } = useWorkspace(); - const { allowPermissions } = useUserPermissions(); - - // derived values - const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Integrations` : undefined; - const { data: appIntegrations } = useSWR(isAdmin ? APP_INTEGRATIONS : null, () => - isAdmin ? integrationService.getAppIntegrationsList() : null - ); - - if (!isAdmin) return ; - - return ( - <> - -
-
-

{t("integrations.integrations")}

-
-
- {appIntegrations ? ( - appIntegrations.map((integration) => ( - - )) - ) : ( - - )} -
-
- - ); -} - -export default observer(WorkspaceIntegrationsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx deleted file mode 100644 index a57df1e70..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { usePathname } from "next/navigation"; -import { Outlet } from "react-router"; -// components -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { getWorkspaceActivePath, pathnameToAccessKey } from "@/components/settings/helper"; -import { SettingsMobileNav } from "@/components/settings/mobile/nav"; -// plane imports -import { WORKSPACE_SETTINGS_ACCESS } from "@plane/constants"; -import type { EUserWorkspaceRoles } from "@plane/types"; -// components -import { WorkspaceSettingsSidebarRoot } from "@/components/settings/workspace/sidebar"; -// hooks -import { useUserPermissions } from "@/hooks/store/user"; - -import type { Route } from "./+types/layout"; - -const WorkspaceSettingLayout = observer(function WorkspaceSettingLayout({ params }: Route.ComponentProps) { - // router - const { workspaceSlug } = params; - // store hooks - const { workspaceUserInfo, getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); - // next hooks - const pathname = usePathname(); - // derived values - const { accessKey } = pathnameToAccessKey(pathname); - const userWorkspaceRole = getWorkspaceRoleByWorkspaceSlug(workspaceSlug); - - let isAuthorized: boolean | string = false; - if (pathname && workspaceSlug && userWorkspaceRole) { - isAuthorized = WORKSPACE_SETTINGS_ACCESS[accessKey]?.includes(userWorkspaceRole as EUserWorkspaceRoles); - } - - return ( - <> - -
- {workspaceUserInfo && !isAuthorized ? ( - - ) : ( -
-
- -
- -
- )} -
- - ); -}); - -export default WorkspaceSettingLayout; diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/header.tsx deleted file mode 100644 index 243f8da7b..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { WORKSPACE_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; - -export const MembersWorkspaceSettingsHeader = observer(function MembersWorkspaceSettingsHeader() { - // plane hooks - const { t } = useTranslation(); - // derived values - const settingsDetails = WORKSPACE_SETTINGS.members; - const Icon = WORKSPACE_SETTINGS_ICONS.members; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx deleted file mode 100644 index 142f7b31c..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx +++ /dev/null @@ -1,151 +0,0 @@ -/** - * 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"; -// types -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { SearchOutline } from "@makeplane/propel/icons"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import type { IWorkspaceBulkInviteFormData } from "@plane/types"; -import { cn } from "@plane/utils"; -// components -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { CountChip } from "@/components/common/count-chip"; -import { PageHead } from "@/components/core/page-title"; -import { MemberListFiltersDropdown } from "@/components/project/dropdowns/filters/member-list"; -import { WorkspaceMembersList } from "@/components/workspace/settings/members-list"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { SendWorkspaceInvitationModal } from "@/components/workspace/members"; -// hooks -import { useMember } from "@/hooks/store/use-member"; -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { MembersWorkspaceSettingsHeader } from "./header"; - -const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsPage({ params }: Route.ComponentProps) { - // states - const [inviteModal, setInviteModal] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - // router - const { workspaceSlug } = params; - // store hooks - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { - workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore }, - } = useMember(); - const { currentWorkspace } = useWorkspace(); - const { t } = useTranslation(); - - // derived values - const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - const canPerformWorkspaceMemberActions = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - const handleWorkspaceInvite = async (data: IWorkspaceBulkInviteFormData) => { - try { - await inviteMembersToWorkspace(workspaceSlug, data); - - setInviteModal(false); - - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: t("workspace_settings.settings.members.invitations_sent_successfully"), - }); - } catch (error: unknown) { - let message = undefined; - if (error instanceof Error) { - const err = error as Error & { error?: string }; - message = err.error; - } - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: `${message ?? t("something_went_wrong_please_try_again")}`, - }); - - throw error; - } - }; - - // Handler for role filter updates - const handleRoleFilterUpdate = (role: string) => { - const currentFilters = filtersStore.filters; - const currentRoles = currentFilters?.roles || []; - const updatedRoles = currentRoles.includes(role) ? currentRoles.filter((r) => r !== role) : [...currentRoles, role]; - - filtersStore.updateFilters({ - roles: updatedRoles.length > 0 ? updatedRoles : undefined, - }); - }; - - // derived values - const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined; - const appliedRoleFilters = filtersStore.filters?.roles || []; - - // if user is not authorized to view this page - if (workspaceUserInfo && !canPerformWorkspaceMemberActions) { - return ; - } - - return ( - } hugging> - - setInviteModal(false)} - onSubmit={handleWorkspaceInvite} - /> -
-
-

- {t("workspace_settings.settings.members.title")} - {workspaceMemberIds && workspaceMemberIds.length > 0 && ( - - )} -

-
-
- - setSearchQuery(e.target.value)} - /> -
- - {canPerformWorkspaceAdminActions && ( - - )} -
-
- -
-
- ); -}); - -export default WorkspaceMembersSettingsPage; diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx deleted file mode 100644 index 55422ecc7..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// components -import { PageHead } from "@/components/core/page-title"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { WorkspaceDetails } from "@/components/workspace/settings/workspace-details"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -// local imports -import { GeneralWorkspaceSettingsHeader } from "./header"; - -function GeneralWorkspaceSettingsPage() { - // store hooks - const { currentWorkspace } = useWorkspace(); - const { t } = useTranslation(); - // derived values - const pageTitle = currentWorkspace?.name - ? t("workspace_settings.page_label", { workspace: currentWorkspace.name }) - : undefined; - - return ( - }> - - - - ); -} - -export default observer(GeneralWorkspaceSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/header.tsx deleted file mode 100644 index 7c24c6ba5..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { WORKSPACE_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; - -export const WebhookDetailsWorkspaceSettingsHeader = observer(function WebhookDetailsWorkspaceSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = WORKSPACE_SETTINGS.webhooks; - const Icon = WORKSPACE_SETTINGS_ICONS.webhooks; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx deleted file mode 100644 index 5cb9d3c17..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/** - * 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 { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import type { IWebhook } from "@plane/types"; -// ui -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { PageHead } from "@/components/core/page-title"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/components/web-hooks"; -// hooks -import { useWebhook } from "@/hooks/store/use-webhook"; -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { WebhookDetailsWorkspaceSettingsHeader } from "./header"; - -function WebhookDetailsPage({ params }: Route.ComponentProps) { - // states - const [deleteWebhookModal, setDeleteWebhookModal] = useState(false); - // router - const { workspaceSlug, webhookId } = params; - // mobx store - const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook(); - const { currentWorkspace } = useWorkspace(); - const { allowPermissions } = useUserPermissions(); - - // TODO: fix this error - // useEffect(() => { - // if (isCreated !== "true") clearSecretKey(); - // }, [clearSecretKey, isCreated]); - // derived values - const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhook` : undefined; - - useSWR( - isAdmin ? `WEBHOOK_DETAILS_${workspaceSlug}_${webhookId}` : null, - isAdmin ? () => fetchWebhookById(workspaceSlug, webhookId) : null - ); - - const handleUpdateWebhook = async (formData: IWebhook) => { - if (!formData || !formData.id) return; - - const payload = { - url: formData.url, - is_active: formData.is_active, - project: formData.project, - cycle: formData.cycle, - module: formData.module, - issue: formData.issue, - issue_comment: formData.issue_comment, - }; - - try { - await updateWebhook(workspaceSlug, formData.id, payload); - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: "Webhook updated successfully.", - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: error?.error ?? "Something went wrong. Please try again.", - }); - } - }; - - if (!isAdmin) - return ( - <> - -
-

You are not authorized to access this page.

-
- - ); - - if (!currentWebhook) - return ( -
- -
- ); - - return ( - }> - - setDeleteWebhookModal(false)} /> -
-
- -
- {currentWebhook && setDeleteWebhookModal(true)} />} -
-
- ); -} - -export default observer(WebhookDetailsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/header.tsx deleted file mode 100644 index 0916bf9f0..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { WORKSPACE_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; - -export const WebhooksWorkspaceSettingsHeader = observer(function WebhooksWorkspaceSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = WORKSPACE_SETTINGS.webhooks; - const Icon = WORKSPACE_SETTINGS_ICONS.webhooks; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx deleted file mode 100644 index 099a0ee40..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import useSWR from "swr"; -// plane imports -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -// components -import { EmptyStateCompact } from "@plane/propel/empty-state"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { SettingsHeading } from "@/components/settings/heading"; -import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks"; -// hooks -import { useWebhook } from "@/hooks/store/use-webhook"; -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { WebhooksWorkspaceSettingsHeader } from "./header"; - -function WebhooksListPage({ params }: Route.ComponentProps) { - // states - const [showCreateWebhookModal, setShowCreateWebhookModal] = useState(false); - // router - const { workspaceSlug } = params; - // plane hooks - const { t } = useTranslation(); - // mobx store - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); - const { currentWorkspace } = useWorkspace(); - // derived values - const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - - useSWR( - canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null, - canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug) : null - ); - - const pageTitle = currentWorkspace?.name - ? `${currentWorkspace.name} - ${t("workspace_settings.settings.webhooks.title")}` - : undefined; - - // clear secret key when modal is closed. - useEffect(() => { - if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey(); - }, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]); - - if (workspaceUserInfo && !canPerformWorkspaceAdminActions) { - return ; - } - - if (!webhooks) return ; - - return ( - }> - -
- { - setShowCreateWebhookModal(false); - }} - /> - setShowCreateWebhookModal(true)}> - {t("workspace_settings.settings.webhooks.add_webhook")} - - } - /> - {Object.keys(webhooks).length > 0 ? ( -
- -
- ) : ( -
-
- { - setShowCreateWebhookModal(true); - }, - }, - ]} - align="start" - rootClassName="py-20" - /> -
-
- )} -
-
- ); -} - -export default observer(WebhooksListPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/header.tsx deleted file mode 100644 index df9ae6e9e..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROJECT_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; - -export const AutomationsProjectSettingsHeader = observer(function AutomationsProjectSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = PROJECT_SETTINGS.automations; - const Icon = PROJECT_SETTINGS_ICONS.automations; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/layout.tsx deleted file mode 100644 index d2be455ea..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { Outlet } from "react-router"; - -function AutomationsListLayout() { - return ; -} - -export default observer(AutomationsListLayout); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx deleted file mode 100644 index 3a8b98631..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import type { IProject } from "@plane/types"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation"; -import { PageHead } from "@/components/core/page-title"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { SettingsHeading } from "@/components/settings/heading"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { AutomationsProjectSettingsHeader } from "./header"; - -function AutomationSettingsPage({ params }: Route.ComponentProps) { - // router - const { workspaceSlug, projectId } = params; - // store hooks - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { currentProjectDetails: projectDetails, updateProject } = useProject(); - - const { t } = useTranslation(); - - // derived values - const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); - - const handleChange = async (formData: Partial) => { - if (!projectDetails) return; - - try { - await updateProject(workspaceSlug, projectId, formData); - } catch { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Something went wrong. Please try again.", - }); - } - }; - - // derived values - const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined; - - if (workspaceUserInfo && !canPerformProjectAdminActions) { - return ; - } - - return ( - } hugging> - -
- -
- - -
-
-
- ); -} - -export default observer(AutomationSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/header.tsx deleted file mode 100644 index 447f925a0..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROJECT_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; - -export const EstimatesProjectSettingsHeader = observer(function EstimatesProjectSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = PROJECT_SETTINGS.estimates; - const Icon = PROJECT_SETTINGS_ICONS.estimates; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx deleted file mode 100644 index cfa1e9423..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// components -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { EstimateRoot } from "@/components/estimates"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { EstimatesProjectSettingsHeader } from "./header"; - -function EstimatesSettingsPage({ params }: Route.ComponentProps) { - const { workspaceSlug, projectId } = params; - // store - const { currentProjectDetails } = useProject(); - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - - // derived values - const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined; - const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); - - if (workspaceUserInfo && !canPerformProjectAdminActions) { - return ; - } - - return ( - }> - -
- -
-
- ); -} - -export default observer(EstimatesSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/header.tsx deleted file mode 100644 index c2fa9dee5..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROJECT_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; - -export const FeaturesCyclesProjectSettingsHeader = observer(function FeaturesCyclesProjectSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = PROJECT_SETTINGS.features_cycles; - const Icon = PROJECT_SETTINGS_ICONS.features_cycles; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/page.tsx deleted file mode 100644 index b86a1b294..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// components -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { FeaturesCyclesProjectSettingsHeader } from "./header"; -import { SettingsHeading } from "@/components/settings/heading"; - -function FeaturesCyclesSettingsPage({ params }: Route.ComponentProps) { - const { workspaceSlug, projectId } = params; - // store hooks - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { currentProjectDetails } = useProject(); - // translation - const { t } = useTranslation(); - // derived values - const pageTitle = currentProjectDetails?.name - ? `${currentProjectDetails?.name} settings - ${t("project_settings.features.cycles.short_title")}` - : undefined; - const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); - - if (workspaceUserInfo && !canPerformProjectAdminActions) { - return ; - } - - return ( - }> - -
- -
- -
-
-
- ); -} - -export default observer(FeaturesCyclesSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/header.tsx deleted file mode 100644 index 5f81d40af..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROJECT_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; - -export const FeaturesIntakeProjectSettingsHeader = observer(function FeaturesIntakeProjectSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = PROJECT_SETTINGS.features_intake; - const Icon = PROJECT_SETTINGS_ICONS.features_intake; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/page.tsx deleted file mode 100644 index 3349a79d3..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// components -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { SettingsHeading } from "@/components/settings/heading"; -import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { FeaturesIntakeProjectSettingsHeader } from "./header"; - -function FeaturesIntakeSettingsPage({ params }: Route.ComponentProps) { - const { workspaceSlug, projectId } = params; - // store hooks - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { currentProjectDetails } = useProject(); - // translation - const { t } = useTranslation(); - // derived values - const pageTitle = currentProjectDetails?.name - ? `${currentProjectDetails?.name} settings - ${t("project_settings.features.intake.short_title")}` - : undefined; - const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); - - if (workspaceUserInfo && !canPerformProjectAdminActions) { - return ; - } - - return ( - }> - -
- -
- -
-
-
- ); -} - -export default observer(FeaturesIntakeSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/header.tsx deleted file mode 100644 index baf32b2b9..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROJECT_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; - -export const FeaturesModulesProjectSettingsHeader = observer(function FeaturesModulesProjectSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = PROJECT_SETTINGS.features_modules; - const Icon = PROJECT_SETTINGS_ICONS.features_modules; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/page.tsx deleted file mode 100644 index 1253f0d65..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// components -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { SettingsHeading } from "@/components/settings/heading"; -import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { FeaturesModulesProjectSettingsHeader } from "./header"; - -function FeaturesModulesSettingsPage({ params }: Route.ComponentProps) { - const { workspaceSlug, projectId } = params; - // store hooks - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { currentProjectDetails } = useProject(); - // translation - const { t } = useTranslation(); - // derived values - const pageTitle = currentProjectDetails?.name - ? `${currentProjectDetails?.name} settings - ${t("project_settings.features.modules.short_title")}` - : undefined; - const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); - - if (workspaceUserInfo && !canPerformProjectAdminActions) { - return ; - } - - return ( - }> - -
- -
- -
-
-
- ); -} - -export default observer(FeaturesModulesSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/header.tsx deleted file mode 100644 index fa7a40e28..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROJECT_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; - -export const FeaturesPagesProjectSettingsHeader = observer(function FeaturesPagesProjectSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = PROJECT_SETTINGS.features_pages; - const Icon = PROJECT_SETTINGS_ICONS.features_pages; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/page.tsx deleted file mode 100644 index 2b018e56d..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// components -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { SettingsHeading } from "@/components/settings/heading"; -import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { FeaturesPagesProjectSettingsHeader } from "./header"; - -function FeaturesPagesSettingsPage({ params }: Route.ComponentProps) { - const { workspaceSlug, projectId } = params; - // store hooks - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { currentProjectDetails } = useProject(); - // translation - const { t } = useTranslation(); - // derived values - const pageTitle = currentProjectDetails?.name - ? `${currentProjectDetails?.name} settings - ${t("project_settings.features.pages.short_title")}` - : undefined; - const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); - - if (workspaceUserInfo && !canPerformProjectAdminActions) { - return ; - } - - return ( - }> - -
- -
- -
-
-
- ); -} - -export default observer(FeaturesPagesSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/header.tsx deleted file mode 100644 index c8429d90c..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROJECT_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; - -export const FeaturesViewsProjectSettingsHeader = observer(function FeaturesViewsProjectSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = PROJECT_SETTINGS.features_views; - const Icon = PROJECT_SETTINGS_ICONS.features_views; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/page.tsx deleted file mode 100644 index 594028f8f..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// components -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { SettingsHeading } from "@/components/settings/heading"; -import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { FeaturesViewsProjectSettingsHeader } from "./header"; - -function FeaturesViewsSettingsPage({ params }: Route.ComponentProps) { - const { workspaceSlug, projectId } = params; - // store hooks - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { currentProjectDetails } = useProject(); - // translation - const { t } = useTranslation(); - // derived values - const pageTitle = currentProjectDetails?.name - ? `${currentProjectDetails?.name} settings - ${t("project_settings.features.views.short_title")}` - : undefined; - const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); - - if (workspaceUserInfo && !canPerformProjectAdminActions) { - return ; - } - - return ( - }> - -
- -
- -
-
-
- ); -} - -export default observer(FeaturesViewsSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/header.tsx deleted file mode 100644 index 95d472051..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROJECT_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; - -export const GeneralProjectSettingsHeader = observer(function GeneralProjectSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = PROJECT_SETTINGS.general; - const Icon = PROJECT_SETTINGS_ICONS.general; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/header.tsx deleted file mode 100644 index b9daaae1e..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROJECT_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; - -export const LabelsProjectSettingsHeader = observer(function LabelsProjectSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = PROJECT_SETTINGS.labels; - const Icon = PROJECT_SETTINGS_ICONS.labels; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx deleted file mode 100644 index 870c05fa7..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect, useRef } from "react"; -import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; -import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; -import { observer } from "mobx-react"; -// components -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { ProjectSettingsLabelList } from "@/components/labels"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import { LabelsProjectSettingsHeader } from "./header"; - -function LabelsSettingsPage() { - // store hooks - const { currentProjectDetails } = useProject(); - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - - const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined; - - const scrollableContainerRef = useRef(null); - - // derived values - const canPerformProjectMemberActions = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT - ); - - // Enable Auto Scroll for Labels list - useEffect(() => { - const element = scrollableContainerRef.current; - - if (!element) return; - - return combine( - autoScrollForElements({ - element, - }) - ); - }, []); - - if (workspaceUserInfo && !canPerformProjectMemberActions) { - return ; - } - - return ( - }> - -
- -
-
- ); -} - -export default observer(LabelsSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx deleted file mode 100644 index 6ebe9df3e..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { usePathname } from "next/navigation"; -import { Outlet } from "react-router"; -// components -import { getProjectActivePath } from "@/components/settings/helper"; -import { SettingsMobileNav } from "@/components/settings/mobile/nav"; -// layouts -import { ProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper"; -// types -import type { Route } from "./+types/layout"; -import { ProjectSettingsSidebarRoot } from "@/components/settings/project/sidebar"; - -function ProjectDetailSettingsLayout({ params }: Route.ComponentProps) { - const { workspaceSlug, projectId } = params; - // router - const pathname = usePathname(); - - return ( - <> - } - activePath={getProjectActivePath(pathname) || ""} - /> -
-
-
- -
- - - -
-
- - ); -} - -export default observer(ProjectDetailSettingsLayout); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/header.tsx deleted file mode 100644 index e8e3c4fe4..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROJECT_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; - -export const MembersProjectSettingsHeader = observer(function MembersProjectSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = PROJECT_SETTINGS.members; - const Icon = PROJECT_SETTINGS_ICONS.members; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx deleted file mode 100644 index 7df00fc52..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -// components -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { ProjectMemberList } from "@/components/project/member-list"; -import { ProjectSettingsMemberDefaults } from "@/components/project/project-settings-member-defaults"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { SettingsHeading } from "@/components/settings/heading"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { MembersProjectSettingsHeader } from "./header"; - -function MembersSettingsPage({ params }: Route.ComponentProps) { - // router - const { workspaceSlug, projectId } = params; - // plane hooks - const { t } = useTranslation(); - // store hooks - const { currentProjectDetails } = useProject(); - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - // derived values - const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined; - const isProjectMemberOrAdmin = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT - ); - const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - const canPerformProjectMemberActions = isProjectMemberOrAdmin || isWorkspaceAdmin; - - if (workspaceUserInfo && !canPerformProjectMemberActions) { - return ; - } - - return ( - } hugging> - - - - - - ); -} - -export default observer(MembersSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx deleted file mode 100644 index 39a804660..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -// components -import { PageHead } from "@/components/core/page-title"; -import { ProjectDetailsForm } from "@/components/project/form"; -import { ProjectDetailsFormLoader } from "@/components/project/form-loader"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { GeneralProjectSettingsHeader } from "./header"; -import { GeneralProjectSettingsControlSection } from "@/components/project/settings/control-section"; - -function ProjectSettingsPage({ params }: Route.ComponentProps) { - // router - const { workspaceSlug, projectId } = params; - // store hooks - const { currentProjectDetails } = useProject(); - const { allowPermissions } = useUserPermissions(); - // derived values - const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId); - - const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined; - - return ( - }> - -
- {currentProjectDetails ? ( - - ) : ( - - )} - {isAdmin && } -
-
- ); -} - -export default observer(ProjectSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/header.tsx deleted file mode 100644 index 957632b25..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/header.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROJECT_SETTINGS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs } from "@plane/ui"; -// components -import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { SettingsPageHeader } from "@/components/settings/page-header"; -import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; - -export const StatesProjectSettingsHeader = observer(function StatesProjectSettingsHeader() { - // translation - const { t } = useTranslation(); - // derived values - const settingsDetails = PROJECT_SETTINGS.states; - const Icon = PROJECT_SETTINGS_ICONS.states; - - return ( - - - } - /> - } - /> - - - } - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx deleted file mode 100644 index 9913d521c..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -// components -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { ProjectStateRoot } from "@/components/project-states"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { SettingsHeading } from "@/components/settings/heading"; -// hook -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -// local imports -import type { Route } from "./+types/page"; -import { StatesProjectSettingsHeader } from "./header"; - -function StatesSettingsPage({ params }: Route.ComponentProps) { - const { workspaceSlug, projectId } = params; - // store - const { currentProjectDetails } = useProject(); - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - - const { t } = useTranslation(); - - // derived values - const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined; - // derived values - const canPerformProjectMemberActions = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT - ); - - if (workspaceUserInfo && !canPerformProjectMemberActions) { - return ; - } - - return ( - }> - -
- -
- -
-
-
- ); -} - -export default observer(StatesSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx deleted file mode 100644 index 86229fe9c..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import { Outlet } from "react-router"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useAppRouter } from "@/hooks/use-app-router"; -// types -import type { Route } from "./+types/layout"; - -function ProjectSettingsLayout({ params }: Route.ComponentProps) { - const { workspaceSlug, projectId } = params; - // router - const router = useAppRouter(); - // store hooks - const { joinedProjectIds } = useProject(); - - useEffect(() => { - if (projectId) return; - if (joinedProjectIds.length > 0) { - router.push(`/${workspaceSlug}/settings/projects/${joinedProjectIds[0]}`); - } - }, [joinedProjectIds, router, workspaceSlug, projectId]); - - return ; -} - -export default observer(ProjectSettingsLayout); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx deleted file mode 100644 index 1d381547d..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import Link from "next/link"; -import { useTheme } from "next-themes"; -// plane imports -import { Button, getButtonStyling } from "@plane/propel/button"; -import { cn } from "@plane/utils"; -// assets -import ProjectDarkEmptyState from "@/app/assets/empty-state/project-settings/no-projects-dark.png?url"; -import ProjectLightEmptyState from "@/app/assets/empty-state/project-settings/no-projects-light.png?url"; -// hooks -import { useCommandPalette } from "@/hooks/store/use-command-palette"; - -function ProjectSettingsPage() { - // store hooks - const { resolvedTheme } = useTheme(); - const { toggleCreateProjectModal } = useCommandPalette(); - // derived values - const resolvedPath = resolvedTheme === "dark" ? ProjectDarkEmptyState : ProjectLightEmptyState; - return ( -
- No projects yet -
No projects yet
-
- Projects act as the foundation for goal-driven work. They let you manage your teams, tasks, and everything you - need to get things done. -
-
- - Learn more about projects - - -
-
- ); -} - -export default observer(ProjectSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/layout.tsx deleted file mode 100644 index 1c295eeee..000000000 --- a/apps/web/app/(all)/[workspaceSlug]/layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; -import { WorkspaceContentWrapper } from "@/components/workspace/content-wrapper"; -import { AppRailVisibilityProvider } from "@/lib/app-rail"; -import { GlobalModals } from "@/components/common/modal/global"; -import { WorkspaceAuthWrapper } from "@/layouts/auth-layout/workspace-wrapper"; -import type { Route } from "./+types/layout"; - -export default function WorkspaceLayout(props: Route.ComponentProps) { - const { workspaceSlug } = props.params; - - return ( - - - - - - - - - - - ); -} diff --git a/apps/web/app/(all)/accounts/forgot-password/layout.tsx b/apps/web/app/(all)/accounts/forgot-password/layout.tsx deleted file mode 100644 index f503c81e2..000000000 --- a/apps/web/app/(all)/accounts/forgot-password/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import type { Route } from "./+types/layout"; - -export default function ForgotPasswordLayout() { - return ; -} - -export const meta: Route.MetaFunction = () => [{ title: "Forgot Password - Plane" }]; diff --git a/apps/web/app/(all)/accounts/forgot-password/page.tsx b/apps/web/app/(all)/accounts/forgot-password/page.tsx deleted file mode 100644 index eb71311cb..000000000 --- a/apps/web/app/(all)/accounts/forgot-password/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// components -import { ForgotPasswordForm } from "@/components/account/auth-forms/forgot-password"; -import { AuthHeader } from "@/components/auth-screens/header"; -// helpers -import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper"; -// layouts -import DefaultLayout from "@/layouts/default-layout"; -import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; - -function ForgotPasswordPage() { - return ( - - -
- - -
-
-
- ); -} - -export default observer(ForgotPasswordPage); diff --git a/apps/web/app/(all)/accounts/reset-password/layout.tsx b/apps/web/app/(all)/accounts/reset-password/layout.tsx deleted file mode 100644 index d3955daa8..000000000 --- a/apps/web/app/(all)/accounts/reset-password/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import type { Route } from "./+types/layout"; - -export default function ResetPasswordLayout() { - return ; -} - -export const meta: Route.MetaFunction = () => [{ title: "Reset Password - Plane" }]; diff --git a/apps/web/app/(all)/accounts/reset-password/page.tsx b/apps/web/app/(all)/accounts/reset-password/page.tsx deleted file mode 100644 index c9914f08d..000000000 --- a/apps/web/app/(all)/accounts/reset-password/page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane imports -import { EAuthModes } from "@plane/constants"; -// components -import { ResetPasswordForm } from "@/components/account/auth-forms/reset-password"; -import { AuthHeader } from "@/components/auth-screens/header"; -// helpers -import { EPageTypes } from "@/helpers/authentication.helper"; -// layouts -import DefaultLayout from "@/layouts/default-layout"; -import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; - -function ResetPasswordPage() { - return ( - - -
- - -
-
-
- ); -} - -export default ResetPasswordPage; diff --git a/apps/web/app/(all)/accounts/set-password/layout.tsx b/apps/web/app/(all)/accounts/set-password/layout.tsx deleted file mode 100644 index 4455b84fc..000000000 --- a/apps/web/app/(all)/accounts/set-password/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import type { Route } from "./+types/layout"; - -export default function SetPasswordLayout() { - return ; -} - -export const meta: Route.MetaFunction = () => [{ title: "Set Password - Plane" }]; diff --git a/apps/web/app/(all)/accounts/set-password/page.tsx b/apps/web/app/(all)/accounts/set-password/page.tsx deleted file mode 100644 index 493f86de8..000000000 --- a/apps/web/app/(all)/accounts/set-password/page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane imports -import { EAuthModes } from "@plane/constants"; -// components -import { ResetPasswordForm } from "@/components/account/auth-forms/reset-password"; -import { AuthHeader } from "@/components/auth-screens/header"; -// helpers -import { EPageTypes } from "@/helpers/authentication.helper"; -// layouts -import DefaultLayout from "@/layouts/default-layout"; -import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; - -function SetPasswordPage() { - return ( - - -
- - -
-
-
- ); -} - -export default SetPasswordPage; diff --git a/apps/web/app/(all)/create-workspace/layout.tsx b/apps/web/app/(all)/create-workspace/layout.tsx deleted file mode 100644 index 95b73bf8d..000000000 --- a/apps/web/app/(all)/create-workspace/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import type { Route } from "./+types/layout"; - -export default function CreateWorkspaceLayout() { - return ; -} - -export const meta: Route.MetaFunction = () => [{ title: "Create Workspace" }]; diff --git a/apps/web/app/(all)/create-workspace/page.tsx b/apps/web/app/(all)/create-workspace/page.tsx deleted file mode 100644 index 38087189c..000000000 --- a/apps/web/app/(all)/create-workspace/page.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/** - * 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 Link from "next/link"; -// plane imports -import { useTranslation } from "@plane/i18n"; -import { Button, getButtonStyling } from "@plane/propel/button"; -import { PlaneLogo } from "@plane/propel/icons"; -import type { IWorkspace } from "@plane/types"; -// assets -import WorkspaceCreationDisabled from "@/app/assets/workspace/workspace-creation-disabled.png?url"; -// components -import { CreateWorkspaceForm } from "@/components/workspace/create-workspace-form"; -// hooks -import { useUser, useUserProfile } from "@/hooks/store/user"; -import { useInstance } from "@/hooks/store/use-instance"; -import { useAppRouter } from "@/hooks/use-app-router"; -// wrappers -import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; - -const CreateWorkspacePage = observer(function CreateWorkspacePage() { - const { t } = useTranslation(); - // router - const router = useAppRouter(); - // store hooks - const { config } = useInstance(); - const { data: currentUser } = useUser(); - const { updateUserProfile } = useUserProfile(); - // states - const [defaultValues, setDefaultValues] = useState>({ - name: "", - slug: "", - organization_size: "", - }); - // derived values - const isWorkspaceCreationDisabled = config?.is_workspace_creation_disabled ?? false; - - // methods - const getMailtoHref = () => { - const subject = t("workspace_creation.request_email.subject"); - const body = t("workspace_creation.request_email.body", { - firstName: currentUser?.first_name || "", - lastName: currentUser?.last_name || "", - email: currentUser?.email || "", - }); - - return `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; - }; - - const onSubmit = async (workspace: IWorkspace) => { - await updateUserProfile({ last_workspace_id: workspace.id }).then(() => router.push(`/${workspace.slug}`)); - }; - - return ( - -
-
-
- - - -
- {currentUser?.email} -
-
-
- {isWorkspaceCreationDisabled ? ( -
- Workspace creation disabled -
- {t("workspace_creation.errors.creation_disabled.title")} -
-

- {t("workspace_creation.errors.creation_disabled.description")} -

-
- - - {t("workspace_creation.errors.creation_disabled.request_button")} - -
-
- ) : ( -
-

{t("workspace_creation.heading")}

-
- -
-
- )} -
-
- - ); -}); - -export default CreateWorkspacePage; diff --git a/apps/web/app/(all)/invitations/layout.tsx b/apps/web/app/(all)/invitations/layout.tsx deleted file mode 100644 index 68f4fe7a4..000000000 --- a/apps/web/app/(all)/invitations/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import type { Route } from "./+types/layout"; - -export default function InvitationsLayout() { - return ; -} - -export const meta: Route.MetaFunction = () => [{ title: "Invitations" }]; diff --git a/apps/web/app/(all)/invitations/page.tsx b/apps/web/app/(all)/invitations/page.tsx deleted file mode 100644 index f325f27eb..000000000 --- a/apps/web/app/(all)/invitations/page.tsx +++ /dev/null @@ -1,202 +0,0 @@ -/** - * 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 Link from "next/link"; - -import useSWR, { mutate } from "swr"; -import { TickCircleOutline } from "@makeplane/propel/icons"; -// plane imports -import { ROLE } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -// types -import { Button } from "@plane/propel/button"; -import { PlaneLogo } from "@plane/propel/icons"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import type { IWorkspaceMemberInvitation } from "@plane/types"; -import { truncateText } from "@plane/utils"; -// assets -import emptyInvitation from "@/app/assets/empty-state/invitation.svg?url"; -// components -import { EmptyState } from "@/components/common/empty-state"; -import { WorkspaceLogo } from "@/components/workspace/logo"; -import { USER_WORKSPACES_LIST } from "@plane/constants"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUser, useUserProfile } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -// services -import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; -// plane web services -import { WorkspaceService } from "@/services/workspace.service"; - -const workspaceService = new WorkspaceService(); - -function UserInvitationsPage() { - // states - const [invitationsRespond, setInvitationsRespond] = useState([]); - const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); - // router - const router = useAppRouter(); - // store hooks - const { t } = useTranslation(); - const { data: currentUser } = useUser(); - const { updateUserProfile } = useUserProfile(); - - const { fetchWorkspaces } = useWorkspace(); - - const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS", () => workspaceService.userWorkspaceInvitations()); - - const redirectWorkspaceSlug = - // currentUserSettings?.workspace?.last_workspace_slug || - // currentUserSettings?.workspace?.fallback_workspace_slug || - ""; - - const handleInvitation = (workspace_invitation: IWorkspaceMemberInvitation, action: "accepted" | "withdraw") => { - if (action === "accepted") { - setInvitationsRespond((prevData) => [...prevData, workspace_invitation.id]); - } else if (action === "withdraw") { - setInvitationsRespond((prevData) => prevData.filter((item: string) => item !== workspace_invitation.id)); - } - }; - - const submitInvitations = () => { - if (invitationsRespond.length === 0) { - setToast({ - type: TOAST_TYPE.ERROR, - title: t("error"), - message: t("please_select_at_least_one_invitation"), - }); - return; - } - - setIsJoiningWorkspaces(true); - - workspaceService - .joinWorkspaces({ invitations: invitationsRespond }) - .then(() => { - mutate(USER_WORKSPACES_LIST); - const firstInviteId = invitationsRespond[0]; - const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace; - updateUserProfile({ last_workspace_id: redirectWorkspace?.id }) - .then(() => { - setIsJoiningWorkspaces(false); - fetchWorkspaces().then(() => { - router.push(`/${redirectWorkspace?.slug}`); - }); - }) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: t("error"), - message: t("something_went_wrong_please_try_again"), - }); - setIsJoiningWorkspaces(false); - }); - }) - .catch((_err) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: t("error"), - message: t("something_went_wrong_please_try_again"), - }); - setIsJoiningWorkspaces(false); - }); - }; - - return ( - -
-
-
- - - -
- {currentUser?.email} -
-
- {invitations ? ( - invitations.length > 0 ? ( -
-
-
{t("we_see_that_someone_has_invited_you_to_join_a_workspace")}
-

{t("join_a_workspace")}

-
- {invitations.map((invitation) => { - const isSelected = invitationsRespond.includes(invitation.id); - - return ( -
handleInvitation(invitation, isSelected ? "withdraw" : "accepted")} - > -
- -
-
-
{truncateText(invitation.workspace.name, 30)}
-

{ROLE[invitation.role]}

-
- - - -
- ); - })} -
-
- - - - - - -
-
-
- ) : ( -
- router.push("/"), - }} - /> -
- ) - ) : null} -
- - ); -} - -export default observer(UserInvitationsPage); diff --git a/apps/web/app/(all)/layout.preload.tsx b/apps/web/app/(all)/layout.preload.tsx deleted file mode 100644 index e070eaa66..000000000 --- a/apps/web/app/(all)/layout.preload.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// TODO: Check if we need this -// https://nextjs.org/docs/app/api-reference/functions/generate-metadata#link-relpreload -// export const usePreloadResources = () => { -// useEffect(() => { -// const preloadItem = (url: string) => { -// ReactDOM.preload(url, { as: "fetch", crossOrigin: "use-credentials" }); -// }; - -// const urls = [ -// `${process.env.VITE_API_BASE_URL}/api/instances/`, -// `${process.env.VITE_API_BASE_URL}/api/users/me/`, -// `${process.env.VITE_API_BASE_URL}/api/users/me/profile/`, -// `${process.env.VITE_API_BASE_URL}/api/users/me/settings/`, -// `${process.env.VITE_API_BASE_URL}/api/users/me/workspaces/?v=${Date.now()}`, -// ]; - -// urls.forEach((url) => preloadItem(url)); -// }, []); -// }; - -export function PreloadResources() { - return ( - // usePreloadResources(); - null - ); -} diff --git a/apps/web/app/(all)/layout.tsx b/apps/web/app/(all)/layout.tsx deleted file mode 100644 index ab06034b0..000000000 --- a/apps/web/app/(all)/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import type { Route } from "./+types/layout"; -import { PreloadResources } from "./layout.preload"; - -export const meta: Route.MetaFunction = () => [ - { name: "robots", content: "noindex, nofollow" }, - { name: "viewport", content: "width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover" }, -]; - -export default function AppLayout() { - return ( - <> - - - - ); -} diff --git a/apps/web/app/(all)/onboarding/layout.tsx b/apps/web/app/(all)/onboarding/layout.tsx deleted file mode 100644 index fee10f7a0..000000000 --- a/apps/web/app/(all)/onboarding/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import type { Route } from "./+types/layout"; - -export default function OnboardingLayout() { - return ; -} - -export const meta: Route.MetaFunction = () => [{ title: "Onboarding" }]; diff --git a/apps/web/app/(all)/onboarding/page.tsx b/apps/web/app/(all)/onboarding/page.tsx deleted file mode 100644 index 86ef1b1f7..000000000 --- a/apps/web/app/(all)/onboarding/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import useSWR from "swr"; - -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { OnboardingRoot } from "@/components/onboarding"; -// constants -import { USER_WORKSPACES_LIST } from "@plane/constants"; -// helpers -import { EPageTypes } from "@/helpers/authentication.helper"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUser } from "@/hooks/store/user"; -// wrappers -import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; -// services -import { WorkspaceService } from "@/services/workspace.service"; - -const workspaceService = new WorkspaceService(); - -function OnboardingPage() { - // store hooks - const { data: user } = useUser(); - const { fetchWorkspaces } = useWorkspace(); - - // fetching workspaces list - useSWR(USER_WORKSPACES_LIST, () => { - if (user?.id) { - fetchWorkspaces(); - } - }); - - // fetching user workspace invitations - const { isLoading: invitationsLoader, data: invitations } = useSWR( - `USER_WORKSPACE_INVITATIONS_LIST_${user?.id}`, - () => { - if (user?.id) return workspaceService.userWorkspaceInvitations(); - } - ); - - return ( - -
-
-
- {user && !invitationsLoader ? ( - - ) : ( -
- -
- )} -
-
-
-
- ); -} - -export default observer(OnboardingPage); diff --git a/apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx b/apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx deleted file mode 100644 index d7df27ccb..000000000 --- a/apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane imports -import { PROFILE_SETTINGS_TABS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import type { TProfileSettingsTabs } from "@plane/types"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { PageHead } from "@/components/core/page-title"; -import { ProfileSettingsContent } from "@/components/settings/profile/content"; -import { ProfileSettingsSidebarRoot } from "@/components/settings/profile/sidebar"; -// hooks -import { useUser } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -// local imports -import type { Route } from "../+types/layout"; - -function ProfileSettingsPage(props: Route.ComponentProps) { - const { profileTabId } = props.params; - // router - const router = useAppRouter(); - // store hooks - const { data: currentUser } = useUser(); - // translation - const { t } = useTranslation(); - // derived values - const isAValidTab = PROFILE_SETTINGS_TABS.includes(profileTabId as TProfileSettingsTabs); - - if (!currentUser || !isAValidTab) - return ( -
- -
- ); - - return ( - <> - -
-
- router.push(`/settings/profile/${tab}`)} - /> - -
-
- - ); -} - -export default observer(ProfileSettingsPage); diff --git a/apps/web/app/(all)/settings/profile/layout.tsx b/apps/web/app/(all)/settings/profile/layout.tsx deleted file mode 100644 index a30518f65..000000000 --- a/apps/web/app/(all)/settings/profile/layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// components -import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider"; -// lib -import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; - -export default function ProfileSettingsLayout() { - return ( - <> - - -
-
-
- -
-
-
-
- - ); -} diff --git a/apps/web/app/(all)/sign-up/layout.tsx b/apps/web/app/(all)/sign-up/layout.tsx deleted file mode 100644 index 97a8147e4..000000000 --- a/apps/web/app/(all)/sign-up/layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import type { Route } from "./+types/layout"; - -export const meta: Route.MetaFunction = () => [ - { title: "Sign up - Plane" }, - { name: "robots", content: "index, nofollow" }, -]; - -export default function SignUpLayout() { - return ; -} diff --git a/apps/web/app/(all)/sign-up/page.tsx b/apps/web/app/(all)/sign-up/page.tsx deleted file mode 100644 index 639d765cc..000000000 --- a/apps/web/app/(all)/sign-up/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// components -import { AuthBase } from "@/components/auth-screens/auth-base"; -// helpers -import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper"; -// assets -import DefaultLayout from "@/layouts/default-layout"; -import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; - -function SignUpPage() { - return ( - - - - - - ); -} - -export default SignUpPage; diff --git a/apps/web/app/(all)/workspace-invitations/layout.tsx b/apps/web/app/(all)/workspace-invitations/layout.tsx deleted file mode 100644 index b9a733746..000000000 --- a/apps/web/app/(all)/workspace-invitations/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -import type { Route } from "./+types/layout"; - -export default function WorkspaceInvitationsLayout() { - return ; -} - -export const meta: Route.MetaFunction = () => [{ title: "Workspace Invitations" }]; diff --git a/apps/web/app/(all)/workspace-invitations/page.tsx b/apps/web/app/(all)/workspace-invitations/page.tsx deleted file mode 100644 index d7e580e86..000000000 --- a/apps/web/app/(all)/workspace-invitations/page.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -import useSWR from "swr"; -import { - BoxesOutline, - CloseOutline, - ShareAltOutline, - StarOutline, - TickOutline, - UserOutline, -} from "@makeplane/propel/icons"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { EmptySpace, EmptySpaceItem } from "@/components/ui/empty-space"; -// constants -import { WORKSPACE_INVITATION } from "@plane/constants"; -// helpers -import { EPageTypes } from "@/helpers/authentication.helper"; -// hooks -import { useUser } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -// wrappers -import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; -import { WorkspaceService } from "@/services/workspace.service"; -// services - -// service initialization -const workspaceService = new WorkspaceService(); - -function WorkspaceInvitationPage() { - // router - const router = useAppRouter(); - // query params - const searchParams = useSearchParams(); - const invitation_id = searchParams.get("invitation_id"); - const slug = searchParams.get("slug"); - const token = searchParams.get("token"); - // store hooks - const { data: currentUser } = useUser(); - - const { data: invitationDetail, error } = useSWR( - invitation_id && slug && WORKSPACE_INVITATION(invitation_id.toString()), - invitation_id && slug - ? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString()) - : null - ); - - const handleAccept = () => { - if (!invitationDetail) return; - workspaceService - .joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, { - accepted: true, - token: token, - }) - .then(() => { - if (invitationDetail.email === currentUser?.email) { - router.push(`/${invitationDetail.workspace.slug}`); - } else { - router.push("/"); - } - }) - .catch((err: unknown) => console.error(err)); - }; - - const handleReject = () => { - if (!invitationDetail || !token) return; - void workspaceService - .joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, { - accepted: false, - token: token, - }) - .then(() => { - router.push("/"); - }) - .catch((err: unknown) => console.error(err)); - }; - - return ( - -
- {invitationDetail && !invitationDetail.responded_at ? ( - error ? ( -
-

INVITATION NOT FOUND

-
- ) : ( - - - - - ) - ) : error || invitationDetail?.responded_at ? ( - invitationDetail?.accepted ? ( - - - - ) : ( - - {!currentUser ? ( - - ) : ( - - )} - - - - ) - ) : ( -
- -
- )} -
-
- ); -} - -export default observer(WorkspaceInvitationPage); diff --git a/apps/web/app/(home)/layout.tsx b/apps/web/app/(home)/layout.tsx deleted file mode 100644 index c723b076c..000000000 --- a/apps/web/app/(home)/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// types -import type { Route } from "./+types/layout"; - -export const meta: Route.MetaFunction = () => [ - { name: "robots", content: "index, nofollow" }, - { name: "viewport", content: "width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover" }, -]; - -export default function HomeLayout() { - return ; -} diff --git a/apps/web/app/(home)/page.tsx b/apps/web/app/(home)/page.tsx deleted file mode 100644 index 4717705f1..000000000 --- a/apps/web/app/(home)/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -// components -import { AuthBase } from "@/components/auth-screens/auth-base"; -// helpers -import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper"; -// layouts -import DefaultLayout from "@/layouts/default-layout"; -// wrappers -import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; - -function HomePage() { - return ( - - - - - - ); -} - -export default HomePage; diff --git a/apps/web/app/assets/404.svg b/apps/web/app/assets/404.svg deleted file mode 100644 index 4c298417d..000000000 --- a/apps/web/app/assets/404.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/attachment/audio-icon.png b/apps/web/app/assets/attachment/audio-icon.png deleted file mode 100644 index a3e551ea7..000000000 Binary files a/apps/web/app/assets/attachment/audio-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/css-icon.png b/apps/web/app/assets/attachment/css-icon.png deleted file mode 100644 index cfb502d97..000000000 Binary files a/apps/web/app/assets/attachment/css-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/csv-icon.png b/apps/web/app/assets/attachment/csv-icon.png deleted file mode 100644 index 39d0ee713..000000000 Binary files a/apps/web/app/assets/attachment/csv-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/default-icon.png b/apps/web/app/assets/attachment/default-icon.png deleted file mode 100644 index eb1ea5175..000000000 Binary files a/apps/web/app/assets/attachment/default-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/doc-icon.png b/apps/web/app/assets/attachment/doc-icon.png deleted file mode 100644 index d1433721b..000000000 Binary files a/apps/web/app/assets/attachment/doc-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/excel-icon.png b/apps/web/app/assets/attachment/excel-icon.png deleted file mode 100644 index b3a1b851e..000000000 Binary files a/apps/web/app/assets/attachment/excel-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/figma-icon.png b/apps/web/app/assets/attachment/figma-icon.png deleted file mode 100644 index b4a1b63b3..000000000 Binary files a/apps/web/app/assets/attachment/figma-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/html-icon.png b/apps/web/app/assets/attachment/html-icon.png deleted file mode 100644 index b6259a2be..000000000 Binary files a/apps/web/app/assets/attachment/html-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/img-icon.png b/apps/web/app/assets/attachment/img-icon.png deleted file mode 100644 index 6c5b8fce0..000000000 Binary files a/apps/web/app/assets/attachment/img-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/jpg-icon.png b/apps/web/app/assets/attachment/jpg-icon.png deleted file mode 100644 index dfd2c9fde..000000000 Binary files a/apps/web/app/assets/attachment/jpg-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/js-icon.png b/apps/web/app/assets/attachment/js-icon.png deleted file mode 100644 index 66aacdaff..000000000 Binary files a/apps/web/app/assets/attachment/js-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/pdf-icon.png b/apps/web/app/assets/attachment/pdf-icon.png deleted file mode 100644 index 21c42d73f..000000000 Binary files a/apps/web/app/assets/attachment/pdf-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/png-icon.png b/apps/web/app/assets/attachment/png-icon.png deleted file mode 100644 index f04207daa..000000000 Binary files a/apps/web/app/assets/attachment/png-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/rar-icon.png b/apps/web/app/assets/attachment/rar-icon.png deleted file mode 100644 index 7305455bd..000000000 Binary files a/apps/web/app/assets/attachment/rar-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/svg-icon.png b/apps/web/app/assets/attachment/svg-icon.png deleted file mode 100644 index 856f94fbe..000000000 Binary files a/apps/web/app/assets/attachment/svg-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/txt-icon.png b/apps/web/app/assets/attachment/txt-icon.png deleted file mode 100644 index 1c1babf9c..000000000 Binary files a/apps/web/app/assets/attachment/txt-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/video-icon.png b/apps/web/app/assets/attachment/video-icon.png deleted file mode 100644 index 510838d91..000000000 Binary files a/apps/web/app/assets/attachment/video-icon.png and /dev/null differ diff --git a/apps/web/app/assets/attachment/zip-icon.png b/apps/web/app/assets/attachment/zip-icon.png deleted file mode 100644 index 0db1da136..000000000 Binary files a/apps/web/app/assets/attachment/zip-icon.png and /dev/null differ diff --git a/apps/web/app/assets/auth/access-denied.svg b/apps/web/app/assets/auth/access-denied.svg deleted file mode 100644 index c7979fee2..000000000 --- a/apps/web/app/assets/auth/access-denied.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/auth/background-pattern-dark.svg b/apps/web/app/assets/auth/background-pattern-dark.svg deleted file mode 100644 index c258cbabf..000000000 --- a/apps/web/app/assets/auth/background-pattern-dark.svg +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/auth/background-pattern.svg b/apps/web/app/assets/auth/background-pattern.svg deleted file mode 100644 index 5fcbeec27..000000000 --- a/apps/web/app/assets/auth/background-pattern.svg +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/auth/gradient-bg-logo.webp b/apps/web/app/assets/auth/gradient-bg-logo.webp deleted file mode 100644 index 47202e562..000000000 Binary files a/apps/web/app/assets/auth/gradient-bg-logo.webp and /dev/null differ diff --git a/apps/web/app/assets/auth/gradient-logo.webp b/apps/web/app/assets/auth/gradient-logo.webp deleted file mode 100644 index 674434cee..000000000 Binary files a/apps/web/app/assets/auth/gradient-logo.webp and /dev/null differ diff --git a/apps/web/app/assets/auth/project-not-authorized.svg b/apps/web/app/assets/auth/project-not-authorized.svg deleted file mode 100644 index 515fd6807..000000000 --- a/apps/web/app/assets/auth/project-not-authorized.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/auth/unauthorized.svg b/apps/web/app/assets/auth/unauthorized.svg deleted file mode 100644 index 2bad48a57..000000000 --- a/apps/web/app/assets/auth/unauthorized.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Error unauthorized - \ No newline at end of file diff --git a/apps/web/app/assets/auth/workspace-not-authorized.svg b/apps/web/app/assets/auth/workspace-not-authorized.svg deleted file mode 100644 index dad2b0c05..000000000 --- a/apps/web/app/assets/auth/workspace-not-authorized.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/cover-images/image_1.jpg b/apps/web/app/assets/cover-images/image_1.jpg deleted file mode 100644 index 3565e8890..000000000 Binary files a/apps/web/app/assets/cover-images/image_1.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_10.jpg b/apps/web/app/assets/cover-images/image_10.jpg deleted file mode 100644 index cecad95d4..000000000 Binary files a/apps/web/app/assets/cover-images/image_10.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_11.jpg b/apps/web/app/assets/cover-images/image_11.jpg deleted file mode 100644 index ebe34f24d..000000000 Binary files a/apps/web/app/assets/cover-images/image_11.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_12.jpg b/apps/web/app/assets/cover-images/image_12.jpg deleted file mode 100644 index cae254d46..000000000 Binary files a/apps/web/app/assets/cover-images/image_12.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_13.jpg b/apps/web/app/assets/cover-images/image_13.jpg deleted file mode 100644 index a3f4d408b..000000000 Binary files a/apps/web/app/assets/cover-images/image_13.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_14.jpg b/apps/web/app/assets/cover-images/image_14.jpg deleted file mode 100644 index 3b3da8801..000000000 Binary files a/apps/web/app/assets/cover-images/image_14.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_15.jpg b/apps/web/app/assets/cover-images/image_15.jpg deleted file mode 100644 index 3e44b7f8d..000000000 Binary files a/apps/web/app/assets/cover-images/image_15.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_16.jpg b/apps/web/app/assets/cover-images/image_16.jpg deleted file mode 100644 index f31335bf2..000000000 Binary files a/apps/web/app/assets/cover-images/image_16.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_17.jpg b/apps/web/app/assets/cover-images/image_17.jpg deleted file mode 100644 index d2a50342d..000000000 Binary files a/apps/web/app/assets/cover-images/image_17.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_18.jpg b/apps/web/app/assets/cover-images/image_18.jpg deleted file mode 100644 index 10cf37c30..000000000 Binary files a/apps/web/app/assets/cover-images/image_18.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_19.jpg b/apps/web/app/assets/cover-images/image_19.jpg deleted file mode 100644 index dca561943..000000000 Binary files a/apps/web/app/assets/cover-images/image_19.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_2.jpg b/apps/web/app/assets/cover-images/image_2.jpg deleted file mode 100644 index a1adad526..000000000 Binary files a/apps/web/app/assets/cover-images/image_2.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_20.jpg b/apps/web/app/assets/cover-images/image_20.jpg deleted file mode 100644 index a8daf9772..000000000 Binary files a/apps/web/app/assets/cover-images/image_20.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_21.jpg b/apps/web/app/assets/cover-images/image_21.jpg deleted file mode 100644 index 57c094ebd..000000000 Binary files a/apps/web/app/assets/cover-images/image_21.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_22.jpg b/apps/web/app/assets/cover-images/image_22.jpg deleted file mode 100644 index 9efc564b9..000000000 Binary files a/apps/web/app/assets/cover-images/image_22.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_23.jpg b/apps/web/app/assets/cover-images/image_23.jpg deleted file mode 100644 index fec33d99d..000000000 Binary files a/apps/web/app/assets/cover-images/image_23.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_24.jpg b/apps/web/app/assets/cover-images/image_24.jpg deleted file mode 100644 index 54c74a692..000000000 Binary files a/apps/web/app/assets/cover-images/image_24.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_25.jpg b/apps/web/app/assets/cover-images/image_25.jpg deleted file mode 100644 index 66841c06b..000000000 Binary files a/apps/web/app/assets/cover-images/image_25.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_26.jpg b/apps/web/app/assets/cover-images/image_26.jpg deleted file mode 100644 index d0c276602..000000000 Binary files a/apps/web/app/assets/cover-images/image_26.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_27.jpg b/apps/web/app/assets/cover-images/image_27.jpg deleted file mode 100644 index 84abce2ef..000000000 Binary files a/apps/web/app/assets/cover-images/image_27.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_28.jpg b/apps/web/app/assets/cover-images/image_28.jpg deleted file mode 100644 index 0ce78e34f..000000000 Binary files a/apps/web/app/assets/cover-images/image_28.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_29.jpg b/apps/web/app/assets/cover-images/image_29.jpg deleted file mode 100644 index 9df7aa0e0..000000000 Binary files a/apps/web/app/assets/cover-images/image_29.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_3.jpg b/apps/web/app/assets/cover-images/image_3.jpg deleted file mode 100644 index 451849288..000000000 Binary files a/apps/web/app/assets/cover-images/image_3.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_4.jpg b/apps/web/app/assets/cover-images/image_4.jpg deleted file mode 100644 index 04d110931..000000000 Binary files a/apps/web/app/assets/cover-images/image_4.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_5.jpg b/apps/web/app/assets/cover-images/image_5.jpg deleted file mode 100644 index 6dcdb25a3..000000000 Binary files a/apps/web/app/assets/cover-images/image_5.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_6.jpg b/apps/web/app/assets/cover-images/image_6.jpg deleted file mode 100644 index f1cb9bf01..000000000 Binary files a/apps/web/app/assets/cover-images/image_6.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_7.jpg b/apps/web/app/assets/cover-images/image_7.jpg deleted file mode 100644 index c70602ca9..000000000 Binary files a/apps/web/app/assets/cover-images/image_7.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_8.jpg b/apps/web/app/assets/cover-images/image_8.jpg deleted file mode 100644 index 508bd5578..000000000 Binary files a/apps/web/app/assets/cover-images/image_8.jpg and /dev/null differ diff --git a/apps/web/app/assets/cover-images/image_9.jpg b/apps/web/app/assets/cover-images/image_9.jpg deleted file mode 100644 index d5267f961..000000000 Binary files a/apps/web/app/assets/cover-images/image_9.jpg and /dev/null differ diff --git a/apps/web/app/assets/emoji/project-emoji.svg b/apps/web/app/assets/emoji/project-emoji.svg deleted file mode 100644 index a2b9a6208..000000000 --- a/apps/web/app/assets/emoji/project-emoji.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/web/app/assets/empty-state/active-cycle/assignee-dark.webp b/apps/web/app/assets/empty-state/active-cycle/assignee-dark.webp deleted file mode 100644 index 259062767..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/assignee-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/active-cycle/assignee-light.webp b/apps/web/app/assets/empty-state/active-cycle/assignee-light.webp deleted file mode 100644 index b8ae5c010..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/assignee-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/active-cycle/chart-dark.webp b/apps/web/app/assets/empty-state/active-cycle/chart-dark.webp deleted file mode 100644 index 033d6fe3c..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/chart-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/active-cycle/chart-light.webp b/apps/web/app/assets/empty-state/active-cycle/chart-light.webp deleted file mode 100644 index 587906fba..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/chart-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/active-cycle/cycle-dark.webp b/apps/web/app/assets/empty-state/active-cycle/cycle-dark.webp deleted file mode 100644 index d09230804..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/cycle-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/active-cycle/cycle-light.webp b/apps/web/app/assets/empty-state/active-cycle/cycle-light.webp deleted file mode 100644 index a218d4d26..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/cycle-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/active-cycle/label-dark.webp b/apps/web/app/assets/empty-state/active-cycle/label-dark.webp deleted file mode 100644 index c15d3be26..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/label-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/active-cycle/label-light.webp b/apps/web/app/assets/empty-state/active-cycle/label-light.webp deleted file mode 100644 index e5ccc6994..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/label-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/active-cycle/priority-dark.webp b/apps/web/app/assets/empty-state/active-cycle/priority-dark.webp deleted file mode 100644 index f662e92ee..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/priority-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/active-cycle/priority-light.webp b/apps/web/app/assets/empty-state/active-cycle/priority-light.webp deleted file mode 100644 index fde814c6a..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/priority-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/active-cycle/progress-dark.webp b/apps/web/app/assets/empty-state/active-cycle/progress-dark.webp deleted file mode 100644 index 4beda66ea..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/progress-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/active-cycle/progress-light.webp b/apps/web/app/assets/empty-state/active-cycle/progress-light.webp deleted file mode 100644 index ae30cf76a..000000000 Binary files a/apps/web/app/assets/empty-state/active-cycle/progress-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/all-issues-dark.webp b/apps/web/app/assets/empty-state/all-issues/all-issues-dark.webp deleted file mode 100644 index f570ad502..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/all-issues-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/all-issues-light.webp b/apps/web/app/assets/empty-state/all-issues/all-issues-light.webp deleted file mode 100644 index eba388d02..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/all-issues-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/assigned-dark.webp b/apps/web/app/assets/empty-state/all-issues/assigned-dark.webp deleted file mode 100644 index 23bc60ace..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/assigned-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/assigned-light.webp b/apps/web/app/assets/empty-state/all-issues/assigned-light.webp deleted file mode 100644 index f48b9cf7b..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/assigned-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/created-dark.webp b/apps/web/app/assets/empty-state/all-issues/created-dark.webp deleted file mode 100644 index 69d35fc10..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/created-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/created-light.webp b/apps/web/app/assets/empty-state/all-issues/created-light.webp deleted file mode 100644 index be4f67d67..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/created-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/custom-view-dark.webp b/apps/web/app/assets/empty-state/all-issues/custom-view-dark.webp deleted file mode 100644 index 48dec16a9..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/custom-view-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/custom-view-light.webp b/apps/web/app/assets/empty-state/all-issues/custom-view-light.webp deleted file mode 100644 index 53b551c7b..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/custom-view-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/no-project-dark.webp b/apps/web/app/assets/empty-state/all-issues/no-project-dark.webp deleted file mode 100644 index 641ec828d..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/no-project-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/no-project-light.webp b/apps/web/app/assets/empty-state/all-issues/no-project-light.webp deleted file mode 100644 index 1c7a810f5..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/no-project-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/subscribed-dark.webp b/apps/web/app/assets/empty-state/all-issues/subscribed-dark.webp deleted file mode 100644 index 68b3b30b3..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/subscribed-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/all-issues/subscribed-light.webp b/apps/web/app/assets/empty-state/all-issues/subscribed-light.webp deleted file mode 100644 index 4dbbedca0..000000000 Binary files a/apps/web/app/assets/empty-state/all-issues/subscribed-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/analytics/empty-chart-area-dark.webp b/apps/web/app/assets/empty-state/analytics/empty-chart-area-dark.webp deleted file mode 100644 index 509f66ccc..000000000 Binary files a/apps/web/app/assets/empty-state/analytics/empty-chart-area-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/analytics/empty-chart-area-light.webp b/apps/web/app/assets/empty-state/analytics/empty-chart-area-light.webp deleted file mode 100644 index cfa27c8ca..000000000 Binary files a/apps/web/app/assets/empty-state/analytics/empty-chart-area-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/analytics/empty-chart-bar-dark.webp b/apps/web/app/assets/empty-state/analytics/empty-chart-bar-dark.webp deleted file mode 100644 index e519574cb..000000000 Binary files a/apps/web/app/assets/empty-state/analytics/empty-chart-bar-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/analytics/empty-chart-bar-light.webp b/apps/web/app/assets/empty-state/analytics/empty-chart-bar-light.webp deleted file mode 100644 index e6eeb09db..000000000 Binary files a/apps/web/app/assets/empty-state/analytics/empty-chart-bar-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/analytics/empty-chart-radar-dark.webp b/apps/web/app/assets/empty-state/analytics/empty-chart-radar-dark.webp deleted file mode 100644 index 94e2f514b..000000000 Binary files a/apps/web/app/assets/empty-state/analytics/empty-chart-radar-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/analytics/empty-chart-radar-light.webp b/apps/web/app/assets/empty-state/analytics/empty-chart-radar-light.webp deleted file mode 100644 index 29820169b..000000000 Binary files a/apps/web/app/assets/empty-state/analytics/empty-chart-radar-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/analytics/empty-grid-background-dark.webp b/apps/web/app/assets/empty-state/analytics/empty-grid-background-dark.webp deleted file mode 100644 index 5e420f285..000000000 Binary files a/apps/web/app/assets/empty-state/analytics/empty-grid-background-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/analytics/empty-grid-background-light.webp b/apps/web/app/assets/empty-state/analytics/empty-grid-background-light.webp deleted file mode 100644 index 592a12816..000000000 Binary files a/apps/web/app/assets/empty-state/analytics/empty-grid-background-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/analytics/empty-table-dark.webp b/apps/web/app/assets/empty-state/analytics/empty-table-dark.webp deleted file mode 100644 index f5fcedd51..000000000 Binary files a/apps/web/app/assets/empty-state/analytics/empty-table-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/analytics/empty-table-light.webp b/apps/web/app/assets/empty-state/analytics/empty-table-light.webp deleted file mode 100644 index c9e8fec95..000000000 Binary files a/apps/web/app/assets/empty-state/analytics/empty-table-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/api-token.svg b/apps/web/app/assets/empty-state/api-token.svg deleted file mode 100644 index f8e32d3e5..000000000 --- a/apps/web/app/assets/empty-state/api-token.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/archived/empty-cycles-dark.webp b/apps/web/app/assets/empty-state/archived/empty-cycles-dark.webp deleted file mode 100644 index 872fb2fca..000000000 Binary files a/apps/web/app/assets/empty-state/archived/empty-cycles-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/archived/empty-cycles-light.webp b/apps/web/app/assets/empty-state/archived/empty-cycles-light.webp deleted file mode 100644 index 2db1dc4b7..000000000 Binary files a/apps/web/app/assets/empty-state/archived/empty-cycles-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/archived/empty-issues-dark.webp b/apps/web/app/assets/empty-state/archived/empty-issues-dark.webp deleted file mode 100644 index c18e94629..000000000 Binary files a/apps/web/app/assets/empty-state/archived/empty-issues-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/archived/empty-issues-light.webp b/apps/web/app/assets/empty-state/archived/empty-issues-light.webp deleted file mode 100644 index d99a5017e..000000000 Binary files a/apps/web/app/assets/empty-state/archived/empty-issues-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/archived/empty-modules-dark.webp b/apps/web/app/assets/empty-state/archived/empty-modules-dark.webp deleted file mode 100644 index e34bf3737..000000000 Binary files a/apps/web/app/assets/empty-state/archived/empty-modules-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/archived/empty-modules-light.webp b/apps/web/app/assets/empty-state/archived/empty-modules-light.webp deleted file mode 100644 index 5caf2ea90..000000000 Binary files a/apps/web/app/assets/empty-state/archived/empty-modules-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/calendar-dark-resp.webp b/apps/web/app/assets/empty-state/cycle-issues/calendar-dark-resp.webp deleted file mode 100644 index 5f0c21979..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/calendar-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/calendar-dark.webp b/apps/web/app/assets/empty-state/cycle-issues/calendar-dark.webp deleted file mode 100644 index 312b13890..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/calendar-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/calendar-light-resp.webp b/apps/web/app/assets/empty-state/cycle-issues/calendar-light-resp.webp deleted file mode 100644 index 556a45080..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/calendar-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/calendar-light.webp b/apps/web/app/assets/empty-state/cycle-issues/calendar-light.webp deleted file mode 100644 index aef2ff176..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/calendar-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-dark-resp.webp b/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-dark-resp.webp deleted file mode 100644 index e2eda0528..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-dark.webp b/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-dark.webp deleted file mode 100644 index d2a9dfbb5..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-light-resp.webp b/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-light-resp.webp deleted file mode 100644 index 1a915d32d..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-light.webp b/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-light.webp deleted file mode 100644 index 9dcbdd6f4..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/gantt_chart-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/kanban-dark-resp.webp b/apps/web/app/assets/empty-state/cycle-issues/kanban-dark-resp.webp deleted file mode 100644 index 227dd488e..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/kanban-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/kanban-dark.webp b/apps/web/app/assets/empty-state/cycle-issues/kanban-dark.webp deleted file mode 100644 index b161dbc7d..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/kanban-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/kanban-light-resp.webp b/apps/web/app/assets/empty-state/cycle-issues/kanban-light-resp.webp deleted file mode 100644 index c377068c7..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/kanban-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/kanban-light.webp b/apps/web/app/assets/empty-state/cycle-issues/kanban-light.webp deleted file mode 100644 index 0958f212a..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/kanban-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/list-dark-resp.webp b/apps/web/app/assets/empty-state/cycle-issues/list-dark-resp.webp deleted file mode 100644 index 4088595c2..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/list-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/list-dark.webp b/apps/web/app/assets/empty-state/cycle-issues/list-dark.webp deleted file mode 100644 index 7e9f8c654..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/list-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/list-light-resp.webp b/apps/web/app/assets/empty-state/cycle-issues/list-light-resp.webp deleted file mode 100644 index 862544a05..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/list-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/list-light.webp b/apps/web/app/assets/empty-state/cycle-issues/list-light.webp deleted file mode 100644 index fb01bff1d..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/list-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-dark-resp.webp b/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-dark-resp.webp deleted file mode 100644 index baccb3fb7..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-dark.webp b/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-dark.webp deleted file mode 100644 index 6f6036198..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-light-resp.webp b/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-light-resp.webp deleted file mode 100644 index c7889d668..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-light.webp b/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-light.webp deleted file mode 100644 index 0f35b4ebf..000000000 Binary files a/apps/web/app/assets/empty-state/cycle-issues/spreadsheet-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle.svg b/apps/web/app/assets/empty-state/cycle.svg deleted file mode 100644 index fe1dba3f8..000000000 --- a/apps/web/app/assets/empty-state/cycle.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/cycle/active-dark.webp b/apps/web/app/assets/empty-state/cycle/active-dark.webp deleted file mode 100644 index 68317b1be..000000000 Binary files a/apps/web/app/assets/empty-state/cycle/active-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle/active-light.webp b/apps/web/app/assets/empty-state/cycle/active-light.webp deleted file mode 100644 index 2341a9053..000000000 Binary files a/apps/web/app/assets/empty-state/cycle/active-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle/all-filters.svg b/apps/web/app/assets/empty-state/cycle/all-filters.svg deleted file mode 100644 index b00841cc8..000000000 --- a/apps/web/app/assets/empty-state/cycle/all-filters.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/cycle/completed-dark.webp b/apps/web/app/assets/empty-state/cycle/completed-dark.webp deleted file mode 100644 index eebe656d6..000000000 Binary files a/apps/web/app/assets/empty-state/cycle/completed-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle/completed-light.webp b/apps/web/app/assets/empty-state/cycle/completed-light.webp deleted file mode 100644 index 6c192fd35..000000000 Binary files a/apps/web/app/assets/empty-state/cycle/completed-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle/completed-no-issues-dark.webp b/apps/web/app/assets/empty-state/cycle/completed-no-issues-dark.webp deleted file mode 100644 index a187dbf5c..000000000 Binary files a/apps/web/app/assets/empty-state/cycle/completed-no-issues-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle/completed-no-issues-light.webp b/apps/web/app/assets/empty-state/cycle/completed-no-issues-light.webp deleted file mode 100644 index 6009f60f4..000000000 Binary files a/apps/web/app/assets/empty-state/cycle/completed-no-issues-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle/draft-dark.webp b/apps/web/app/assets/empty-state/cycle/draft-dark.webp deleted file mode 100644 index 887e2e916..000000000 Binary files a/apps/web/app/assets/empty-state/cycle/draft-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle/draft-light.webp b/apps/web/app/assets/empty-state/cycle/draft-light.webp deleted file mode 100644 index d1c88bc37..000000000 Binary files a/apps/web/app/assets/empty-state/cycle/draft-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle/name-filter.svg b/apps/web/app/assets/empty-state/cycle/name-filter.svg deleted file mode 100644 index 168611119..000000000 --- a/apps/web/app/assets/empty-state/cycle/name-filter.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/cycle/upcoming-dark.webp b/apps/web/app/assets/empty-state/cycle/upcoming-dark.webp deleted file mode 100644 index 205164f48..000000000 Binary files a/apps/web/app/assets/empty-state/cycle/upcoming-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/cycle/upcoming-light.webp b/apps/web/app/assets/empty-state/cycle/upcoming-light.webp deleted file mode 100644 index 522122f4a..000000000 Binary files a/apps/web/app/assets/empty-state/cycle/upcoming-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/dashboard/dark/completed-issues.svg b/apps/web/app/assets/empty-state/dashboard/dark/completed-issues.svg deleted file mode 100644 index 8c4d083fb..000000000 --- a/apps/web/app/assets/empty-state/dashboard/dark/completed-issues.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/dark/issues-by-priority.svg b/apps/web/app/assets/empty-state/dashboard/dark/issues-by-priority.svg deleted file mode 100644 index 803f3a727..000000000 --- a/apps/web/app/assets/empty-state/dashboard/dark/issues-by-priority.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/dark/issues-by-state-group.svg b/apps/web/app/assets/empty-state/dashboard/dark/issues-by-state-group.svg deleted file mode 100644 index b615c313f..000000000 --- a/apps/web/app/assets/empty-state/dashboard/dark/issues-by-state-group.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/dark/overdue-issues.svg b/apps/web/app/assets/empty-state/dashboard/dark/overdue-issues.svg deleted file mode 100644 index cceb3480d..000000000 --- a/apps/web/app/assets/empty-state/dashboard/dark/overdue-issues.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/dark/recent-activity.svg b/apps/web/app/assets/empty-state/dashboard/dark/recent-activity.svg deleted file mode 100644 index bf7234828..000000000 --- a/apps/web/app/assets/empty-state/dashboard/dark/recent-activity.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/dark/recent-collaborators-1.svg b/apps/web/app/assets/empty-state/dashboard/dark/recent-collaborators-1.svg deleted file mode 100644 index 341364f59..000000000 --- a/apps/web/app/assets/empty-state/dashboard/dark/recent-collaborators-1.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/dark/recent-collaborators-2.svg b/apps/web/app/assets/empty-state/dashboard/dark/recent-collaborators-2.svg deleted file mode 100644 index 6889f750e..000000000 --- a/apps/web/app/assets/empty-state/dashboard/dark/recent-collaborators-2.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/dark/recent-collaborators-3.svg b/apps/web/app/assets/empty-state/dashboard/dark/recent-collaborators-3.svg deleted file mode 100644 index deb0e211c..000000000 --- a/apps/web/app/assets/empty-state/dashboard/dark/recent-collaborators-3.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/dark/upcoming-issues.svg b/apps/web/app/assets/empty-state/dashboard/dark/upcoming-issues.svg deleted file mode 100644 index 25599450a..000000000 --- a/apps/web/app/assets/empty-state/dashboard/dark/upcoming-issues.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/light/completed-issues.svg b/apps/web/app/assets/empty-state/dashboard/light/completed-issues.svg deleted file mode 100644 index 6f9c4c430..000000000 --- a/apps/web/app/assets/empty-state/dashboard/light/completed-issues.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/light/issues-by-priority.svg b/apps/web/app/assets/empty-state/dashboard/light/issues-by-priority.svg deleted file mode 100644 index 23c35d92f..000000000 --- a/apps/web/app/assets/empty-state/dashboard/light/issues-by-priority.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/light/issues-by-state-group.svg b/apps/web/app/assets/empty-state/dashboard/light/issues-by-state-group.svg deleted file mode 100644 index eb3275372..000000000 --- a/apps/web/app/assets/empty-state/dashboard/light/issues-by-state-group.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/light/overdue-issues.svg b/apps/web/app/assets/empty-state/dashboard/light/overdue-issues.svg deleted file mode 100644 index 1c66ddd0b..000000000 --- a/apps/web/app/assets/empty-state/dashboard/light/overdue-issues.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/light/recent-activity.svg b/apps/web/app/assets/empty-state/dashboard/light/recent-activity.svg deleted file mode 100644 index 3bb67491d..000000000 --- a/apps/web/app/assets/empty-state/dashboard/light/recent-activity.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/light/recent-collaborators-1.svg b/apps/web/app/assets/empty-state/dashboard/light/recent-collaborators-1.svg deleted file mode 100644 index fb77e418d..000000000 --- a/apps/web/app/assets/empty-state/dashboard/light/recent-collaborators-1.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/light/recent-collaborators-2.svg b/apps/web/app/assets/empty-state/dashboard/light/recent-collaborators-2.svg deleted file mode 100644 index e25fc9532..000000000 --- a/apps/web/app/assets/empty-state/dashboard/light/recent-collaborators-2.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/light/recent-collaborators-3.svg b/apps/web/app/assets/empty-state/dashboard/light/recent-collaborators-3.svg deleted file mode 100644 index e46473e84..000000000 --- a/apps/web/app/assets/empty-state/dashboard/light/recent-collaborators-3.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/light/upcoming-issues.svg b/apps/web/app/assets/empty-state/dashboard/light/upcoming-issues.svg deleted file mode 100644 index ad3035819..000000000 --- a/apps/web/app/assets/empty-state/dashboard/light/upcoming-issues.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/dashboard/widgets-dark.webp b/apps/web/app/assets/empty-state/dashboard/widgets-dark.webp deleted file mode 100644 index 3a626f78e..000000000 Binary files a/apps/web/app/assets/empty-state/dashboard/widgets-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/dashboard/widgets-light.webp b/apps/web/app/assets/empty-state/dashboard/widgets-light.webp deleted file mode 100644 index 6161c57c2..000000000 Binary files a/apps/web/app/assets/empty-state/dashboard/widgets-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/dashboard_empty_project.webp b/apps/web/app/assets/empty-state/dashboard_empty_project.webp deleted file mode 100644 index c1f633f6b..000000000 Binary files a/apps/web/app/assets/empty-state/dashboard_empty_project.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/disabled-feature/cycles-dark.webp b/apps/web/app/assets/empty-state/disabled-feature/cycles-dark.webp deleted file mode 100644 index 589706280..000000000 Binary files a/apps/web/app/assets/empty-state/disabled-feature/cycles-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/disabled-feature/cycles-light.webp b/apps/web/app/assets/empty-state/disabled-feature/cycles-light.webp deleted file mode 100644 index c13d0826d..000000000 Binary files a/apps/web/app/assets/empty-state/disabled-feature/cycles-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/disabled-feature/intake-dark.webp b/apps/web/app/assets/empty-state/disabled-feature/intake-dark.webp deleted file mode 100644 index 9b459b8c8..000000000 Binary files a/apps/web/app/assets/empty-state/disabled-feature/intake-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/disabled-feature/intake-light.webp b/apps/web/app/assets/empty-state/disabled-feature/intake-light.webp deleted file mode 100644 index 70757be9a..000000000 Binary files a/apps/web/app/assets/empty-state/disabled-feature/intake-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/disabled-feature/modules-dark.webp b/apps/web/app/assets/empty-state/disabled-feature/modules-dark.webp deleted file mode 100644 index 8e198ed33..000000000 Binary files a/apps/web/app/assets/empty-state/disabled-feature/modules-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/disabled-feature/modules-light.webp b/apps/web/app/assets/empty-state/disabled-feature/modules-light.webp deleted file mode 100644 index d1db65cc0..000000000 Binary files a/apps/web/app/assets/empty-state/disabled-feature/modules-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/disabled-feature/pages-dark.webp b/apps/web/app/assets/empty-state/disabled-feature/pages-dark.webp deleted file mode 100644 index 19263a0cb..000000000 Binary files a/apps/web/app/assets/empty-state/disabled-feature/pages-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/disabled-feature/pages-light.webp b/apps/web/app/assets/empty-state/disabled-feature/pages-light.webp deleted file mode 100644 index fd1c5b4d6..000000000 Binary files a/apps/web/app/assets/empty-state/disabled-feature/pages-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/disabled-feature/views-dark.webp b/apps/web/app/assets/empty-state/disabled-feature/views-dark.webp deleted file mode 100644 index bfdb573fe..000000000 Binary files a/apps/web/app/assets/empty-state/disabled-feature/views-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/disabled-feature/views-light.webp b/apps/web/app/assets/empty-state/disabled-feature/views-light.webp deleted file mode 100644 index e318e2717..000000000 Binary files a/apps/web/app/assets/empty-state/disabled-feature/views-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/draft/draft-issues-empty-dark.webp b/apps/web/app/assets/empty-state/draft/draft-issues-empty-dark.webp deleted file mode 100644 index c50ed24b5..000000000 Binary files a/apps/web/app/assets/empty-state/draft/draft-issues-empty-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/draft/draft-issues-empty-light.webp b/apps/web/app/assets/empty-state/draft/draft-issues-empty-light.webp deleted file mode 100644 index c6ded2b04..000000000 Binary files a/apps/web/app/assets/empty-state/draft/draft-issues-empty-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty-filters/calendar-dark.webp b/apps/web/app/assets/empty-state/empty-filters/calendar-dark.webp deleted file mode 100644 index 872be08bc..000000000 Binary files a/apps/web/app/assets/empty-state/empty-filters/calendar-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty-filters/calendar-light.webp b/apps/web/app/assets/empty-state/empty-filters/calendar-light.webp deleted file mode 100644 index 50e56eb90..000000000 Binary files a/apps/web/app/assets/empty-state/empty-filters/calendar-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty-filters/gantt_chart-dark.webp b/apps/web/app/assets/empty-state/empty-filters/gantt_chart-dark.webp deleted file mode 100644 index c587d968b..000000000 Binary files a/apps/web/app/assets/empty-state/empty-filters/gantt_chart-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty-filters/gantt_chart-light.webp b/apps/web/app/assets/empty-state/empty-filters/gantt_chart-light.webp deleted file mode 100644 index 8c874ea6e..000000000 Binary files a/apps/web/app/assets/empty-state/empty-filters/gantt_chart-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty-filters/kanban-dark.webp b/apps/web/app/assets/empty-state/empty-filters/kanban-dark.webp deleted file mode 100644 index a368c6867..000000000 Binary files a/apps/web/app/assets/empty-state/empty-filters/kanban-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty-filters/kanban-light.webp b/apps/web/app/assets/empty-state/empty-filters/kanban-light.webp deleted file mode 100644 index 50507e7ee..000000000 Binary files a/apps/web/app/assets/empty-state/empty-filters/kanban-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty-filters/list-dark.webp b/apps/web/app/assets/empty-state/empty-filters/list-dark.webp deleted file mode 100644 index 6c49f29f3..000000000 Binary files a/apps/web/app/assets/empty-state/empty-filters/list-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty-filters/list-light.webp b/apps/web/app/assets/empty-state/empty-filters/list-light.webp deleted file mode 100644 index bc4f28475..000000000 Binary files a/apps/web/app/assets/empty-state/empty-filters/list-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty-filters/spreadsheet-dark.webp b/apps/web/app/assets/empty-state/empty-filters/spreadsheet-dark.webp deleted file mode 100644 index 2e9ff4c9f..000000000 Binary files a/apps/web/app/assets/empty-state/empty-filters/spreadsheet-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty-filters/spreadsheet-light.webp b/apps/web/app/assets/empty-state/empty-filters/spreadsheet-light.webp deleted file mode 100644 index 174b3d084..000000000 Binary files a/apps/web/app/assets/empty-state/empty-filters/spreadsheet-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty-updates-light.png b/apps/web/app/assets/empty-state/empty-updates-light.png deleted file mode 100644 index a92c09371..000000000 Binary files a/apps/web/app/assets/empty-state/empty-updates-light.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty_analytics.webp b/apps/web/app/assets/empty-state/empty_analytics.webp deleted file mode 100644 index 46174fff2..000000000 Binary files a/apps/web/app/assets/empty-state/empty_analytics.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty_bar_graph.svg b/apps/web/app/assets/empty-state/empty_bar_graph.svg deleted file mode 100644 index 7742a4238..000000000 --- a/apps/web/app/assets/empty-state/empty_bar_graph.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/apps/web/app/assets/empty-state/empty_cycles.webp b/apps/web/app/assets/empty-state/empty_cycles.webp deleted file mode 100644 index 710ae2dce..000000000 Binary files a/apps/web/app/assets/empty-state/empty_cycles.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty_graph.svg b/apps/web/app/assets/empty-state/empty_graph.svg deleted file mode 100644 index 5eae523a5..000000000 --- a/apps/web/app/assets/empty-state/empty_graph.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/empty_issues.webp b/apps/web/app/assets/empty-state/empty_issues.webp deleted file mode 100644 index e60d663d1..000000000 Binary files a/apps/web/app/assets/empty-state/empty_issues.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty_label.svg b/apps/web/app/assets/empty-state/empty_label.svg deleted file mode 100644 index c664da6f4..000000000 --- a/apps/web/app/assets/empty-state/empty_label.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/web/app/assets/empty-state/empty_members.svg b/apps/web/app/assets/empty-state/empty_members.svg deleted file mode 100644 index 6672c587b..000000000 --- a/apps/web/app/assets/empty-state/empty_members.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/empty_modules.webp b/apps/web/app/assets/empty-state/empty_modules.webp deleted file mode 100644 index 49050b53b..000000000 Binary files a/apps/web/app/assets/empty-state/empty_modules.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty_page.png b/apps/web/app/assets/empty-state/empty_page.png deleted file mode 100644 index 993774c70..000000000 Binary files a/apps/web/app/assets/empty-state/empty_page.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty_project.webp b/apps/web/app/assets/empty-state/empty_project.webp deleted file mode 100644 index ad66f0966..000000000 Binary files a/apps/web/app/assets/empty-state/empty_project.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/empty_users.svg b/apps/web/app/assets/empty-state/empty_users.svg deleted file mode 100644 index 7298e9e8f..000000000 --- a/apps/web/app/assets/empty-state/empty_users.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/web/app/assets/empty-state/empty_view.webp b/apps/web/app/assets/empty-state/empty_view.webp deleted file mode 100644 index aa0f5a65e..000000000 Binary files a/apps/web/app/assets/empty-state/empty_view.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/epics/epics-dark.webp b/apps/web/app/assets/empty-state/epics/epics-dark.webp deleted file mode 100644 index 648392af4..000000000 Binary files a/apps/web/app/assets/empty-state/epics/epics-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/epics/epics-light.webp b/apps/web/app/assets/empty-state/epics/epics-light.webp deleted file mode 100644 index f677e5d66..000000000 Binary files a/apps/web/app/assets/empty-state/epics/epics-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/epics/settings-dark.webp b/apps/web/app/assets/empty-state/epics/settings-dark.webp deleted file mode 100644 index 31931379d..000000000 Binary files a/apps/web/app/assets/empty-state/epics/settings-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/epics/settings-light.webp b/apps/web/app/assets/empty-state/epics/settings-light.webp deleted file mode 100644 index 1f9596eac..000000000 Binary files a/apps/web/app/assets/empty-state/epics/settings-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/estimates/dark.svg b/apps/web/app/assets/empty-state/estimates/dark.svg deleted file mode 100644 index 2f5d7589f..000000000 --- a/apps/web/app/assets/empty-state/estimates/dark.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/estimates/light.svg b/apps/web/app/assets/empty-state/estimates/light.svg deleted file mode 100644 index 817b9fb12..000000000 --- a/apps/web/app/assets/empty-state/estimates/light.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/intake/filter-issue-dark.webp b/apps/web/app/assets/empty-state/intake/filter-issue-dark.webp deleted file mode 100644 index dab3eb78c..000000000 Binary files a/apps/web/app/assets/empty-state/intake/filter-issue-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/intake/filter-issue-light.webp b/apps/web/app/assets/empty-state/intake/filter-issue-light.webp deleted file mode 100644 index 671670672..000000000 Binary files a/apps/web/app/assets/empty-state/intake/filter-issue-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/intake/intake-dark-resp.webp b/apps/web/app/assets/empty-state/intake/intake-dark-resp.webp deleted file mode 100644 index bf6166e67..000000000 Binary files a/apps/web/app/assets/empty-state/intake/intake-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/intake/intake-dark.webp b/apps/web/app/assets/empty-state/intake/intake-dark.webp deleted file mode 100644 index 9b459b8c8..000000000 Binary files a/apps/web/app/assets/empty-state/intake/intake-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/intake/intake-issue-dark.webp b/apps/web/app/assets/empty-state/intake/intake-issue-dark.webp deleted file mode 100644 index 0c14ce249..000000000 Binary files a/apps/web/app/assets/empty-state/intake/intake-issue-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/intake/intake-issue-light.webp b/apps/web/app/assets/empty-state/intake/intake-issue-light.webp deleted file mode 100644 index c557f5dc0..000000000 Binary files a/apps/web/app/assets/empty-state/intake/intake-issue-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/intake/intake-light-resp.webp b/apps/web/app/assets/empty-state/intake/intake-light-resp.webp deleted file mode 100644 index 69a79d678..000000000 Binary files a/apps/web/app/assets/empty-state/intake/intake-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/intake/intake-light.webp b/apps/web/app/assets/empty-state/intake/intake-light.webp deleted file mode 100644 index 70757be9a..000000000 Binary files a/apps/web/app/assets/empty-state/intake/intake-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/intake/issue-detail-dark.webp b/apps/web/app/assets/empty-state/intake/issue-detail-dark.webp deleted file mode 100644 index 2a2c7063b..000000000 Binary files a/apps/web/app/assets/empty-state/intake/issue-detail-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/intake/issue-detail-light.webp b/apps/web/app/assets/empty-state/intake/issue-detail-light.webp deleted file mode 100644 index a71306910..000000000 Binary files a/apps/web/app/assets/empty-state/intake/issue-detail-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/invitation.svg b/apps/web/app/assets/empty-state/invitation.svg deleted file mode 100644 index bda66dadc..000000000 --- a/apps/web/app/assets/empty-state/invitation.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/issue.svg b/apps/web/app/assets/empty-state/issue.svg deleted file mode 100644 index 98a420d75..000000000 --- a/apps/web/app/assets/empty-state/issue.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/label.svg b/apps/web/app/assets/empty-state/label.svg deleted file mode 100644 index 55241e251..000000000 --- a/apps/web/app/assets/empty-state/label.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/module-issues/calendar-dark-resp.webp b/apps/web/app/assets/empty-state/module-issues/calendar-dark-resp.webp deleted file mode 100644 index 5e6404e75..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/calendar-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/calendar-dark.webp b/apps/web/app/assets/empty-state/module-issues/calendar-dark.webp deleted file mode 100644 index 5170f6c04..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/calendar-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/calendar-light-resp.webp b/apps/web/app/assets/empty-state/module-issues/calendar-light-resp.webp deleted file mode 100644 index 0d890af68..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/calendar-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/calendar-light.webp b/apps/web/app/assets/empty-state/module-issues/calendar-light.webp deleted file mode 100644 index 5bee9c91d..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/calendar-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/gantt_chart-dark-resp.webp b/apps/web/app/assets/empty-state/module-issues/gantt_chart-dark-resp.webp deleted file mode 100644 index 90d7985ab..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/gantt_chart-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/gantt_chart-dark.webp b/apps/web/app/assets/empty-state/module-issues/gantt_chart-dark.webp deleted file mode 100644 index d38219832..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/gantt_chart-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/gantt_chart-light-resp.webp b/apps/web/app/assets/empty-state/module-issues/gantt_chart-light-resp.webp deleted file mode 100644 index ccaadee83..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/gantt_chart-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/gantt_chart-light.webp b/apps/web/app/assets/empty-state/module-issues/gantt_chart-light.webp deleted file mode 100644 index e51bbeea7..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/gantt_chart-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/kanban-dark-resp.webp b/apps/web/app/assets/empty-state/module-issues/kanban-dark-resp.webp deleted file mode 100644 index b447fb6c6..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/kanban-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/kanban-dark.webp b/apps/web/app/assets/empty-state/module-issues/kanban-dark.webp deleted file mode 100644 index 5e978d598..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/kanban-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/kanban-light-resp.webp b/apps/web/app/assets/empty-state/module-issues/kanban-light-resp.webp deleted file mode 100644 index 2be0b8f18..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/kanban-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/kanban-light.webp b/apps/web/app/assets/empty-state/module-issues/kanban-light.webp deleted file mode 100644 index 5d30a652e..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/kanban-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/list-dark-resp.webp b/apps/web/app/assets/empty-state/module-issues/list-dark-resp.webp deleted file mode 100644 index 84d92aaa1..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/list-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/list-dark.webp b/apps/web/app/assets/empty-state/module-issues/list-dark.webp deleted file mode 100644 index 96e9518c2..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/list-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/list-light-resp.webp b/apps/web/app/assets/empty-state/module-issues/list-light-resp.webp deleted file mode 100644 index b688add24..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/list-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/list-light.webp b/apps/web/app/assets/empty-state/module-issues/list-light.webp deleted file mode 100644 index d9b1c4c22..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/list-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/spreadsheet-dark-resp.webp b/apps/web/app/assets/empty-state/module-issues/spreadsheet-dark-resp.webp deleted file mode 100644 index afb2f1cbc..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/spreadsheet-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/spreadsheet-dark.webp b/apps/web/app/assets/empty-state/module-issues/spreadsheet-dark.webp deleted file mode 100644 index 1894cfe75..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/spreadsheet-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/spreadsheet-light-resp.webp b/apps/web/app/assets/empty-state/module-issues/spreadsheet-light-resp.webp deleted file mode 100644 index 11115d4d4..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/spreadsheet-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module-issues/spreadsheet-light.webp b/apps/web/app/assets/empty-state/module-issues/spreadsheet-light.webp deleted file mode 100644 index aa3813f58..000000000 Binary files a/apps/web/app/assets/empty-state/module-issues/spreadsheet-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/module.svg b/apps/web/app/assets/empty-state/module.svg deleted file mode 100644 index 359b3e972..000000000 --- a/apps/web/app/assets/empty-state/module.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/module/all-filters.svg b/apps/web/app/assets/empty-state/module/all-filters.svg deleted file mode 100644 index 6ba0731fe..000000000 --- a/apps/web/app/assets/empty-state/module/all-filters.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/module/name-filter.svg b/apps/web/app/assets/empty-state/module/name-filter.svg deleted file mode 100644 index 0d9655b66..000000000 --- a/apps/web/app/assets/empty-state/module/name-filter.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/notification.svg b/apps/web/app/assets/empty-state/notification.svg deleted file mode 100644 index 700a1552f..000000000 --- a/apps/web/app/assets/empty-state/notification.svg +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/web/app/assets/empty-state/onboarding/analytics-dark.webp b/apps/web/app/assets/empty-state/onboarding/analytics-dark.webp deleted file mode 100644 index a83d55de9..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/analytics-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/analytics-light.webp b/apps/web/app/assets/empty-state/onboarding/analytics-light.webp deleted file mode 100644 index 64a762f99..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/analytics-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/archive-dark.png b/apps/web/app/assets/empty-state/onboarding/archive-dark.png deleted file mode 100644 index 54cca36ec..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/archive-dark.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/archive-light.png b/apps/web/app/assets/empty-state/onboarding/archive-light.png deleted file mode 100644 index 8530bbac1..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/archive-light.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/cycles-dark.webp b/apps/web/app/assets/empty-state/onboarding/cycles-dark.webp deleted file mode 100644 index 716f121d7..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/cycles-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/cycles-light.webp b/apps/web/app/assets/empty-state/onboarding/cycles-light.webp deleted file mode 100644 index 0b2b40088..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/cycles-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/dashboard-dark.webp b/apps/web/app/assets/empty-state/onboarding/dashboard-dark.webp deleted file mode 100644 index afa10bea7..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/dashboard-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/dashboard-light.webp b/apps/web/app/assets/empty-state/onboarding/dashboard-light.webp deleted file mode 100644 index d4bce8bee..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/dashboard-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/graph-dark.png b/apps/web/app/assets/empty-state/onboarding/graph-dark.png deleted file mode 100644 index e8a938d8f..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/graph-dark.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/graph-light.png b/apps/web/app/assets/empty-state/onboarding/graph-light.png deleted file mode 100644 index eb48b45a1..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/graph-light.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/issues-closed-dark.png b/apps/web/app/assets/empty-state/onboarding/issues-closed-dark.png deleted file mode 100644 index d4bafb28e..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/issues-closed-dark.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/issues-closed-light.png b/apps/web/app/assets/empty-state/onboarding/issues-closed-light.png deleted file mode 100644 index 739acd10d..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/issues-closed-light.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/issues-dark.webp b/apps/web/app/assets/empty-state/onboarding/issues-dark.webp deleted file mode 100644 index 39ffb5c17..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/issues-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/issues-light.webp b/apps/web/app/assets/empty-state/onboarding/issues-light.webp deleted file mode 100644 index 18d42b19c..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/issues-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/members-dark.png b/apps/web/app/assets/empty-state/onboarding/members-dark.png deleted file mode 100644 index 2f8a1c873..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/members-dark.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/members-light.png b/apps/web/app/assets/empty-state/onboarding/members-light.png deleted file mode 100644 index eaa43ef90..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/members-light.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/modules-dark.webp b/apps/web/app/assets/empty-state/onboarding/modules-dark.webp deleted file mode 100644 index d855df030..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/modules-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/modules-light.webp b/apps/web/app/assets/empty-state/onboarding/modules-light.webp deleted file mode 100644 index ff3e6ac31..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/modules-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/notification-dark.png b/apps/web/app/assets/empty-state/onboarding/notification-dark.png deleted file mode 100644 index dee284ce6..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/notification-dark.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/notification-light.png b/apps/web/app/assets/empty-state/onboarding/notification-light.png deleted file mode 100644 index 3065787e1..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/notification-light.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/pages-dark.webp b/apps/web/app/assets/empty-state/onboarding/pages-dark.webp deleted file mode 100644 index bc592703f..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/pages-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/pages-light.webp b/apps/web/app/assets/empty-state/onboarding/pages-light.webp deleted file mode 100644 index 1e28c0d31..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/pages-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/projects-dark.webp b/apps/web/app/assets/empty-state/onboarding/projects-dark.webp deleted file mode 100644 index d4f6c627d..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/projects-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/projects-light.webp b/apps/web/app/assets/empty-state/onboarding/projects-light.webp deleted file mode 100644 index c475c8ff5..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/projects-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/search-dark.png b/apps/web/app/assets/empty-state/onboarding/search-dark.png deleted file mode 100644 index 531081d43..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/search-dark.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/search-light.png b/apps/web/app/assets/empty-state/onboarding/search-light.png deleted file mode 100644 index 39e263667..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/search-light.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/snooze-light.png b/apps/web/app/assets/empty-state/onboarding/snooze-light.png deleted file mode 100644 index 6ccd7ab91..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/snooze-light.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/snoozed-dark.png b/apps/web/app/assets/empty-state/onboarding/snoozed-dark.png deleted file mode 100644 index c8b88fe56..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/snoozed-dark.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/views-dark.webp b/apps/web/app/assets/empty-state/onboarding/views-dark.webp deleted file mode 100644 index 0a7fe7b22..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/views-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/views-light.webp b/apps/web/app/assets/empty-state/onboarding/views-light.webp deleted file mode 100644 index cfb2031b1..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/views-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/workspace-active-cycles-dark.webp b/apps/web/app/assets/empty-state/onboarding/workspace-active-cycles-dark.webp deleted file mode 100644 index 68317b1be..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/workspace-active-cycles-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/workspace-active-cycles-light.webp b/apps/web/app/assets/empty-state/onboarding/workspace-active-cycles-light.webp deleted file mode 100644 index 2341a9053..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/workspace-active-cycles-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/workspace-invites-dark.webp b/apps/web/app/assets/empty-state/onboarding/workspace-invites-dark.webp deleted file mode 100644 index d6e10046a..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/workspace-invites-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/onboarding/workspace-invites-light.webp b/apps/web/app/assets/empty-state/onboarding/workspace-invites-light.webp deleted file mode 100644 index 123a600ef..000000000 Binary files a/apps/web/app/assets/empty-state/onboarding/workspace-invites-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/activities-dark.webp b/apps/web/app/assets/empty-state/profile/activities-dark.webp deleted file mode 100644 index 1b693947a..000000000 Binary files a/apps/web/app/assets/empty-state/profile/activities-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/activities-light.webp b/apps/web/app/assets/empty-state/profile/activities-light.webp deleted file mode 100644 index c287e0cd5..000000000 Binary files a/apps/web/app/assets/empty-state/profile/activities-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/activity-dark.webp b/apps/web/app/assets/empty-state/profile/activity-dark.webp deleted file mode 100644 index d44e6a1de..000000000 Binary files a/apps/web/app/assets/empty-state/profile/activity-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/activity-light.webp b/apps/web/app/assets/empty-state/profile/activity-light.webp deleted file mode 100644 index 206685a4c..000000000 Binary files a/apps/web/app/assets/empty-state/profile/activity-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/assigned-dark.webp b/apps/web/app/assets/empty-state/profile/assigned-dark.webp deleted file mode 100644 index 173ebe621..000000000 Binary files a/apps/web/app/assets/empty-state/profile/assigned-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/assigned-light.webp b/apps/web/app/assets/empty-state/profile/assigned-light.webp deleted file mode 100644 index 78416c927..000000000 Binary files a/apps/web/app/assets/empty-state/profile/assigned-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/created-dark.webp b/apps/web/app/assets/empty-state/profile/created-dark.webp deleted file mode 100644 index 8232ce2ce..000000000 Binary files a/apps/web/app/assets/empty-state/profile/created-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/created-light.webp b/apps/web/app/assets/empty-state/profile/created-light.webp deleted file mode 100644 index 4ea037aeb..000000000 Binary files a/apps/web/app/assets/empty-state/profile/created-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/issues-by-priority-dark.webp b/apps/web/app/assets/empty-state/profile/issues-by-priority-dark.webp deleted file mode 100644 index e1a71802e..000000000 Binary files a/apps/web/app/assets/empty-state/profile/issues-by-priority-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/issues-by-priority-light.webp b/apps/web/app/assets/empty-state/profile/issues-by-priority-light.webp deleted file mode 100644 index 16abada61..000000000 Binary files a/apps/web/app/assets/empty-state/profile/issues-by-priority-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/issues-by-state-dark.webp b/apps/web/app/assets/empty-state/profile/issues-by-state-dark.webp deleted file mode 100644 index 82210aa86..000000000 Binary files a/apps/web/app/assets/empty-state/profile/issues-by-state-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/issues-by-state-light.webp b/apps/web/app/assets/empty-state/profile/issues-by-state-light.webp deleted file mode 100644 index 73788bb86..000000000 Binary files a/apps/web/app/assets/empty-state/profile/issues-by-state-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/subscribed-dark.webp b/apps/web/app/assets/empty-state/profile/subscribed-dark.webp deleted file mode 100644 index 26837e1a9..000000000 Binary files a/apps/web/app/assets/empty-state/profile/subscribed-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/profile/subscribed-light.webp b/apps/web/app/assets/empty-state/profile/subscribed-light.webp deleted file mode 100644 index 7faf66099..000000000 Binary files a/apps/web/app/assets/empty-state/profile/subscribed-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/estimates-dark-resp.webp b/apps/web/app/assets/empty-state/project-settings/estimates-dark-resp.webp deleted file mode 100644 index 6f6a49492..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/estimates-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/estimates-dark.png b/apps/web/app/assets/empty-state/project-settings/estimates-dark.png deleted file mode 100644 index 9cc5346e6..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/estimates-dark.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/estimates-dark.webp b/apps/web/app/assets/empty-state/project-settings/estimates-dark.webp deleted file mode 100644 index 35d42e52d..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/estimates-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/estimates-light-resp.webp b/apps/web/app/assets/empty-state/project-settings/estimates-light-resp.webp deleted file mode 100644 index 13a29caf5..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/estimates-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/estimates-light.png b/apps/web/app/assets/empty-state/project-settings/estimates-light.png deleted file mode 100644 index f846a2a41..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/estimates-light.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/estimates-light.webp b/apps/web/app/assets/empty-state/project-settings/estimates-light.webp deleted file mode 100644 index b329ea916..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/estimates-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/integrations-dark-resp.webp b/apps/web/app/assets/empty-state/project-settings/integrations-dark-resp.webp deleted file mode 100644 index f968f8f40..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/integrations-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/integrations-dark.webp b/apps/web/app/assets/empty-state/project-settings/integrations-dark.webp deleted file mode 100644 index f968f8f40..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/integrations-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/integrations-light-resp.webp b/apps/web/app/assets/empty-state/project-settings/integrations-light-resp.webp deleted file mode 100644 index 94f6ffe39..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/integrations-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/integrations-light.webp b/apps/web/app/assets/empty-state/project-settings/integrations-light.webp deleted file mode 100644 index 94f6ffe39..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/integrations-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/labels-dark-resp.webp b/apps/web/app/assets/empty-state/project-settings/labels-dark-resp.webp deleted file mode 100644 index bf23c60ba..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/labels-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/labels-dark.webp b/apps/web/app/assets/empty-state/project-settings/labels-dark.webp deleted file mode 100644 index 6d41f556d..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/labels-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/labels-light-resp.webp b/apps/web/app/assets/empty-state/project-settings/labels-light-resp.webp deleted file mode 100644 index d992fb1f3..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/labels-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/labels-light.webp b/apps/web/app/assets/empty-state/project-settings/labels-light.webp deleted file mode 100644 index c6e701544..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/labels-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/no-projects-dark.png b/apps/web/app/assets/empty-state/project-settings/no-projects-dark.png deleted file mode 100644 index deed3d0c0..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/no-projects-dark.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/no-projects-light.png b/apps/web/app/assets/empty-state/project-settings/no-projects-light.png deleted file mode 100644 index e038de289..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/no-projects-light.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/updates-dark.png b/apps/web/app/assets/empty-state/project-settings/updates-dark.png deleted file mode 100644 index 46e27091a..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/updates-dark.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project-settings/updates-light.png b/apps/web/app/assets/empty-state/project-settings/updates-light.png deleted file mode 100644 index d2e920876..000000000 Binary files a/apps/web/app/assets/empty-state/project-settings/updates-light.png and /dev/null differ diff --git a/apps/web/app/assets/empty-state/project.svg b/apps/web/app/assets/empty-state/project.svg deleted file mode 100644 index b89854909..000000000 --- a/apps/web/app/assets/empty-state/project.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/project/all-filters-dark.svg b/apps/web/app/assets/empty-state/project/all-filters-dark.svg deleted file mode 100644 index e0d69c46c..000000000 --- a/apps/web/app/assets/empty-state/project/all-filters-dark.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/project/all-filters-light.svg b/apps/web/app/assets/empty-state/project/all-filters-light.svg deleted file mode 100644 index 93ac4de74..000000000 --- a/apps/web/app/assets/empty-state/project/all-filters-light.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/project/name-filter-dark.svg b/apps/web/app/assets/empty-state/project/name-filter-dark.svg deleted file mode 100644 index df4c2379c..000000000 --- a/apps/web/app/assets/empty-state/project/name-filter-dark.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/project/name-filter-light.svg b/apps/web/app/assets/empty-state/project/name-filter-light.svg deleted file mode 100644 index ec6322837..000000000 --- a/apps/web/app/assets/empty-state/project/name-filter-light.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/project/name-filter.svg b/apps/web/app/assets/empty-state/project/name-filter.svg deleted file mode 100644 index ec6322837..000000000 --- a/apps/web/app/assets/empty-state/project/name-filter.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/recent_activity.svg b/apps/web/app/assets/empty-state/recent_activity.svg deleted file mode 100644 index bef318fae..000000000 --- a/apps/web/app/assets/empty-state/recent_activity.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/web/app/assets/empty-state/search/all-issue-view-dark.webp b/apps/web/app/assets/empty-state/search/all-issue-view-dark.webp deleted file mode 100644 index dbc69c0aa..000000000 Binary files a/apps/web/app/assets/empty-state/search/all-issue-view-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/all-issues-view-light.webp b/apps/web/app/assets/empty-state/search/all-issues-view-light.webp deleted file mode 100644 index 829b97377..000000000 Binary files a/apps/web/app/assets/empty-state/search/all-issues-view-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/archive-dark.webp b/apps/web/app/assets/empty-state/search/archive-dark.webp deleted file mode 100644 index d586be888..000000000 Binary files a/apps/web/app/assets/empty-state/search/archive-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/archive-light.webp b/apps/web/app/assets/empty-state/search/archive-light.webp deleted file mode 100644 index 5a79179e1..000000000 Binary files a/apps/web/app/assets/empty-state/search/archive-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/comments-dark.webp b/apps/web/app/assets/empty-state/search/comments-dark.webp deleted file mode 100644 index d06ba7e42..000000000 Binary files a/apps/web/app/assets/empty-state/search/comments-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/comments-light.webp b/apps/web/app/assets/empty-state/search/comments-light.webp deleted file mode 100644 index 5a66067d6..000000000 Binary files a/apps/web/app/assets/empty-state/search/comments-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/issues-dark.webp b/apps/web/app/assets/empty-state/search/issues-dark.webp deleted file mode 100644 index dcd00aa94..000000000 Binary files a/apps/web/app/assets/empty-state/search/issues-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/issues-light.webp b/apps/web/app/assets/empty-state/search/issues-light.webp deleted file mode 100644 index 7703fdede..000000000 Binary files a/apps/web/app/assets/empty-state/search/issues-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/member-dark.webp b/apps/web/app/assets/empty-state/search/member-dark.webp deleted file mode 100644 index 20521a5d5..000000000 Binary files a/apps/web/app/assets/empty-state/search/member-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/member-light.webp b/apps/web/app/assets/empty-state/search/member-light.webp deleted file mode 100644 index dcd38c974..000000000 Binary files a/apps/web/app/assets/empty-state/search/member-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/notification-dark.webp b/apps/web/app/assets/empty-state/search/notification-dark.webp deleted file mode 100644 index cb299d112..000000000 Binary files a/apps/web/app/assets/empty-state/search/notification-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/notification-light.webp b/apps/web/app/assets/empty-state/search/notification-light.webp deleted file mode 100644 index 55c4ffac7..000000000 Binary files a/apps/web/app/assets/empty-state/search/notification-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/project-dark.webp b/apps/web/app/assets/empty-state/search/project-dark.webp deleted file mode 100644 index 98c65721a..000000000 Binary files a/apps/web/app/assets/empty-state/search/project-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/project-light.webp b/apps/web/app/assets/empty-state/search/project-light.webp deleted file mode 100644 index cb3e470d0..000000000 Binary files a/apps/web/app/assets/empty-state/search/project-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/search-dark.webp b/apps/web/app/assets/empty-state/search/search-dark.webp deleted file mode 100644 index 92abc616b..000000000 Binary files a/apps/web/app/assets/empty-state/search/search-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/search-light.webp b/apps/web/app/assets/empty-state/search/search-light.webp deleted file mode 100644 index 807ed2615..000000000 Binary files a/apps/web/app/assets/empty-state/search/search-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/snooze-dark.webp b/apps/web/app/assets/empty-state/search/snooze-dark.webp deleted file mode 100644 index 296e797e8..000000000 Binary files a/apps/web/app/assets/empty-state/search/snooze-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/snooze-light.webp b/apps/web/app/assets/empty-state/search/snooze-light.webp deleted file mode 100644 index 897ff3598..000000000 Binary files a/apps/web/app/assets/empty-state/search/snooze-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/views-dark.webp b/apps/web/app/assets/empty-state/search/views-dark.webp deleted file mode 100644 index 96da5e336..000000000 Binary files a/apps/web/app/assets/empty-state/search/views-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/search/views-light.webp b/apps/web/app/assets/empty-state/search/views-light.webp deleted file mode 100644 index 3cd1d23fd..000000000 Binary files a/apps/web/app/assets/empty-state/search/views-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/state_graph.svg b/apps/web/app/assets/empty-state/state_graph.svg deleted file mode 100644 index 651ea3474..000000000 --- a/apps/web/app/assets/empty-state/state_graph.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/stickies/stickies-dark.webp b/apps/web/app/assets/empty-state/stickies/stickies-dark.webp deleted file mode 100644 index a93e44baa..000000000 Binary files a/apps/web/app/assets/empty-state/stickies/stickies-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/stickies/stickies-light.webp b/apps/web/app/assets/empty-state/stickies/stickies-light.webp deleted file mode 100644 index c7fd1f125..000000000 Binary files a/apps/web/app/assets/empty-state/stickies/stickies-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/stickies/stickies-search-dark.webp b/apps/web/app/assets/empty-state/stickies/stickies-search-dark.webp deleted file mode 100644 index 6afdc0c51..000000000 Binary files a/apps/web/app/assets/empty-state/stickies/stickies-search-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/stickies/stickies-search-light.webp b/apps/web/app/assets/empty-state/stickies/stickies-search-light.webp deleted file mode 100644 index 2b8ca8c9c..000000000 Binary files a/apps/web/app/assets/empty-state/stickies/stickies-search-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/view.svg b/apps/web/app/assets/empty-state/view.svg deleted file mode 100644 index 478cf6e09..000000000 --- a/apps/web/app/assets/empty-state/view.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/web-hook.svg b/apps/web/app/assets/empty-state/web-hook.svg deleted file mode 100644 index f8e32d3e5..000000000 --- a/apps/web/app/assets/empty-state/web-hook.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/wiki/all-dark.webp b/apps/web/app/assets/empty-state/wiki/all-dark.webp deleted file mode 100644 index 81ef36de5..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/all-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/wiki/all-filters-dark.svg b/apps/web/app/assets/empty-state/wiki/all-filters-dark.svg deleted file mode 100644 index 037a2fd6f..000000000 --- a/apps/web/app/assets/empty-state/wiki/all-filters-dark.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/wiki/all-filters-light.svg b/apps/web/app/assets/empty-state/wiki/all-filters-light.svg deleted file mode 100644 index 8ecd8d2e6..000000000 --- a/apps/web/app/assets/empty-state/wiki/all-filters-light.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/wiki/all-light.webp b/apps/web/app/assets/empty-state/wiki/all-light.webp deleted file mode 100644 index 02b1d8897..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/all-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/wiki/archived-dark.webp b/apps/web/app/assets/empty-state/wiki/archived-dark.webp deleted file mode 100644 index 64700c83b..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/archived-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/wiki/archived-light.webp b/apps/web/app/assets/empty-state/wiki/archived-light.webp deleted file mode 100644 index 9412f73a4..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/archived-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/wiki/name-filter-dark.svg b/apps/web/app/assets/empty-state/wiki/name-filter-dark.svg deleted file mode 100644 index 969a1fe6e..000000000 --- a/apps/web/app/assets/empty-state/wiki/name-filter-dark.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/wiki/name-filter-light.svg b/apps/web/app/assets/empty-state/wiki/name-filter-light.svg deleted file mode 100644 index bc2465ae2..000000000 --- a/apps/web/app/assets/empty-state/wiki/name-filter-light.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/empty-state/wiki/navigation-pane/assets-dark.webp b/apps/web/app/assets/empty-state/wiki/navigation-pane/assets-dark.webp deleted file mode 100644 index c6bc05e23..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/navigation-pane/assets-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/wiki/navigation-pane/assets-light.webp b/apps/web/app/assets/empty-state/wiki/navigation-pane/assets-light.webp deleted file mode 100644 index 1e3982924..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/navigation-pane/assets-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/wiki/navigation-pane/outline-dark.webp b/apps/web/app/assets/empty-state/wiki/navigation-pane/outline-dark.webp deleted file mode 100644 index fca82f16b..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/navigation-pane/outline-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/wiki/navigation-pane/outline-light.webp b/apps/web/app/assets/empty-state/wiki/navigation-pane/outline-light.webp deleted file mode 100644 index 6870e6ccf..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/navigation-pane/outline-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/wiki/private-dark.webp b/apps/web/app/assets/empty-state/wiki/private-dark.webp deleted file mode 100644 index b482f4321..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/private-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/wiki/private-light.webp b/apps/web/app/assets/empty-state/wiki/private-light.webp deleted file mode 100644 index e1c3af918..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/private-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/wiki/public-dark.webp b/apps/web/app/assets/empty-state/wiki/public-dark.webp deleted file mode 100644 index 571bf8a9c..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/public-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/wiki/public-light.webp b/apps/web/app/assets/empty-state/wiki/public-light.webp deleted file mode 100644 index e0f7b7df0..000000000 Binary files a/apps/web/app/assets/empty-state/wiki/public-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-draft/issue-dark.webp b/apps/web/app/assets/empty-state/workspace-draft/issue-dark.webp deleted file mode 100644 index 276533523..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-draft/issue-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-draft/issue-light.webp b/apps/web/app/assets/empty-state/workspace-draft/issue-light.webp deleted file mode 100644 index 8be274b3c..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-draft/issue-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/api-tokens-dark-resp.webp b/apps/web/app/assets/empty-state/workspace-settings/api-tokens-dark-resp.webp deleted file mode 100644 index ae19eb46f..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/api-tokens-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/api-tokens-dark.webp b/apps/web/app/assets/empty-state/workspace-settings/api-tokens-dark.webp deleted file mode 100644 index bddaf05df..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/api-tokens-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/api-tokens-light-resp.webp b/apps/web/app/assets/empty-state/workspace-settings/api-tokens-light-resp.webp deleted file mode 100644 index 2ec9bf8bf..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/api-tokens-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/api-tokens-light.webp b/apps/web/app/assets/empty-state/workspace-settings/api-tokens-light.webp deleted file mode 100644 index ba3bc585e..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/api-tokens-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/exports-dark-resp.webp b/apps/web/app/assets/empty-state/workspace-settings/exports-dark-resp.webp deleted file mode 100644 index 6417ffa36..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/exports-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/exports-dark.webp b/apps/web/app/assets/empty-state/workspace-settings/exports-dark.webp deleted file mode 100644 index b0911c901..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/exports-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/exports-light-resp.webp b/apps/web/app/assets/empty-state/workspace-settings/exports-light-resp.webp deleted file mode 100644 index 29a695f35..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/exports-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/exports-light.webp b/apps/web/app/assets/empty-state/workspace-settings/exports-light.webp deleted file mode 100644 index 5e08f21d7..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/exports-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/imports-dark-resp.webp b/apps/web/app/assets/empty-state/workspace-settings/imports-dark-resp.webp deleted file mode 100644 index 70e789b4a..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/imports-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/imports-dark.webp b/apps/web/app/assets/empty-state/workspace-settings/imports-dark.webp deleted file mode 100644 index 428ee978b..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/imports-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/imports-light-resp.webp b/apps/web/app/assets/empty-state/workspace-settings/imports-light-resp.webp deleted file mode 100644 index 9bc95efa9..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/imports-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/imports-light.webp b/apps/web/app/assets/empty-state/workspace-settings/imports-light.webp deleted file mode 100644 index 5f42180eb..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/imports-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/integrations-dark-resp.webp b/apps/web/app/assets/empty-state/workspace-settings/integrations-dark-resp.webp deleted file mode 100644 index 3f9e00956..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/integrations-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/integrations-dark.webp b/apps/web/app/assets/empty-state/workspace-settings/integrations-dark.webp deleted file mode 100644 index 603a658ef..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/integrations-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/integrations-light-resp.webp b/apps/web/app/assets/empty-state/workspace-settings/integrations-light-resp.webp deleted file mode 100644 index 13e08be84..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/integrations-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/integrations-light.webp b/apps/web/app/assets/empty-state/workspace-settings/integrations-light.webp deleted file mode 100644 index eb62503e0..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/integrations-light.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/webhooks-dark-resp.webp b/apps/web/app/assets/empty-state/workspace-settings/webhooks-dark-resp.webp deleted file mode 100644 index 792307321..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/webhooks-dark-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/webhooks-dark.webp b/apps/web/app/assets/empty-state/workspace-settings/webhooks-dark.webp deleted file mode 100644 index 88a584eb8..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/webhooks-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/webhooks-light-resp.webp b/apps/web/app/assets/empty-state/workspace-settings/webhooks-light-resp.webp deleted file mode 100644 index ecb0f28ac..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/webhooks-light-resp.webp and /dev/null differ diff --git a/apps/web/app/assets/empty-state/workspace-settings/webhooks-light.webp b/apps/web/app/assets/empty-state/workspace-settings/webhooks-light.webp deleted file mode 100644 index 6c76b5af7..000000000 Binary files a/apps/web/app/assets/empty-state/workspace-settings/webhooks-light.webp and /dev/null differ diff --git a/apps/web/app/assets/favicon/apple-touch-icon.png b/apps/web/app/assets/favicon/apple-touch-icon.png deleted file mode 100644 index a63126783..000000000 Binary files a/apps/web/app/assets/favicon/apple-touch-icon.png and /dev/null differ diff --git a/apps/web/app/assets/favicon/favicon-16x16.png b/apps/web/app/assets/favicon/favicon-16x16.png deleted file mode 100644 index af59ef010..000000000 Binary files a/apps/web/app/assets/favicon/favicon-16x16.png and /dev/null differ diff --git a/apps/web/app/assets/favicon/favicon-32x32.png b/apps/web/app/assets/favicon/favicon-32x32.png deleted file mode 100644 index 16a1271ad..000000000 Binary files a/apps/web/app/assets/favicon/favicon-32x32.png and /dev/null differ diff --git a/apps/web/app/assets/favicon/favicon.ico b/apps/web/app/assets/favicon/favicon.ico deleted file mode 100644 index 613b1a313..000000000 Binary files a/apps/web/app/assets/favicon/favicon.ico and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/bold-italic.ttf b/apps/web/app/assets/fonts/inter/bold-italic.ttf deleted file mode 100644 index 0d19c1aab..000000000 Binary files a/apps/web/app/assets/fonts/inter/bold-italic.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/bold.ttf b/apps/web/app/assets/fonts/inter/bold.ttf deleted file mode 100644 index cd13f60ce..000000000 Binary files a/apps/web/app/assets/fonts/inter/bold.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/heavy-italic.ttf b/apps/web/app/assets/fonts/inter/heavy-italic.ttf deleted file mode 100644 index b33602f1e..000000000 Binary files a/apps/web/app/assets/fonts/inter/heavy-italic.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/heavy.ttf b/apps/web/app/assets/fonts/inter/heavy.ttf deleted file mode 100644 index 89673de1c..000000000 Binary files a/apps/web/app/assets/fonts/inter/heavy.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/light-italic.ttf b/apps/web/app/assets/fonts/inter/light-italic.ttf deleted file mode 100644 index f69e18b03..000000000 Binary files a/apps/web/app/assets/fonts/inter/light-italic.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/light.ttf b/apps/web/app/assets/fonts/inter/light.ttf deleted file mode 100644 index acae36128..000000000 Binary files a/apps/web/app/assets/fonts/inter/light.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/medium-italic.ttf b/apps/web/app/assets/fonts/inter/medium-italic.ttf deleted file mode 100644 index 5c8c8b145..000000000 Binary files a/apps/web/app/assets/fonts/inter/medium-italic.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/medium.ttf b/apps/web/app/assets/fonts/inter/medium.ttf deleted file mode 100644 index 71d90172f..000000000 Binary files a/apps/web/app/assets/fonts/inter/medium.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/regular-italic.ttf b/apps/web/app/assets/fonts/inter/regular-italic.ttf deleted file mode 100644 index 14d3595bc..000000000 Binary files a/apps/web/app/assets/fonts/inter/regular-italic.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/regular.ttf b/apps/web/app/assets/fonts/inter/regular.ttf deleted file mode 100644 index ce097c825..000000000 Binary files a/apps/web/app/assets/fonts/inter/regular.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/semibold-italic.ttf b/apps/web/app/assets/fonts/inter/semibold-italic.ttf deleted file mode 100644 index d9c9896d3..000000000 Binary files a/apps/web/app/assets/fonts/inter/semibold-italic.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/semibold.ttf b/apps/web/app/assets/fonts/inter/semibold.ttf deleted file mode 100644 index 053185e5d..000000000 Binary files a/apps/web/app/assets/fonts/inter/semibold.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/thin-italic.ttf b/apps/web/app/assets/fonts/inter/thin-italic.ttf deleted file mode 100644 index 134e8372f..000000000 Binary files a/apps/web/app/assets/fonts/inter/thin-italic.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/thin.ttf b/apps/web/app/assets/fonts/inter/thin.ttf deleted file mode 100644 index e68ec4704..000000000 Binary files a/apps/web/app/assets/fonts/inter/thin.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/ultrabold-italic.ttf b/apps/web/app/assets/fonts/inter/ultrabold-italic.ttf deleted file mode 100644 index df450629b..000000000 Binary files a/apps/web/app/assets/fonts/inter/ultrabold-italic.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/ultrabold.ttf b/apps/web/app/assets/fonts/inter/ultrabold.ttf deleted file mode 100644 index e71c601c0..000000000 Binary files a/apps/web/app/assets/fonts/inter/ultrabold.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/ultralight-italic.ttf b/apps/web/app/assets/fonts/inter/ultralight-italic.ttf deleted file mode 100644 index 275f305e2..000000000 Binary files a/apps/web/app/assets/fonts/inter/ultralight-italic.ttf and /dev/null differ diff --git a/apps/web/app/assets/fonts/inter/ultralight.ttf b/apps/web/app/assets/fonts/inter/ultralight.ttf deleted file mode 100644 index f9c6cfc5b..000000000 Binary files a/apps/web/app/assets/fonts/inter/ultralight.ttf and /dev/null differ diff --git a/apps/web/app/assets/icons/icon-180x180.png b/apps/web/app/assets/icons/icon-180x180.png deleted file mode 100644 index e7142bc60..000000000 Binary files a/apps/web/app/assets/icons/icon-180x180.png and /dev/null differ diff --git a/apps/web/app/assets/icons/icon-512x512.png b/apps/web/app/assets/icons/icon-512x512.png deleted file mode 100644 index 4c070d079..000000000 Binary files a/apps/web/app/assets/icons/icon-512x512.png and /dev/null differ diff --git a/apps/web/app/assets/images/logo-spinner-dark.gif b/apps/web/app/assets/images/logo-spinner-dark.gif deleted file mode 100644 index 8bd083255..000000000 Binary files a/apps/web/app/assets/images/logo-spinner-dark.gif and /dev/null differ diff --git a/apps/web/app/assets/images/logo-spinner-light.gif b/apps/web/app/assets/images/logo-spinner-light.gif deleted file mode 100644 index 8b5710314..000000000 Binary files a/apps/web/app/assets/images/logo-spinner-light.gif and /dev/null differ diff --git a/apps/web/app/assets/instance-not-ready.webp b/apps/web/app/assets/instance-not-ready.webp deleted file mode 100644 index ceabbcc0d..000000000 Binary files a/apps/web/app/assets/instance-not-ready.webp and /dev/null differ diff --git a/apps/web/app/assets/instance-setup-done.webp b/apps/web/app/assets/instance-setup-done.webp deleted file mode 100644 index 4773ed70b..000000000 Binary files a/apps/web/app/assets/instance-setup-done.webp and /dev/null differ diff --git a/apps/web/app/assets/instance/maintenance-mode-dark.svg b/apps/web/app/assets/instance/maintenance-mode-dark.svg deleted file mode 100644 index 546125ff9..000000000 --- a/apps/web/app/assets/instance/maintenance-mode-dark.svg +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/instance/maintenance-mode-light.svg b/apps/web/app/assets/instance/maintenance-mode-light.svg deleted file mode 100644 index 6cd8bab40..000000000 --- a/apps/web/app/assets/instance/maintenance-mode-light.svg +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/logos/gitea-logo.svg b/apps/web/app/assets/logos/gitea-logo.svg deleted file mode 100644 index 43291345d..000000000 --- a/apps/web/app/assets/logos/gitea-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/web/app/assets/logos/github-black.png b/apps/web/app/assets/logos/github-black.png deleted file mode 100644 index 7a7a82474..000000000 Binary files a/apps/web/app/assets/logos/github-black.png and /dev/null differ diff --git a/apps/web/app/assets/logos/github-dark.svg b/apps/web/app/assets/logos/github-dark.svg deleted file mode 100644 index a0cb35c3a..000000000 --- a/apps/web/app/assets/logos/github-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/web/app/assets/logos/github-square.png b/apps/web/app/assets/logos/github-square.png deleted file mode 100644 index 527ba79f2..000000000 Binary files a/apps/web/app/assets/logos/github-square.png and /dev/null differ diff --git a/apps/web/app/assets/logos/github-white.png b/apps/web/app/assets/logos/github-white.png deleted file mode 100644 index dbb2b578c..000000000 Binary files a/apps/web/app/assets/logos/github-white.png and /dev/null differ diff --git a/apps/web/app/assets/logos/gitlab-logo.svg b/apps/web/app/assets/logos/gitlab-logo.svg deleted file mode 100644 index dab4d8b74..000000000 --- a/apps/web/app/assets/logos/gitlab-logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/web/app/assets/logos/google-logo.svg b/apps/web/app/assets/logos/google-logo.svg deleted file mode 100644 index 088288fa3..000000000 --- a/apps/web/app/assets/logos/google-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/web/app/assets/mac-command.svg b/apps/web/app/assets/mac-command.svg deleted file mode 100644 index 5cee45951..000000000 --- a/apps/web/app/assets/mac-command.svg +++ /dev/null @@ -1,2 +0,0 @@ - -mac-command \ No newline at end of file diff --git a/apps/web/app/assets/og-image.png b/apps/web/app/assets/og-image.png deleted file mode 100644 index dbf89c67a..000000000 Binary files a/apps/web/app/assets/og-image.png and /dev/null differ diff --git a/apps/web/app/assets/onboarding/cycles.webp b/apps/web/app/assets/onboarding/cycles.webp deleted file mode 100644 index 278803b6a..000000000 Binary files a/apps/web/app/assets/onboarding/cycles.webp and /dev/null differ diff --git a/apps/web/app/assets/onboarding/issues.webp b/apps/web/app/assets/onboarding/issues.webp deleted file mode 100644 index 5aed9e354..000000000 Binary files a/apps/web/app/assets/onboarding/issues.webp and /dev/null differ diff --git a/apps/web/app/assets/onboarding/modules.webp b/apps/web/app/assets/onboarding/modules.webp deleted file mode 100644 index 974e464e0..000000000 Binary files a/apps/web/app/assets/onboarding/modules.webp and /dev/null differ diff --git a/apps/web/app/assets/onboarding/onboarding-pages.webp b/apps/web/app/assets/onboarding/onboarding-pages.webp deleted file mode 100644 index 536f9eca0..000000000 Binary files a/apps/web/app/assets/onboarding/onboarding-pages.webp and /dev/null differ diff --git a/apps/web/app/assets/onboarding/pages.webp b/apps/web/app/assets/onboarding/pages.webp deleted file mode 100644 index 27004d687..000000000 Binary files a/apps/web/app/assets/onboarding/pages.webp and /dev/null differ diff --git a/apps/web/app/assets/onboarding/views.webp b/apps/web/app/assets/onboarding/views.webp deleted file mode 100644 index 9c958ec8f..000000000 Binary files a/apps/web/app/assets/onboarding/views.webp and /dev/null differ diff --git a/apps/web/app/assets/plane-logos/black-horizontal-with-blue-logo.png b/apps/web/app/assets/plane-logos/black-horizontal-with-blue-logo.png deleted file mode 100644 index c14505a6f..000000000 Binary files a/apps/web/app/assets/plane-logos/black-horizontal-with-blue-logo.png and /dev/null differ diff --git a/apps/web/app/assets/plane-logos/blue-without-text.png b/apps/web/app/assets/plane-logos/blue-without-text.png deleted file mode 100644 index ea94aec79..000000000 Binary files a/apps/web/app/assets/plane-logos/blue-without-text.png and /dev/null differ diff --git a/apps/web/app/assets/plane-logos/white-horizontal-with-blue-logo.png b/apps/web/app/assets/plane-logos/white-horizontal-with-blue-logo.png deleted file mode 100644 index 97560fb9f..000000000 Binary files a/apps/web/app/assets/plane-logos/white-horizontal-with-blue-logo.png and /dev/null differ diff --git a/apps/web/app/assets/plane-logos/white-horizontal.svg b/apps/web/app/assets/plane-logos/white-horizontal.svg deleted file mode 100644 index 13e2dbb9f..000000000 --- a/apps/web/app/assets/plane-logos/white-horizontal.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/plane-takeoff.png b/apps/web/app/assets/plane-takeoff.png deleted file mode 100644 index 417ff8299..000000000 Binary files a/apps/web/app/assets/plane-takeoff.png and /dev/null differ diff --git a/apps/web/app/assets/scribble/scribble-black.svg b/apps/web/app/assets/scribble/scribble-black.svg deleted file mode 100644 index 556b9565e..000000000 --- a/apps/web/app/assets/scribble/scribble-black.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/web/app/assets/scribble/scribble-white.svg b/apps/web/app/assets/scribble/scribble-white.svg deleted file mode 100644 index 213389737..000000000 --- a/apps/web/app/assets/scribble/scribble-white.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/web/app/assets/services/csv.svg b/apps/web/app/assets/services/csv.svg deleted file mode 100644 index adea1ddc9..000000000 --- a/apps/web/app/assets/services/csv.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/apps/web/app/assets/services/excel.svg b/apps/web/app/assets/services/excel.svg deleted file mode 100644 index 86fb8e64b..000000000 --- a/apps/web/app/assets/services/excel.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/apps/web/app/assets/services/github.png b/apps/web/app/assets/services/github.png deleted file mode 100644 index 527ba79f2..000000000 Binary files a/apps/web/app/assets/services/github.png and /dev/null differ diff --git a/apps/web/app/assets/services/jira.svg b/apps/web/app/assets/services/jira.svg deleted file mode 100644 index 5e5cebc84..000000000 --- a/apps/web/app/assets/services/jira.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/apps/web/app/assets/services/json.svg b/apps/web/app/assets/services/json.svg deleted file mode 100644 index 0fe32e276..000000000 --- a/apps/web/app/assets/services/json.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/apps/web/app/assets/services/slack.png b/apps/web/app/assets/services/slack.png deleted file mode 100644 index fc918001f..000000000 Binary files a/apps/web/app/assets/services/slack.png and /dev/null differ diff --git a/apps/web/app/assets/user.png b/apps/web/app/assets/user.png deleted file mode 100644 index bc0251228..000000000 Binary files a/apps/web/app/assets/user.png and /dev/null differ diff --git a/apps/web/app/assets/users/user-1.png b/apps/web/app/assets/users/user-1.png deleted file mode 100644 index b69f539cc..000000000 Binary files a/apps/web/app/assets/users/user-1.png and /dev/null differ diff --git a/apps/web/app/assets/users/user-2.png b/apps/web/app/assets/users/user-2.png deleted file mode 100644 index c5b9bc519..000000000 Binary files a/apps/web/app/assets/users/user-2.png and /dev/null differ diff --git a/apps/web/app/assets/users/user-profile-cover-default-img.png b/apps/web/app/assets/users/user-profile-cover-default-img.png deleted file mode 100644 index 6f555daf4..000000000 Binary files a/apps/web/app/assets/users/user-profile-cover-default-img.png and /dev/null differ diff --git a/apps/web/app/assets/workspace-active-cycles/cta-l-1-dark.webp b/apps/web/app/assets/workspace-active-cycles/cta-l-1-dark.webp deleted file mode 100644 index 901f4b38d..000000000 Binary files a/apps/web/app/assets/workspace-active-cycles/cta-l-1-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/workspace-active-cycles/cta-l-1-light.webp b/apps/web/app/assets/workspace-active-cycles/cta-l-1-light.webp deleted file mode 100644 index 7cce304c1..000000000 Binary files a/apps/web/app/assets/workspace-active-cycles/cta-l-1-light.webp and /dev/null differ diff --git a/apps/web/app/assets/workspace-active-cycles/cta-r-1-dark.webp b/apps/web/app/assets/workspace-active-cycles/cta-r-1-dark.webp deleted file mode 100644 index 3c3a32acf..000000000 Binary files a/apps/web/app/assets/workspace-active-cycles/cta-r-1-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/workspace-active-cycles/cta-r-1-light.webp b/apps/web/app/assets/workspace-active-cycles/cta-r-1-light.webp deleted file mode 100644 index 4911a27b4..000000000 Binary files a/apps/web/app/assets/workspace-active-cycles/cta-r-1-light.webp and /dev/null differ diff --git a/apps/web/app/assets/workspace-active-cycles/cta-r-2-dark.webp b/apps/web/app/assets/workspace-active-cycles/cta-r-2-dark.webp deleted file mode 100644 index 0776cc705..000000000 Binary files a/apps/web/app/assets/workspace-active-cycles/cta-r-2-dark.webp and /dev/null differ diff --git a/apps/web/app/assets/workspace-active-cycles/cta-r-2-light.webp b/apps/web/app/assets/workspace-active-cycles/cta-r-2-light.webp deleted file mode 100644 index 971d28fcb..000000000 Binary files a/apps/web/app/assets/workspace-active-cycles/cta-r-2-light.webp and /dev/null differ diff --git a/apps/web/app/assets/workspace/workspace-creation-disabled.png b/apps/web/app/assets/workspace/workspace-creation-disabled.png deleted file mode 100644 index 09e05ef83..000000000 Binary files a/apps/web/app/assets/workspace/workspace-creation-disabled.png and /dev/null differ diff --git a/apps/web/app/assets/workspace/workspace-not-available.png b/apps/web/app/assets/workspace/workspace-not-available.png deleted file mode 100644 index e95cdf02a..000000000 Binary files a/apps/web/app/assets/workspace/workspace-not-available.png and /dev/null differ diff --git a/apps/web/app/compat/next/helper.ts b/apps/web/app/compat/next/helper.ts deleted file mode 100644 index c4edf3d54..000000000 --- a/apps/web/app/compat/next/helper.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -/** - * Ensures that a URL has a trailing slash while preserving query parameters and fragments - * @param url - The URL to process - * @returns The URL with a trailing slash added to the pathname (if not already present) - */ -export function ensureTrailingSlash(url: string): string { - try { - const fallbackBaseUrl = - typeof window !== "undefined" && window.location.origin ? window.location.origin : "http://dummy.com"; - // Handle relative URLs by creating a URL object with a fallback base URL - const urlObj = new URL(url, fallbackBaseUrl); - - // Don't modify root path - if (urlObj.pathname === "/") { - return url; - } - - // Add trailing slash if it doesn't exist - if (!urlObj.pathname.endsWith("/")) { - urlObj.pathname += "/"; - } - - // For relative URLs, return just the path + search + hash - if (url.startsWith("/")) { - return urlObj.pathname + urlObj.search + urlObj.hash; - } - - // For absolute URLs, return the full URL - return urlObj.toString(); - } catch (error) { - // If URL parsing fails, return the original URL - console.warn("Failed to parse URL for trailing slash enforcement:", url, error); - return url; - } -} diff --git a/apps/web/app/compat/next/image.tsx b/apps/web/app/compat/next/image.tsx deleted file mode 100644 index f229d0d5f..000000000 --- a/apps/web/app/compat/next/image.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; - -// Minimal shim so code using next/image compiles under React Router + Vite -// without changing call sites. It just renders a native img. - -type NextImageProps = React.ImgHTMLAttributes & { - src: string; - fill?: boolean; - priority?: boolean; - quality?: number; - placeholder?: "blur" | "empty"; - blurDataURL?: string; -}; - -function Image({ - src, - alt = "", - fill, - priority: _priority, - quality: _quality, - placeholder: _placeholder, - blurDataURL: _blurDataURL, - ...rest -}: NextImageProps) { - // If fill is true, apply object-fit styles - const style = fill ? { objectFit: "cover" as const, width: "100%", height: "100%" } : rest.style; - - return {alt}; -} - -export default Image; diff --git a/apps/web/app/compat/next/link.tsx b/apps/web/app/compat/next/link.tsx deleted file mode 100644 index 85177560f..000000000 --- a/apps/web/app/compat/next/link.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -import { Link as RRLink } from "react-router"; -import { ensureTrailingSlash } from "./helper"; - -type NextLinkProps = React.ComponentProps<"a"> & { - href: string; - replace?: boolean; - prefetch?: boolean; // next.js prop, ignored - scroll?: boolean; // next.js prop, ignored - shallow?: boolean; // next.js prop, ignored -}; - -function Link({ href, replace, prefetch: _prefetch, scroll: _scroll, shallow: _shallow, ...rest }: NextLinkProps) { - return ; -} - -export default Link; diff --git a/apps/web/app/compat/next/navigation.ts b/apps/web/app/compat/next/navigation.ts deleted file mode 100644 index 3803ffe1f..000000000 --- a/apps/web/app/compat/next/navigation.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useMemo } from "react"; -import { useLocation, useNavigate, useParams as useParamsRR, useSearchParams as useSearchParamsRR } from "react-router"; -import { ensureTrailingSlash } from "./helper"; - -export function useRouter() { - const navigate = useNavigate(); - return useMemo( - () => ({ - push: (to: string) => { - // Defer navigation to avoid state updates during render - setTimeout(() => navigate(ensureTrailingSlash(to)), 0); - }, - replace: (to: string) => { - // Defer navigation to avoid state updates during render - setTimeout(() => navigate(ensureTrailingSlash(to), { replace: true }), 0); - }, - back: () => { - setTimeout(() => navigate(-1), 0); - }, - forward: () => { - setTimeout(() => navigate(1), 0); - }, - refresh: () => { - location.reload(); - }, - prefetch: async (_to: string) => { - // no-op in this shim - }, - }), - [navigate] - ); -} - -export function usePathname(): string { - const { pathname } = useLocation(); - return pathname; -} - -export function useSearchParams(): URLSearchParams { - const [searchParams] = useSearchParamsRR(); - return searchParams; -} - -export function useParams() { - return useParamsRR(); -} diff --git a/apps/web/app/compat/next/script.tsx b/apps/web/app/compat/next/script.tsx deleted file mode 100644 index c2b470b79..000000000 --- a/apps/web/app/compat/next/script.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect } from "react"; - -type ScriptProps = { - src?: string; - id?: string; - strategy?: "beforeInteractive" | "afterInteractive" | "lazyOnload" | "worker"; - onLoad?: () => void; - onError?: () => void; - children?: string; - defer?: boolean; - [key: string]: any; -}; - -// Minimal shim for next/script that creates a script tag -function Script({ src, id, strategy: _strategy, onLoad, onError, children, ...rest }: ScriptProps) { - useEffect(() => { - if (src) { - const script = document.createElement("script"); - if (id) script.id = id; - script.src = src; - if (onLoad) script.onload = onLoad; - if (onError) script.onerror = onError; - Object.keys(rest).forEach((key) => { - script.setAttribute(key, rest[key]); - }); - document.body.appendChild(script); - - return () => { - if (script.parentNode) { - document.body.removeChild(script); - } - }; - } else if (children) { - const script = document.createElement("script"); - if (id) script.id = id; - script.textContent = children; - Object.keys(rest).forEach((key) => { - script.setAttribute(key, rest[key]); - }); - document.body.appendChild(script); - - return () => { - if (script.parentNode) { - document.body.removeChild(script); - } - }; - } - }, [src, id, children, onLoad, onError, rest]); - - return null; -} - -export default Script; diff --git a/apps/web/app/entry.client.tsx b/apps/web/app/entry.client.tsx deleted file mode 100644 index 27b7f4483..000000000 --- a/apps/web/app/entry.client.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { initPromise } from "@plane/i18n"; -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; -import { HydratedRouter } from "react-router/dom"; - -import polyfills from "@/lib/polyfills"; -import { isStaleAssetErrorMessage, recoverFromStaleAsset } from "@/lib/stale-asset-error"; - -void polyfills; - -function reportClientLog(type: "error" | "warn" | "info", message: string, extra?: Record) { - try { - const payload = JSON.stringify({ - type, - message, - url: window.location.href, - stack: extra?.stack || "", - extra: extra || {}, - }); - if (navigator.sendBeacon) { - navigator.sendBeacon("/api/client-logs/", new Blob([payload], { type: "application/json" })); - } else { - fetch("/api/client-logs/", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: payload, - keepalive: true, - }).catch(() => {}); - } - } catch {} -} - -if (typeof window !== "undefined") { - window.addEventListener("error", (event) => { - reportClientLog("error", event.message || "Uncaught error", { - filename: event.filename, - lineno: event.lineno, - colno: event.colno, - stack: event.error?.stack, - }); - }); - - window.addEventListener("unhandledrejection", (event) => { - const reason = event.reason instanceof Error ? event.reason.message : String(event.reason ?? ""); - reportClientLog("error", `Unhandled Promise Rejection: ${reason}`, { - stack: event.reason instanceof Error ? event.reason.stack : undefined, - }); - }); -} - -// Production-only: in dev these errors come from the dev server itself (restarts, -// stale optimized deps) and auto-reloading would mask them. -if (import.meta.env.PROD) { - window.addEventListener("vite:preloadError", (event) => { - if (recoverFromStaleAsset()) event.preventDefault(); - }); - - window.addEventListener("error", (event) => { - if (isStaleAssetErrorMessage(event.message || "")) recoverFromStaleAsset(); - }); - - window.addEventListener("unhandledrejection", (event) => { - const reason = event.reason instanceof Error ? event.reason.message : String(event.reason ?? ""); - if (isStaleAssetErrorMessage(reason)) recoverFromStaleAsset(); - }); -} - -// Initialize i18n before hydrating (the remix-i18next pattern for React -// Router: await init, then hydrateRoot). Hydrating before the instance is -// ready would make the first client render diverge from the prerendered -// shell, and React 19 leaves DOM it could not adopt in place instead of -// clearing it. -void initPromise - .catch(() => {}) - .then(() => { - startTransition(() => { - hydrateRoot( - document, - - - - ); - }); - }); diff --git a/apps/web/app/error/dev.tsx b/apps/web/app/error/dev.tsx deleted file mode 100644 index b9b1d4843..000000000 --- a/apps/web/app/error/dev.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane imports -import { isRouteErrorResponse } from "react-router"; -import { Banner } from "@makeplane/propel/components/banner"; -import { InfoFilled } from "@makeplane/propel/icons"; -import { Button } from "@plane/propel/button"; -import { Card, ECardVariant } from "@plane/propel/card"; - -interface ErrorActionsProps { - onGoHome: () => void; - onReload?: () => void; -} - -function ErrorActions({ onGoHome, onReload }: ErrorActionsProps) { - return ( -
- - {onReload && ( - - )} -
- ); -} - -interface DevErrorComponentProps { - error: unknown; - onGoHome: () => void; - onReload: () => void; -} - -export function DevErrorComponent({ error, onGoHome, onReload }: DevErrorComponentProps) { - if (isRouteErrorResponse(error)) { - return ( -
-
- } title="Route Error Response" /> - - -
-
-

- {error.status} {error.statusText} -

-
-
- -
-

Error Data

-
-

{error.data}

-
-
- - -
- -
-
- ); - } - - if (error instanceof Error) { - return ( -
-
- } title="Runtime Error" /> - -
-
-

Error

-
-
- -
-

Message

-
-

{error.message}

-
-
- - {error.stack && ( -
-

Stack Trace

-
-
-                      {error.stack}
-                    
-
-
- )} - - -
- - - -
- -
-

Development Mode

-

- This detailed error view is only visible in development. In production, users will see a friendly - error page. -

-
-
-
-
-
- ); - } - - return ( -
-
- } title="Unknown Error" /> - - -
-
-

Unknown Error

-
-
- -
-

- An unknown error occurred. Please try refreshing the page or contact support if the problem persists. -

-
- - -
- -
-
- ); -} diff --git a/apps/web/app/error/index.tsx b/apps/web/app/error/index.tsx deleted file mode 100644 index 9c3712b6d..000000000 --- a/apps/web/app/error/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// hooks -import { useAppRouter } from "@/hooks/use-app-router"; -// layouts -import { DevErrorComponent } from "./dev"; -import { ProdErrorComponent } from "./prod"; - -export function CustomErrorComponent({ error }: { error: unknown }) { - // router - const router = useAppRouter(); - - const handleGoHome = () => router.push("/"); - const handleReload = () => window.location.reload(); - - if (import.meta.env.DEV) { - return ; - } - - return ; -} diff --git a/apps/web/app/error/prod.tsx b/apps/web/app/error/prod.tsx deleted file mode 100644 index a8d61829e..000000000 --- a/apps/web/app/error/prod.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useTheme } from "next-themes"; -// plane imports -import { Button } from "@plane/propel/button"; -// assets -import maintenanceModeDarkModeImage from "@/app/assets/instance/maintenance-mode-dark.svg?url"; -import maintenanceModeLightModeImage from "@/app/assets/instance/maintenance-mode-light.svg?url"; -// layouts -import DefaultLayout from "@/layouts/default-layout"; - -const linkMap = [ - { - key: "mail_to", - label: "Contact Support", - value: "mailto:support@plane.so", - }, - { - key: "status", - label: "Status Page", - value: "https://status.plane.so/", - }, - { - key: "twitter_handle", - label: "@planepowers", - value: "https://x.com/planepowers", - }, -]; - -// Production Error Component -interface ProdErrorComponentProps { - onGoHome: () => void; -} - -export function ProdErrorComponent({ onGoHome }: ProdErrorComponentProps) { - // hooks - const { resolvedTheme } = useTheme(); - - // derived values - const maintenanceModeImage = resolvedTheme === "dark" ? maintenanceModeDarkModeImage : maintenanceModeLightModeImage; - - return ( - -
-
- ProjectSettingImg -
-
-
-

🚧 Looks like something went wrong!

- - We track these errors automatically and working on getting things back up and running. If the problem - persists feel free to contact us. In the meantime, try refreshing. - -
- -
- {linkMap.map((link) => ( - - ))} -
- -
- -
-
-
-
- ); -} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx deleted file mode 100644 index ee10bcc24..000000000 --- a/apps/web/app/layout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Outlet } from "react-router"; -// plane imports -import { cn } from "@plane/utils"; -// local -import { AppProvider } from "./provider"; - -// Pathless layout route wrapping every route (see app/routes.ts). Providers, the store -// layer, and app chrome live here instead of root.tsx so they stay out of the SPA-mode -// server build — see the note in app/root.tsx. -export default function AppShellLayout() { - return ( - -
-
- -
-
-
- ); -} diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx deleted file mode 100644 index 13763951c..000000000 --- a/apps/web/app/not-found.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import Link from "next/link"; -// ui -import { Button } from "@plane/propel/button"; -// images -import Image404 from "@/app/assets/404.svg?url"; -// types -import type { Route } from "./+types/not-found"; - -export const meta: Route.MetaFunction = () => [ - { title: "404 - Page Not Found" }, - { name: "robots", content: "noindex, nofollow" }, -]; - -function PageNotFound() { - return ( -
-
-
-
- 404- Page not found -
-
-

Oops! Something went wrong.

-

- Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is - temporarily unavailable. -

-
- - - - - -
-
-
- ); -} - -export default PageNotFound; diff --git a/apps/web/app/provider.tsx b/apps/web/app/provider.tsx deleted file mode 100644 index 016b66c0e..000000000 --- a/apps/web/app/provider.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { lazy, Suspense } from "react"; -import { useTheme } from "next-themes"; -import { SWRConfig } from "swr"; -// Plane Imports -import { WEB_SWR_CONFIG } from "@plane/constants"; -import { TranslationProvider } from "@plane/i18n"; -import { Toast } from "@plane/propel/toast"; -// helpers -import { resolveGeneralTheme } from "@plane/utils"; -// mobx store provider -import { StoreProvider } from "@/lib/store-context"; - -// lazy imports -const AppProgressBar = lazy(function AppProgressBar() { - return import("@/lib/b-progress/AppProgressBar"); -}); - -const StoreWrapper = lazy(function StoreWrapper() { - return import("@/lib/wrappers/store-wrapper"); -}); - -const InstanceWrapper = lazy(function InstanceWrapper() { - return import("@/lib/wrappers/instance-wrapper"); -}); - -export interface IAppProvider { - children: React.ReactNode; -} - -export function AppProvider(props: IAppProvider) { - const { children } = props; - // themes - const { resolvedTheme } = useTheme(); - - return ( - - <> - - - - - - - {children} - - - - - - - ); -} diff --git a/apps/web/app/root.tsx b/apps/web/app/root.tsx deleted file mode 100644 index 6da79924a..000000000 --- a/apps/web/app/root.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { ReactNode } from "react"; -import { Links, Meta, Outlet, Scripts } from "react-router"; -import type { LinksFunction } from "react-router"; -import { ThemeProvider } from "next-themes"; -// plane imports -import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants"; -// types -// assets -import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url"; -import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url"; -import faviconIco from "@/app/assets/favicon/favicon.ico?url"; -import icon180 from "@/app/assets/icons/icon-180x180.png?url"; -import icon512 from "@/app/assets/icons/icon-512x512.png?url"; -import ogImage from "@/app/assets/og-image.png?url"; -import globalStyles from "@/styles/globals.css?url"; -import type { Route } from "./+types/root"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -// lib -import { isStaleAssetError, recoverFromStaleAsset } from "@/lib/stale-asset-error"; -// local -import { CustomErrorComponent } from "./error"; -// fonts -// oxlint-disable-next-line import/no-unassigned-import -import "@fontsource-variable/inter"; -import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url"; -// oxlint-disable-next-line import/no-unassigned-import -import "@fontsource/material-symbols-rounded"; -// oxlint-disable-next-line import/no-unassigned-import -import "@fontsource/ibm-plex-mono"; - -const APP_TITLE = "Plane | Simple, extensible, open-source project management tool."; - -export const links: LinksFunction = () => [ - { rel: "icon", type: "image/png", sizes: "32x32", href: favicon32 }, - { rel: "icon", type: "image/png", sizes: "16x16", href: favicon16 }, - { rel: "shortcut icon", href: faviconIco }, - { rel: "manifest", href: "/site.webmanifest.json" }, - { rel: "apple-touch-icon", href: icon512 }, - { rel: "apple-touch-icon", sizes: "180x180", href: icon180 }, - { rel: "apple-touch-icon", sizes: "512x512", href: icon512 }, - { rel: "manifest", href: "/manifest.json" }, - { rel: "stylesheet", href: globalStyles }, - { - rel: "preload", - href: interVariableWoff2, - as: "font", - type: "font/woff2", - crossOrigin: "anonymous", - }, -]; - -export function Layout({ children }: { children: ReactNode }) { - return ( - - - - - - {/* Meta info for PWA */} - - - - - - - - - - -
-
- - {children} - - - - - ); -} - -export const meta: Route.MetaFunction = () => [ - { title: APP_TITLE }, - { name: "description", content: SITE_DESCRIPTION }, - { property: "og:title", content: APP_TITLE }, - { - property: "og:description", - content: "Open-source project management tool to manage work items, cycles, and product roadmaps easily", - }, - { property: "og:url", content: "https://app.plane.so/" }, - { property: "og:image", content: ogImage }, - { property: "og:image:width", content: "1200" }, - { property: "og:image:height", content: "630" }, - { property: "og:image:alt", content: "Plane - Modern project management" }, - { - name: "keywords", - content: - "software development, plan, ship, software, accelerate, code management, release management, project management, work item tracking, agile, scrum, kanban, collaboration", - }, - { name: "twitter:site", content: "@planepowers" }, - { name: "twitter:card", content: "summary_large_image" }, - { name: "twitter:image", content: ogImage }, - { name: "twitter:image:width", content: "1200" }, - { name: "twitter:image:height", content: "630" }, - { name: "twitter:image:alt", content: "Plane - Modern project management" }, -]; - -// Root stays shell-thin: in SPA mode React Router server-builds only the root route, so -// everything imported here is evaluated in Node just to prerender the fallback index.html. -// Providers, the store layer, and app chrome belong in app/layout.tsx — never import them here. -export default function Root() { - return ; -} - -export function HydrateFallback() { - return ( -
- -
- ); -} - -export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { - // A stale chunk failure surfaces here as React Router's own wrapper error - // (the failed dynamic import itself never reaches a window event) — recover - // the same way entry.client.tsx does instead of just showing the error page. - if (import.meta.env.PROD && isStaleAssetError(error)) recoverFromStaleAsset(); - - return ; -} diff --git a/apps/web/app/routes.ts b/apps/web/app/routes.ts deleted file mode 100644 index 028395813..000000000 --- a/apps/web/app/routes.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { layout, route } from "@react-router/dev/routes"; -import type { RouteConfigEntry } from "@react-router/dev/routes"; -import { coreRoutes } from "./routes/core"; -import { extendedRoutes } from "./routes/extended"; -import { mergeRoutes } from "./routes/helper"; - -/** - * Main Routes Configuration - * This file serves as the entry point for the route configuration. - * - * Every route nests under the pathless app/layout.tsx shell; root.tsx stays - * shell-thin (see the note in app/root.tsx). - */ -const mergedRoutes: RouteConfigEntry[] = mergeRoutes(coreRoutes, extendedRoutes); - -// Add catch-all route at the end (404 handler) -const routes: RouteConfigEntry[] = [layout("./layout.tsx", [...mergedRoutes, route("*", "./not-found.tsx")])]; - -export default routes; diff --git a/apps/web/app/routes/core.ts b/apps/web/app/routes/core.ts deleted file mode 100644 index c9c82bd24..000000000 --- a/apps/web/app/routes/core.ts +++ /dev/null @@ -1,405 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { index, layout, route } from "@react-router/dev/routes"; -import type { RouteConfig, RouteConfigEntry } from "@react-router/dev/routes"; - -export const coreRoutes: RouteConfigEntry[] = [ - // ======================================================================== - // USER MANAGEMENT ROUTES - // ======================================================================== - - // Home - Sign In - layout("./(home)/layout.tsx", [index("./(home)/page.tsx")]), - - // Sign Up - layout("./(all)/sign-up/layout.tsx", [route("sign-up", "./(all)/sign-up/page.tsx")]), - - // Account Routes - Password Management - layout("./(all)/accounts/forgot-password/layout.tsx", [ - route("accounts/forgot-password", "./(all)/accounts/forgot-password/page.tsx"), - ]), - layout("./(all)/accounts/reset-password/layout.tsx", [ - route("accounts/reset-password", "./(all)/accounts/reset-password/page.tsx"), - ]), - layout("./(all)/accounts/set-password/layout.tsx", [ - route("accounts/set-password", "./(all)/accounts/set-password/page.tsx"), - ]), - - // Create Workspace - layout("./(all)/create-workspace/layout.tsx", [route("create-workspace", "./(all)/create-workspace/page.tsx")]), - - // Onboarding - layout("./(all)/onboarding/layout.tsx", [route("onboarding", "./(all)/onboarding/page.tsx")]), - - // Invitations - layout("./(all)/invitations/layout.tsx", [route("invitations", "./(all)/invitations/page.tsx")]), - - // Workspace Invitations - layout("./(all)/workspace-invitations/layout.tsx", [ - route("workspace-invitations", "./(all)/workspace-invitations/page.tsx"), - ]), - - // ======================================================================== - // ALL APP ROUTES - // ======================================================================== - layout("./(all)/layout.tsx", [ - // ====================================================================== - // WORKSPACE-SCOPED ROUTES - // ====================================================================== - layout("./(all)/[workspaceSlug]/layout.tsx", [ - // ==================================================================== - // PROJECTS APP SECTION - WORKSPACE LEVEL ROUTES - // ==================================================================== - layout("./(all)/[workspaceSlug]/(projects)/layout.tsx", [ - // -------------------------------------------------------------------- - // WORKSPACE LEVEL ROUTES - // -------------------------------------------------------------------- - - // Workspace Home - route(":workspaceSlug", "./(all)/[workspaceSlug]/(projects)/page.tsx"), - - // Active Cycles - layout("./(all)/[workspaceSlug]/(projects)/active-cycles/layout.tsx", [ - route(":workspaceSlug/active-cycles", "./(all)/[workspaceSlug]/(projects)/active-cycles/page.tsx"), - ]), - - // Analytics - layout("./(all)/[workspaceSlug]/(projects)/analytics/[tabId]/layout.tsx", [ - route(":workspaceSlug/analytics/:tabId", "./(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx"), - ]), - - // Browse - layout("./(all)/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx", [ - route(":workspaceSlug/browse/:workItem", "./(all)/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx"), - ]), - - // Drafts - layout("./(all)/[workspaceSlug]/(projects)/drafts/layout.tsx", [ - route(":workspaceSlug/drafts", "./(all)/[workspaceSlug]/(projects)/drafts/page.tsx"), - ]), - - // Notifications - layout("./(all)/[workspaceSlug]/(projects)/notifications/layout.tsx", [ - route(":workspaceSlug/notifications", "./(all)/[workspaceSlug]/(projects)/notifications/page.tsx"), - ]), - - // Profile - layout("./(all)/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx", [ - route(":workspaceSlug/profile/:userId", "./(all)/[workspaceSlug]/(projects)/profile/[userId]/page.tsx"), - route( - ":workspaceSlug/profile/:userId/:profileViewId", - "./(all)/[workspaceSlug]/(projects)/profile/[userId]/[profileViewId]/page.tsx" - ), - route( - ":workspaceSlug/profile/:userId/activity", - "./(all)/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx" - ), - ]), - - // Stickies - layout("./(all)/[workspaceSlug]/(projects)/stickies/layout.tsx", [ - route(":workspaceSlug/stickies", "./(all)/[workspaceSlug]/(projects)/stickies/page.tsx"), - ]), - - // Workspace Views - layout("./(all)/[workspaceSlug]/(projects)/workspace-views/layout.tsx", [ - route(":workspaceSlug/workspace-views", "./(all)/[workspaceSlug]/(projects)/workspace-views/page.tsx"), - route( - ":workspaceSlug/workspace-views/:globalViewId", - "./(all)/[workspaceSlug]/(projects)/workspace-views/[globalViewId]/page.tsx" - ), - ]), - - // Archived Projects - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx", [ - route( - ":workspaceSlug/projects/archives", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx" - ), - ]), - - // -------------------------------------------------------------------- - // PROJECT LEVEL ROUTES - // -------------------------------------------------------------------- - - // Project List - layout("./(all)/[workspaceSlug]/(projects)/projects/(list)/layout.tsx", [ - route(":workspaceSlug/projects", "./(all)/[workspaceSlug]/(projects)/projects/(list)/page.tsx"), - ]), - - // Project Detail - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx", [ - // Project Issues List - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/issues", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx" - ), - ]), - // Issue Detail - route( - ":workspaceSlug/projects/:projectId/issues/:issueId", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx" - ), - - // Cycle Detail - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/cycles/:cycleId", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx" - ), - ]), - - // Cycles List - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/cycles", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx" - ), - ]), - - // Module Detail - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/modules/:moduleId", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx" - ), - ]), - - // Modules List - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/modules", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx" - ), - ]), - - // View Detail - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/views/:viewId", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx" - ), - ]), - - // Views List - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/views", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx" - ), - ]), - - // Page Detail - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/pages/:pageId", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx" - ), - ]), - - // Pages List - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/pages", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx" - ), - ]), - // Intake list - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/intake", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx" - ), - ]), - ]), - - // Project Archives - Issues, Cycles, Modules - // Project Archives - Issues - List - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/archives/issues", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx" - ), - ]), - - // Project Archives - Issues - Detail - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/archives/issues/:archivedIssueId", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx" - ), - ]), - - // Project Archives - Cycles - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/archives/cycles", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx" - ), - ]), - - // Project Archives - Modules - layout("./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx", [ - route( - ":workspaceSlug/projects/:projectId/archives/modules", - "./(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx" - ), - ]), - ]), - - // ==================================================================== - // SETTINGS SECTION - // ==================================================================== - layout("./(all)/[workspaceSlug]/(settings)/layout.tsx", [ - // -------------------------------------------------------------------- - // WORKSPACE SETTINGS - // -------------------------------------------------------------------- - - layout("./(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx", [ - route(":workspaceSlug/settings", "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx"), - route( - ":workspaceSlug/settings/members", - "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx" - ), - route( - ":workspaceSlug/settings/billing", - "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx" - ), - route( - ":workspaceSlug/settings/exports", - "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx" - ), - route( - ":workspaceSlug/settings/webhooks", - "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx" - ), - route( - ":workspaceSlug/settings/webhooks/:webhookId", - "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx" - ), - ]), - - // -------------------------------------------------------------------- - // PROJECT SETTINGS - // -------------------------------------------------------------------- - - layout("./(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx", [ - // No Projects available page - route(":workspaceSlug/settings/projects", "./(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx"), - layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx", [ - // Project Settings - route( - ":workspaceSlug/settings/projects/:projectId", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx" - ), - // Project Members - route( - ":workspaceSlug/settings/projects/:projectId/members", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx" - ), - // Project Features - route( - ":workspaceSlug/settings/projects/:projectId/features/cycles", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/page.tsx" - ), - route( - ":workspaceSlug/settings/projects/:projectId/features/modules", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/page.tsx" - ), - route( - ":workspaceSlug/settings/projects/:projectId/features/views", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/page.tsx" - ), - route( - ":workspaceSlug/settings/projects/:projectId/features/pages", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/page.tsx" - ), - route( - ":workspaceSlug/settings/projects/:projectId/features/intake", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/page.tsx" - ), - // Project States - route( - ":workspaceSlug/settings/projects/:projectId/states", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx" - ), - // Project Labels - route( - ":workspaceSlug/settings/projects/:projectId/labels", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx" - ), - // Project Estimates - route( - ":workspaceSlug/settings/projects/:projectId/estimates", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx" - ), - // Project Automations - layout("./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/layout.tsx", [ - route( - ":workspaceSlug/settings/projects/:projectId/automations", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx" - ), - ]), - ]), - ]), - ]), - ]), - // ====================================================================== - // STANDALONE ROUTES (outside workspace context) - // ====================================================================== - - // -------------------------------------------------------------------- - // PROFILE SETTINGS - // -------------------------------------------------------------------- - - layout("./(all)/settings/profile/layout.tsx", [ - route("settings/profile/:profileTabId", "./(all)/settings/profile/[profileTabId]/page.tsx"), - ]), - ]), - - // ======================================================================== - // REDIRECT ROUTES - // ======================================================================== - // Legacy URL redirects for backward compatibility - - // -------------------------------------------------------------------- - // REDIRECT ROUTES - // -------------------------------------------------------------------- - - // Project settings redirect: /:workspaceSlug/projects/:projectId/settings/:path* - // → /:workspaceSlug/settings/projects/:projectId/:path* - route(":workspaceSlug/projects/:projectId/settings/*", "routes/redirects/core/project-settings.tsx"), - - // Analytics redirect: /:workspaceSlug/analytics → /:workspaceSlug/analytics/overview - route(":workspaceSlug/analytics", "routes/redirects/core/analytics.tsx"), - - // API tokens redirect: /:workspaceSlug/settings/api-tokens - // → /settings/profile/api-tokens - route(":workspaceSlug/settings/api-tokens", "routes/redirects/core/api-tokens.tsx"), - - // Inbox redirect: /:workspaceSlug/projects/:projectId/inbox - // → /:workspaceSlug/projects/:projectId/intake - route(":workspaceSlug/projects/:projectId/inbox", "routes/redirects/core/inbox.tsx"), - - // Sign-up redirects - route("accounts/sign-up", "routes/redirects/core/accounts-signup.tsx"), - - // Sign-in redirects (all redirect to home page) - route("sign-in", "routes/redirects/core/sign-in.tsx"), - route("signin", "routes/redirects/core/signin.tsx"), - route("login", "routes/redirects/core/login.tsx"), - - // Register redirect - route("register", "routes/redirects/core/register.tsx"), - - // Profile settings redirects - route("profile/*", "routes/redirects/core/profile-settings.tsx"), - - // Account settings redirects - route(":workspaceSlug/settings/account/*", "routes/redirects/core/workspace-account-settings.tsx"), -] satisfies RouteConfig; diff --git a/apps/web/app/routes/extended.ts b/apps/web/app/routes/extended.ts deleted file mode 100644 index 452d214ad..000000000 --- a/apps/web/app/routes/extended.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { RouteConfigEntry } from "@react-router/dev/routes"; - -export const extendedRoutes: RouteConfigEntry[] = []; diff --git a/apps/web/app/routes/helper.ts b/apps/web/app/routes/helper.ts deleted file mode 100644 index 74dcab50c..000000000 --- a/apps/web/app/routes/helper.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { RouteConfigEntry } from "@react-router/dev/routes"; - -/** - * Merges two route configurations intelligently. - * - Deep merges children when the same layout file exists in both arrays - * - Deduplicates routes by file property, preferring extended over core - * - Maintains order: core routes first, then extended routes at each level - */ -export function mergeRoutes(core: RouteConfigEntry[], extended: RouteConfigEntry[]): RouteConfigEntry[] { - // Step 1: Create a Map to track routes by file path - const routeMap = new Map(); - - // Step 2: Process core routes first - for (const coreRoute of core) { - const fileKey = coreRoute.file; - routeMap.set(fileKey, coreRoute); - } - - // Step 3: Process extended routes - for (const extendedRoute of extended) { - const fileKey = extendedRoute.file; - - if (routeMap.has(fileKey)) { - // Route exists in both - need to merge - const coreRoute = routeMap.get(fileKey)!; - - // Check if both have children (layouts that need deep merging) - if (coreRoute.children && extendedRoute.children) { - // Deep merge: recursively merge children - const mergedChildren = mergeRoutes( - Array.isArray(coreRoute.children) ? coreRoute.children : [], - Array.isArray(extendedRoute.children) ? extendedRoute.children : [] - ); - routeMap.set(fileKey, { - ...extendedRoute, - children: mergedChildren, - }); - } else { - // No children or only one has children - prefer extended - routeMap.set(fileKey, extendedRoute); - } - } else { - // Route only exists in extended - routeMap.set(fileKey, extendedRoute); - } - } - - // Step 4: Build final array maintaining order (core first, then extended-only) - const result: RouteConfigEntry[] = []; - - // Add all core routes (now merged or original) - for (const coreRoute of core) { - const fileKey = coreRoute.file; - if (routeMap.has(fileKey)) { - result.push(routeMap.get(fileKey)!); - routeMap.delete(fileKey); // Remove so we don't add it again - } - } - - // Add remaining extended-only routes - for (const extendedRoute of extended) { - const fileKey = extendedRoute.file; - if (routeMap.has(fileKey)) { - result.push(routeMap.get(fileKey)!); - routeMap.delete(fileKey); - } - } - - return result; -} diff --git a/apps/web/app/routes/redirects/core/accounts-signup.tsx b/apps/web/app/routes/redirects/core/accounts-signup.tsx deleted file mode 100644 index be1ca73ab..000000000 --- a/apps/web/app/routes/redirects/core/accounts-signup.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; - -export const clientLoader = () => { - throw redirect("/sign-up/"); -}; - -export default function AccountsSignup() { - return null; -} diff --git a/apps/web/app/routes/redirects/core/analytics.tsx b/apps/web/app/routes/redirects/core/analytics.tsx deleted file mode 100644 index 3cc7c81ef..000000000 --- a/apps/web/app/routes/redirects/core/analytics.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; -import type { Route } from "./+types/analytics"; - -export const clientLoader = ({ params }: Route.ClientLoaderArgs) => { - const { workspaceSlug } = params; - throw redirect(`/${workspaceSlug}/analytics/overview/`); -}; - -export default function Analytics() { - return null; -} diff --git a/apps/web/app/routes/redirects/core/api-tokens.tsx b/apps/web/app/routes/redirects/core/api-tokens.tsx deleted file mode 100644 index 9d070a167..000000000 --- a/apps/web/app/routes/redirects/core/api-tokens.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; - -export const clientLoader = () => { - throw redirect(`/settings/profile/api-tokens/`); -}; - -export default function ApiTokens() { - return null; -} diff --git a/apps/web/app/routes/redirects/core/inbox.tsx b/apps/web/app/routes/redirects/core/inbox.tsx deleted file mode 100644 index 2d71229a9..000000000 --- a/apps/web/app/routes/redirects/core/inbox.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; -import type { Route } from "./+types/inbox"; - -export const clientLoader = ({ params }: Route.ClientLoaderArgs) => { - const { workspaceSlug, projectId } = params; - throw redirect(`/${workspaceSlug}/projects/${projectId}/intake/`); -}; - -export default function Inbox() { - return null; -} diff --git a/apps/web/app/routes/redirects/core/index.ts b/apps/web/app/routes/redirects/core/index.ts deleted file mode 100644 index e1eaeca46..000000000 --- a/apps/web/app/routes/redirects/core/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { route } from "@react-router/dev/routes"; -import type { RouteConfigEntry } from "@react-router/dev/routes"; - -export const coreRedirectRoutes: RouteConfigEntry[] = [ - // ======================================================================== - // WORKSPACE & PROJECT REDIRECTS - // ======================================================================== - - // Project settings redirect: /:workspaceSlug/projects/:projectId/settings/:path* - // → /:workspaceSlug/settings/projects/:projectId/:path* - route(":workspaceSlug/projects/:projectId/settings/*", "routes/redirects/core/project-settings.tsx"), - - // Analytics redirect: /:workspaceSlug/analytics → /:workspaceSlug/analytics/overview - route(":workspaceSlug/analytics", "routes/redirects/core/analytics.tsx"), - - // API tokens redirect: /:workspaceSlug/settings/api-tokens - // → /settings/profile/api-tokens - route(":workspaceSlug/settings/api-tokens", "routes/redirects/core/api-tokens.tsx"), - - // Inbox redirect: /:workspaceSlug/projects/:projectId/inbox - // → /:workspaceSlug/projects/:projectId/intake - route(":workspaceSlug/projects/:projectId/inbox", "routes/redirects/core/inbox.tsx"), - - // ======================================================================== - // AUTHENTICATION REDIRECTS - // ======================================================================== - - // Sign-up redirects - route("accounts/sign-up", "routes/redirects/core/accounts-signup.tsx"), - - // Sign-in redirects (all redirect to home page) - route("sign-in", "routes/redirects/core/sign-in.tsx"), - route("signin", "routes/redirects/core/signin.tsx"), - route("login", "routes/redirects/core/login.tsx"), - - // Register redirect - route("register", "routes/redirects/core/register.tsx"), -]; diff --git a/apps/web/app/routes/redirects/core/login.tsx b/apps/web/app/routes/redirects/core/login.tsx deleted file mode 100644 index d591a33fd..000000000 --- a/apps/web/app/routes/redirects/core/login.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; - -export const clientLoader = () => { - throw redirect("/"); -}; - -export default function Login() { - return null; -} diff --git a/apps/web/app/routes/redirects/core/profile-settings.tsx b/apps/web/app/routes/redirects/core/profile-settings.tsx deleted file mode 100644 index 82362df47..000000000 --- a/apps/web/app/routes/redirects/core/profile-settings.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; -import type { Route } from "./+types/profile-settings"; - -export const clientLoader = ({ params, request }: Route.ClientLoaderArgs) => { - const searchParams = new URL(request.url).searchParams; - const splat = params["*"] || ""; - throw redirect(`/settings/profile/${splat || "general"}?${searchParams.toString()}`); -}; - -export default function ProfileSettings() { - return null; -} diff --git a/apps/web/app/routes/redirects/core/project-settings.tsx b/apps/web/app/routes/redirects/core/project-settings.tsx deleted file mode 100644 index db74d57cc..000000000 --- a/apps/web/app/routes/redirects/core/project-settings.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; -import type { Route } from "./+types/project-settings"; - -export const clientLoader = ({ params }: Route.ClientLoaderArgs) => { - const { workspaceSlug, projectId } = params; - const splat = params["*"] || ""; - const destination = `/${workspaceSlug}/settings/projects/${projectId}${splat ? `/${splat}` : ""}/`; - throw redirect(destination); -}; - -export default function ProjectSettings() { - return null; -} diff --git a/apps/web/app/routes/redirects/core/register.tsx b/apps/web/app/routes/redirects/core/register.tsx deleted file mode 100644 index bdcb453f9..000000000 --- a/apps/web/app/routes/redirects/core/register.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; - -export const clientLoader = () => { - throw redirect("/sign-up/"); -}; - -export default function Register() { - return null; -} diff --git a/apps/web/app/routes/redirects/core/sign-in.tsx b/apps/web/app/routes/redirects/core/sign-in.tsx deleted file mode 100644 index 5016e254e..000000000 --- a/apps/web/app/routes/redirects/core/sign-in.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; - -export const clientLoader = () => { - throw redirect("/"); -}; - -export default function SignIn() { - return null; -} diff --git a/apps/web/app/routes/redirects/core/signin.tsx b/apps/web/app/routes/redirects/core/signin.tsx deleted file mode 100644 index f557230de..000000000 --- a/apps/web/app/routes/redirects/core/signin.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; - -export const clientLoader = () => { - throw redirect("/"); -}; - -export default function Signin() { - return null; -} diff --git a/apps/web/app/routes/redirects/core/workspace-account-settings.tsx b/apps/web/app/routes/redirects/core/workspace-account-settings.tsx deleted file mode 100644 index 2e44a5bda..000000000 --- a/apps/web/app/routes/redirects/core/workspace-account-settings.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { redirect } from "react-router"; -import type { Route } from "./+types/workspace-account-settings"; - -export const clientLoader = ({ params, request }: Route.ClientLoaderArgs) => { - const searchParams = new URL(request.url).searchParams; - const splat = params["*"] || ""; - throw redirect(`/settings/profile/${splat || "general"}?${searchParams.toString()}`); -}; - -export default function WorkspaceAccountSettings() { - return null; -} diff --git a/apps/web/app/routes/redirects/extended/index.ts b/apps/web/app/routes/redirects/extended/index.ts deleted file mode 100644 index 7b4da929d..000000000 --- a/apps/web/app/routes/redirects/extended/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { RouteConfigEntry } from "@react-router/dev/routes"; - -export const extendedRedirectRoutes: RouteConfigEntry[] = []; diff --git a/apps/web/app/routes/redirects/index.ts b/apps/web/app/routes/redirects/index.ts deleted file mode 100644 index b736b22f4..000000000 --- a/apps/web/app/routes/redirects/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { RouteConfigEntry } from "@react-router/dev/routes"; -import { coreRedirectRoutes } from "./core"; -import { extendedRedirectRoutes } from "./extended"; - -/** - * REDIRECT ROUTES - * Centralized configuration for all route redirects - * Migrated from Next.js next.config.js redirects - */ -export const redirectRoutes: RouteConfigEntry[] = [...coreRedirectRoutes, ...extendedRedirectRoutes]; diff --git a/apps/web/app/types/next-link.d.ts b/apps/web/app/types/next-link.d.ts deleted file mode 100644 index c724e3aec..000000000 --- a/apps/web/app/types/next-link.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare module "next/link" { - type Props = React.ComponentProps<"a"> & { - href: string; - replace?: boolean; - prefetch?: boolean; - scroll?: boolean; - shallow?: boolean; - }; - - const Link: React.FC; - export default Link; -} diff --git a/apps/web/app/types/next-navigation.d.ts b/apps/web/app/types/next-navigation.d.ts deleted file mode 100644 index 67a80c4fa..000000000 --- a/apps/web/app/types/next-navigation.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module "next/navigation" { - export function useRouter(): { - push: (url: string) => void; - replace: (url: string) => void; - back: () => void; - forward: () => void; - refresh: () => void; - prefetch: (url: string) => Promise; - }; - - export function usePathname(): string; - export function useSearchParams(): URLSearchParams; - export function useParams>(): T; -} diff --git a/apps/web/app/types/next-script.d.ts b/apps/web/app/types/next-script.d.ts deleted file mode 100644 index 299b5ed4c..000000000 --- a/apps/web/app/types/next-script.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare module "next/script" { - type ScriptProps = { - src?: string; - id?: string; - strategy?: "beforeInteractive" | "afterInteractive" | "lazyOnload" | "worker"; - onLoad?: () => void; - onError?: () => void; - children?: string; - defer?: boolean; - [key: string]: any; - }; - - const Script: React.FC; - export default Script; -} diff --git a/apps/web/app/types/react-router-virtual.d.ts b/apps/web/app/types/react-router-virtual.d.ts deleted file mode 100644 index abf3b638e..000000000 --- a/apps/web/app/types/react-router-virtual.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module "virtual:react-router/server-build" { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const build: any; - export default build; -} diff --git a/apps/web/caddy/Caddyfile b/apps/web/caddy/Caddyfile deleted file mode 100644 index a687b6315..000000000 --- a/apps/web/caddy/Caddyfile +++ /dev/null @@ -1,40 +0,0 @@ -{ - servers { - trusted_proxies static private_ranges - } - - # order rate_limit before file_server -} - -:3000 { - root * /usr/share/caddy/html - - # Per-client-IP rate limiting for static asset requests - # rate_limit { - # zone static { - # key {client_ip} - # events 3000 - # window 1m - # } - # } - - # Security headers (HSTS is set at the TLS terminator) - header { - X-Frame-Options "DENY" - X-Content-Type-Options "nosniff" - X-XSS-Protection "1; mode=block" - } - - # SPA fallback - try_files {path} /index.html - file_server - - # Access logs - log { - output file /var/log/caddy/web-access.log { - roll_size 20mb - roll_keep 10 - } - format json - } -} diff --git a/apps/web/core/components/account/auth-forms/auth-header.tsx b/apps/web/core/components/account/auth-forms/auth-header.tsx deleted file mode 100644 index 239d65829..000000000 --- a/apps/web/core/components/account/auth-forms/auth-header.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import useSWR from "swr"; -import { useTranslation } from "@plane/i18n"; -import type { IWorkspaceMemberInvitation } from "@plane/types"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { WorkspaceLogo } from "@/components/workspace/logo"; -// helpers -import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper"; -// services -import { WorkspaceService } from "@/services/workspace.service"; - -type TAuthHeader = { - workspaceSlug: string | undefined; - invitationId: string | undefined; - invitationEmail: string | undefined; - authMode: EAuthModes; - currentAuthStep: EAuthSteps; -}; - -const Titles = { - [EAuthModes.SIGN_IN]: { - [EAuthSteps.EMAIL]: { - header: "Work in all dimensions.", - subHeader: "Welcome back to Plane.", - }, - [EAuthSteps.PASSWORD]: { - header: "Work in all dimensions.", - subHeader: "Welcome back to Plane.", - }, - [EAuthSteps.UNIQUE_CODE]: { - header: "Work in all dimensions.", - subHeader: "Welcome back to Plane.", - }, - }, - [EAuthModes.SIGN_UP]: { - [EAuthSteps.EMAIL]: { - header: "Work in all dimensions.", - subHeader: "Create your Plane account.", - }, - [EAuthSteps.PASSWORD]: { - header: "Work in all dimensions.", - subHeader: "Create your Plane account.", - }, - [EAuthSteps.UNIQUE_CODE]: { - header: "Work in all dimensions.", - subHeader: "Create your Plane account.", - }, - }, -}; - -const workSpaceService = new WorkspaceService(); - -export const AuthHeader = observer(function AuthHeader(props: TAuthHeader) { - const { workspaceSlug, invitationId, invitationEmail, authMode, currentAuthStep } = props; - // plane imports - const { t } = useTranslation(); - - const { data: invitation, isLoading } = useSWR( - workspaceSlug && invitationId ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null, - async () => workspaceSlug && invitationId && workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId), - { - revalidateOnFocus: false, - shouldRetryOnError: false, - } - ); - - const getHeaderSubHeader = ( - step: EAuthSteps, - mode: EAuthModes, - invitation: IWorkspaceMemberInvitation | undefined, - email: string | undefined - ) => { - if (invitation && email && invitation.email === email && invitation.workspace) { - const workspace = invitation.workspace; - return { - header: ( -
- {t("common.join")}{" "} - {" "} - {workspace.name} -
- ), - subHeader: - mode == EAuthModes.SIGN_UP - ? "Create an account to start managing work with your team." - : "Log in to start managing work with your team.", - }; - } - - return Titles[mode][step]; - }; - - const { header, subHeader } = getHeaderSubHeader(currentAuthStep, authMode, invitation || undefined, invitationEmail); - - if (isLoading) - return ( -
- -
- ); - - return ; -}); - -type TAuthHeaderBase = { - header: React.ReactNode; - subHeader: string; -}; - -export function AuthHeaderBase(props: TAuthHeaderBase) { - return ( -
- {props.header} - {props.subHeader} -
- ); -} diff --git a/apps/web/core/components/account/auth-forms/auth-root.tsx b/apps/web/core/components/account/auth-forms/auth-root.tsx deleted file mode 100644 index 46ae2c461..000000000 --- a/apps/web/core/components/account/auth-forms/auth-root.tsx +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -// plane imports -import { Banner } from "@makeplane/propel/components/banner"; -import { OAuthOptions } from "@plane/ui"; -// helpers -import type { TAuthErrorInfo } from "@/helpers/authentication.helper"; -import { - EAuthModes, - EAuthSteps, - EAuthenticationErrorCodes, - EErrorAlertType, - authErrorHandler, -} from "@/helpers/authentication.helper"; -// hooks -import { useOAuthConfig } from "@/hooks/oauth"; -import { useInstance } from "@/hooks/store/use-instance"; -// local imports -import { TermsAndConditions } from "../terms-and-conditions"; -import { AuthHeader, AuthHeaderBase } from "./auth-header"; -import { AuthFormRoot } from "./form-root"; - -type TAuthRoot = { - authMode: EAuthModes; -}; - -export const AuthRoot = observer(function AuthRoot(props: TAuthRoot) { - //router - const searchParams = useSearchParams(); - // query params - const emailParam = searchParams.get("email"); - const invitation_id = searchParams.get("invitation_id"); - const workspaceSlug = searchParams.get("slug"); - const error_code = searchParams.get("error_code"); - // props - const { authMode: currentAuthMode } = props; - // states - const [authMode, setAuthMode] = useState(undefined); - const [authStep, setAuthStep] = useState(EAuthSteps.EMAIL); - const [email, setEmail] = useState(emailParam ? emailParam.toString() : ""); - const [errorInfo, setErrorInfo] = useState(undefined); - // store hooks - const { config } = useInstance(); - // derived values - const oAuthActionText = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in"; - const { isOAuthEnabled, oAuthOptions } = useOAuthConfig(oAuthActionText); - const isEmailBasedAuthEnabled = config?.is_email_password_enabled || config?.is_magic_login_enabled; - const noAuthMethodsAvailable = !isOAuthEnabled && !isEmailBasedAuthEnabled; - - useEffect(() => { - if (!authMode && currentAuthMode) setAuthMode(currentAuthMode); - }, [currentAuthMode, authMode]); - - useEffect(() => { - if (error_code && authMode) { - const errorhandler = authErrorHandler(error_code?.toString() as EAuthenticationErrorCodes); - if (errorhandler) { - // password error handler - if ([EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP].includes(errorhandler.code)) { - setAuthMode(EAuthModes.SIGN_UP); - setAuthStep(EAuthSteps.PASSWORD); - } - if ([EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN].includes(errorhandler.code)) { - setAuthMode(EAuthModes.SIGN_IN); - setAuthStep(EAuthSteps.PASSWORD); - } - // magic_code error handler - if ( - [ - EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_UP, - EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP, - EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP, - EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP, - ].includes(errorhandler.code) - ) { - setAuthMode(EAuthModes.SIGN_UP); - setAuthStep(EAuthSteps.UNIQUE_CODE); - } - if ( - [ - EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_IN, - EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN, - EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN, - EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN, - ].includes(errorhandler.code) - ) { - setAuthMode(EAuthModes.SIGN_IN); - setAuthStep(EAuthSteps.UNIQUE_CODE); - } - - setErrorInfo(errorhandler); - } - } - }, [error_code, authMode]); - - if (!authMode) return <>; - - if (noAuthMethodsAvailable) { - return ( - - - - ); - } - - return ( - - {errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && ( - setErrorInfo(undefined)} - /> - )} - - {isOAuthEnabled && ( - - )} - {isEmailBasedAuthEnabled && ( - setEmail(email)} - setAuthMode={(authMode) => setAuthMode(authMode)} - setAuthStep={(authStep) => setAuthStep(authStep)} - setErrorInfo={(errorInfo) => setErrorInfo(errorInfo)} - currentAuthMode={currentAuthMode} - /> - )} - - - ); -}); - -function AuthContainer({ children }: { children: React.ReactNode }) { - return ( -
-
{children}
-
- ); -} diff --git a/apps/web/core/components/account/auth-forms/common/container.tsx b/apps/web/core/components/account/auth-forms/common/container.tsx deleted file mode 100644 index ecf13f9dd..000000000 --- a/apps/web/core/components/account/auth-forms/common/container.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export function FormContainer({ children }: { children: React.ReactNode }) { - return ( -
-
{children}
-
- ); -} diff --git a/apps/web/core/components/account/auth-forms/common/header.tsx b/apps/web/core/components/account/auth-forms/common/header.tsx deleted file mode 100644 index 4639634df..000000000 --- a/apps/web/core/components/account/auth-forms/common/header.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export function AuthFormHeader({ title, description }: { title: string; description: string }) { - return ( -
- {title} - {description} -
- ); -} diff --git a/apps/web/core/components/account/auth-forms/email.tsx b/apps/web/core/components/account/auth-forms/email.tsx deleted file mode 100644 index 0fb17c2d8..000000000 --- a/apps/web/core/components/account/auth-forms/email.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { FormEvent } from "react"; -import { useMemo, useRef, useState } from "react"; -import { observer } from "mobx-react"; -// icons -import { CloseCircleOutline, WarningCircleOutline } from "@makeplane/propel/icons"; -// plane imports -import { Field } from "@makeplane/propel/components/field"; -import { Input, InputGroup } from "@makeplane/propel/components/input"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import type { IEmailCheckData } from "@plane/types"; -import { Spinner } from "@plane/ui"; -import { checkEmailValidity } from "@plane/utils"; -// helpers -type TAuthEmailForm = { - defaultEmail: string; - onSubmit: (data: IEmailCheckData) => Promise; -}; - -export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailForm) { - const { onSubmit, defaultEmail } = props; - // states - const [isSubmitting, setIsSubmitting] = useState(false); - const [email, setEmail] = useState(defaultEmail); - // plane hooks - const { t } = useTranslation(); - const emailError = useMemo( - () => (email && !checkEmailValidity(email) ? { email: "auth.common.email.errors.invalid" } : undefined), - [email] - ); - - const handleFormSubmit = async (event: FormEvent) => { - event.preventDefault(); - setIsSubmitting(true); - const payload: IEmailCheckData = { - email: email, - }; - await onSubmit(payload); - setIsSubmitting(false); - }; - - const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting; - - const [isFocused, setIsFocused] = useState(true); - const inputRef = useRef(null); - - return ( -
-
- - - { - setIsFocused(true); - }} - onBlur={() => { - setIsFocused(false); - }} - > - setEmail(e.target.value)} - placeholder={t("auth.common.email.placeholder")} - autoComplete="off" - autoFocus - ref={inputRef} - /> - {email.length > 0 && ( - - )} - - - {emailError?.email && !isFocused && ( -

- - {t(emailError.email)} -

- )} -
- -
- ); -}); diff --git a/apps/web/core/components/account/auth-forms/forgot-password-popover.tsx b/apps/web/core/components/account/auth-forms/forgot-password-popover.tsx deleted file mode 100644 index 865e0342e..000000000 --- a/apps/web/core/components/account/auth-forms/forgot-password-popover.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Fragment, useState } from "react"; -import { usePopper } from "react-popper"; -import { Popover } from "@headlessui/react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -import { CloseOutline } from "@makeplane/propel/icons"; - -export function ForgotPasswordPopover() { - // popper-js refs - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - // popper-js init - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: "right-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - // plane hooks - const { t } = useTranslation(); - - return ( - - - - - - {({ close }) => ( -
- 🤥 -

{t("auth.forgot_password.errors.smtp_not_enabled")}

- -
- )} -
-
- ); -} diff --git a/apps/web/core/components/account/auth-forms/forgot-password.tsx b/apps/web/core/components/account/auth-forms/forgot-password.tsx deleted file mode 100644 index 71ad47169..000000000 --- a/apps/web/core/components/account/auth-forms/forgot-password.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import Link from "next/link"; -import { useSearchParams } from "next/navigation"; -import { Controller, useForm } from "react-hook-form"; -// icons -import { TickCircleOutline } from "@makeplane/propel/icons"; -// plane imports -import { Field } from "@makeplane/propel/components/field"; -import { Input, InputGroup } from "@makeplane/propel/components/input"; -import { useTranslation } from "@plane/i18n"; -import { Button, getButtonStyling } from "@plane/propel/button"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; - -import { cn, checkEmailValidity } from "@plane/utils"; -// hooks -import useTimer from "@/hooks/use-timer"; -// services -import { AuthService } from "@/services/auth.service"; -// local components -import { FormContainer } from "./common/container"; -import { AuthFormHeader } from "./common/header"; - -type TForgotPasswordFormValues = { - email: string; -}; - -const defaultValues: TForgotPasswordFormValues = { - email: "", -}; - -// services -const authService = new AuthService(); - -export const ForgotPasswordForm = observer(function ForgotPasswordForm() { - // search params - const searchParams = useSearchParams(); - const email = searchParams.get("email"); - // plane hooks - const { t } = useTranslation(); - // timer - const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0); - - // form info - const { - control, - formState: { errors, isSubmitting, isValid }, - handleSubmit, - } = useForm({ - defaultValues: { - ...defaultValues, - email: email?.toString() ?? "", - }, - }); - - const handleForgotPassword = async (formData: TForgotPasswordFormValues) => { - await authService - .sendResetPasswordLink({ - email: formData.email, - }) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: t("auth.forgot_password.toast.success.title"), - message: t("auth.forgot_password.toast.success.message"), - }); - setResendCodeTimer(30); - }) - .catch((err) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: t("auth.forgot_password.toast.error.title"), - message: err?.error ?? t("auth.forgot_password.toast.error.message"), - }); - }); - }; - - return ( - - -
-
- - checkEmailValidity(value) || t("auth.common.email.errors.invalid"), - }} - render={({ field: { value, onChange, ref } }) => ( - - - 0} - /> - - - )} - /> - {resendTimerCode > 0 && ( -

- - {t("auth.forgot_password.email_sent")} -

- )} -
- - - {t("auth.common.back_to_sign_in")} - -
-
- ); -}); diff --git a/apps/web/core/components/account/auth-forms/form-root.tsx b/apps/web/core/components/account/auth-forms/form-root.tsx deleted file mode 100644 index ab3aa8340..000000000 --- a/apps/web/core/components/account/auth-forms/form-root.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useState } from "react"; -import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -import { EAuthModes, EAuthSteps } from "@plane/constants"; -import type { IEmailCheckData } from "@plane/types"; -// helpers -import type { TAuthErrorInfo } from "@/helpers/authentication.helper"; -import { authErrorHandler } from "@/helpers/authentication.helper"; -// hooks -import { useInstance } from "@/hooks/store/use-instance"; -import { useAppRouter } from "@/hooks/use-app-router"; -// services -import { AuthService } from "@/services/auth.service"; -// local components -import { AuthEmailForm } from "./email"; -import { AuthPasswordForm } from "./password"; -import { AuthUniqueCodeForm } from "./unique-code"; - -type TAuthFormRoot = { - authStep: EAuthSteps; - authMode: EAuthModes; - email: string; - setEmail: (email: string) => void; - setAuthMode: (authMode: EAuthModes) => void; - setAuthStep: (authStep: EAuthSteps) => void; - setErrorInfo: (errorInfo: TAuthErrorInfo | undefined) => void; - currentAuthMode: EAuthModes; -}; - -const authService = new AuthService(); - -export const AuthFormRoot = observer(function AuthFormRoot(props: TAuthFormRoot) { - const { authStep, authMode, email, setEmail, setAuthMode, setAuthStep, setErrorInfo, currentAuthMode } = props; - // router - const router = useAppRouter(); - // query params - const searchParams = useSearchParams(); - const nextPath = searchParams.get("next_path"); - // states - const [isExistingEmail, setIsExistingEmail] = useState(false); - // hooks - const { config } = useInstance(); - - const isSMTPConfigured = config?.is_smtp_configured || false; - - // submit handler- email verification - const handleEmailVerification = async (data: IEmailCheckData) => { - setEmail(data.email); - setErrorInfo(undefined); - await authService - .emailCheck(data) - .then(async (response) => { - if (response.existing) { - if (currentAuthMode === EAuthModes.SIGN_UP) setAuthMode(EAuthModes.SIGN_IN); - if (response.status === "MAGIC_CODE") { - setAuthStep(EAuthSteps.UNIQUE_CODE); - generateEmailUniqueCode(data.email); - } else if (response.status === "CREDENTIAL") { - setAuthStep(EAuthSteps.PASSWORD); - } - } else { - if (currentAuthMode === EAuthModes.SIGN_IN) setAuthMode(EAuthModes.SIGN_UP); - if (response.status === "MAGIC_CODE") { - setAuthStep(EAuthSteps.UNIQUE_CODE); - generateEmailUniqueCode(data.email); - } else if (response.status === "CREDENTIAL") { - setAuthStep(EAuthSteps.PASSWORD); - } - } - setIsExistingEmail(response.existing); - }) - .catch((error) => { - const errorhandler = authErrorHandler(error?.error_code?.toString(), data?.email || undefined); - if (errorhandler?.type) setErrorInfo(errorhandler); - }); - }; - - const handleEmailClear = () => { - setAuthMode(currentAuthMode); - setErrorInfo(undefined); - setEmail(""); - setAuthStep(EAuthSteps.EMAIL); - router.push(currentAuthMode === EAuthModes.SIGN_IN ? `/` : "/sign-up"); - }; - - // generating the unique code - const generateEmailUniqueCode = async (email: string): Promise<{ code: string } | undefined> => { - if (!isSMTPConfigured) return; - const payload = { email: email }; - return await authService - .generateUniqueCode(payload) - .then(() => ({ code: "" })) - .catch((error) => { - const errorhandler = authErrorHandler(error?.error_code?.toString()); - if (errorhandler?.type) setErrorInfo(errorhandler); - throw error; - }); - }; - - if (authStep === EAuthSteps.EMAIL) { - return ; - } - if (authStep === EAuthSteps.UNIQUE_CODE) { - return ( - - ); - } - if (authStep === EAuthSteps.PASSWORD) { - return ( - { - if (step === EAuthSteps.UNIQUE_CODE) generateEmailUniqueCode(email); - setAuthStep(step); - }} - nextPath={nextPath || undefined} - /> - ); - } - - return <>; -}); diff --git a/apps/web/core/components/account/auth-forms/index.ts b/apps/web/core/components/account/auth-forms/index.ts deleted file mode 100644 index 125f6699c..000000000 --- a/apps/web/core/components/account/auth-forms/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./auth-root"; diff --git a/apps/web/core/components/account/auth-forms/password.tsx b/apps/web/core/components/account/auth-forms/password.tsx deleted file mode 100644 index 9b8684ceb..000000000 --- a/apps/web/core/components/account/auth-forms/password.tsx +++ /dev/null @@ -1,304 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect, useMemo, useRef, useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -// icons -import { CloseCircleOutline, HideOutline, ShowOutline } from "@makeplane/propel/icons"; -// plane imports -import { Banner } from "@makeplane/propel/components/banner"; -import { Input, InputGroup } from "@makeplane/propel/components/input"; -import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { PasswordStrengthIndicator, Spinner } from "@plane/ui"; -import { getPasswordStrength } from "@plane/utils"; -// components -import { ForgotPasswordPopover } from "@/components/account/auth-forms/forgot-password-popover"; -// constants -// helpers -import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper"; -// services -import { AuthService } from "@/services/auth.service"; - -type Props = { - email: string; - isSMTPConfigured: boolean; - mode: EAuthModes; - handleEmailClear: () => void; - handleAuthStep: (step: EAuthSteps) => void; - nextPath: string | undefined; -}; - -type TPasswordFormValues = { - email: string; - password: string; - confirm_password?: string; -}; - -const defaultValues: TPasswordFormValues = { - email: "", - password: "", -}; - -const authService = new AuthService(); - -export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props) { - const { email, isSMTPConfigured, handleAuthStep, handleEmailClear, mode, nextPath } = props; - // plane imports - const { t } = useTranslation(); - // ref - const formRef = useRef(null); - // states - const [csrfPromise, setCsrfPromise] = useState | undefined>(undefined); - const [passwordFormData, setPasswordFormData] = useState({ ...defaultValues, email }); - const [showPassword, setShowPassword] = useState({ - password: false, - retypePassword: false, - }); - const [isSubmitting, setIsSubmitting] = useState(false); - const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); - const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false); - const [isBannerMessage, setBannerMessage] = useState(false); - - const handleShowPassword = (key: keyof typeof showPassword) => - setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); - - const handleFormChange = (key: keyof TPasswordFormValues, value: string) => - setPasswordFormData((prev) => ({ ...prev, [key]: value })); - - useEffect(() => { - if (csrfPromise === undefined) { - const promise = authService.requestCSRFToken(); - setCsrfPromise(promise); - } - }, [csrfPromise]); - - const redirectToUniqueCodeSignIn = async () => { - handleAuthStep(EAuthSteps.UNIQUE_CODE); - }; - - const passwordSupport = - mode === EAuthModes.SIGN_IN ? ( -
- {isSMTPConfigured ? ( - - {t("auth.common.forgot_password")} - - ) : ( - - )} -
- ) : ( - passwordFormData.password.length > 0 && - getPasswordStrength(passwordFormData.password) != E_PASSWORD_STRENGTH.STRENGTH_VALID && ( - - ) - ); - - const isButtonDisabled = useMemo( - () => - !isSubmitting && - !!passwordFormData.password && - (mode === EAuthModes.SIGN_UP ? passwordFormData.password === passwordFormData.confirm_password : true) - ? false - : true, - [isSubmitting, mode, passwordFormData.confirm_password, passwordFormData.password] - ); - - const password = passwordFormData?.password ?? ""; - const confirmPassword = passwordFormData?.confirm_password ?? ""; - const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length; - - const handleCSRFToken = async () => { - if (!formRef || !formRef.current) return; - const token = await csrfPromise; - if (!token?.csrf_token) return; - const csrfElement = formRef.current.querySelector("input[name=csrfmiddlewaretoken]"); - csrfElement?.setAttribute("value", token?.csrf_token); - }; - - return ( - <> - {isBannerMessage && mode === EAuthModes.SIGN_UP && ( - setBannerMessage(false)} - /> - )} -
{ - event.preventDefault(); // Prevent form from submitting by default - await handleCSRFToken(); - const isPasswordValid = - mode === EAuthModes.SIGN_UP - ? getPasswordStrength(passwordFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID - : true; - if (isPasswordValid) { - setIsSubmitting(true); - if (formRef.current) formRef.current.submit(); // Manually submit the form if the condition is met - } else { - setBannerMessage(true); - } - }} - onError={() => { - setIsSubmitting(false); - }} - > - - - {nextPath && } -
- - - handleFormChange("email", e.target.value)} - placeholder={t("auth.common.email.placeholder")} - disabled - /> - {passwordFormData.email.length > 0 && ( - - )} - -
- -
- - - handleFormChange("password", e.target.value)} - placeholder={t("auth.common.password.placeholder")} - onFocus={() => setIsPasswordInputFocused(true)} - onBlur={() => setIsPasswordInputFocused(false)} - autoComplete="off" - autoFocus - /> - - - {passwordSupport} -
- - {mode === EAuthModes.SIGN_UP && ( -
- - - handleFormChange("confirm_password", e.target.value)} - placeholder={t("auth.common.password.confirm_password.placeholder")} - onFocus={() => setIsRetryPasswordInputFocused(true)} - onBlur={() => setIsRetryPasswordInputFocused(false)} - autoComplete="off" - /> - - - {!!passwordFormData.confirm_password && - passwordFormData.password !== passwordFormData.confirm_password && - renderPasswordMatchError && ( - {t("auth.common.password.errors.match")} - )} -
- )} - -
- {mode === EAuthModes.SIGN_IN ? ( - <> - - {isSMTPConfigured && ( - - )} - - ) : ( - - )} -
-
- - ); -}); diff --git a/apps/web/core/components/account/auth-forms/reset-password.tsx b/apps/web/core/components/account/auth-forms/reset-password.tsx deleted file mode 100644 index a11b05bea..000000000 --- a/apps/web/core/components/account/auth-forms/reset-password.tsx +++ /dev/null @@ -1,211 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect, useMemo, useState } from "react"; -import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -// icons -import { HideOutline, ShowOutline } from "@makeplane/propel/icons"; -// ui -import { Banner } from "@makeplane/propel/components/banner"; -import { Input, InputGroup } from "@makeplane/propel/components/input"; -import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { PasswordStrengthIndicator } from "@plane/ui"; -// components -import { getPasswordStrength } from "@plane/utils"; -// helpers -import type { EAuthenticationErrorCodes, TAuthErrorInfo } from "@/helpers/authentication.helper"; -import { EErrorAlertType, authErrorHandler } from "@/helpers/authentication.helper"; -// services -import { AuthService } from "@/services/auth.service"; -// local imports -import { FormContainer } from "./common/container"; -import { AuthFormHeader } from "./common/header"; - -type TResetPasswordFormValues = { - email: string; - password: string; - confirm_password?: string; -}; - -const defaultValues: TResetPasswordFormValues = { - email: "", - password: "", -}; - -// services -const authService = new AuthService(); - -export const ResetPasswordForm = observer(function ResetPasswordForm() { - // search params - const searchParams = useSearchParams(); - const uidb64 = searchParams.get("uidb64"); - const token = searchParams.get("token"); - const email = searchParams.get("email"); - const error_code = searchParams.get("error_code"); - // states - const [showPassword, setShowPassword] = useState({ - password: false, - retypePassword: false, - }); - const [resetFormData, setResetFormData] = useState({ - ...defaultValues, - email: email ? email.toString() : "", - }); - const [csrfToken, setCsrfToken] = useState(undefined); - const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); - const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false); - const [errorInfo, setErrorInfo] = useState(undefined); - // plane hooks - const { t } = useTranslation(); - - const handleShowPassword = (key: keyof typeof showPassword) => - setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); - - const handleFormChange = (key: keyof TResetPasswordFormValues, value: string) => - setResetFormData((prev) => ({ ...prev, [key]: value })); - - useEffect(() => { - if (csrfToken === undefined) - authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); - }, [csrfToken]); - - const isButtonDisabled = useMemo( - () => - !!resetFormData.password && - getPasswordStrength(resetFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID && - resetFormData.password === resetFormData.confirm_password - ? false - : true, - [resetFormData] - ); - - useEffect(() => { - if (error_code) { - const errorhandler = authErrorHandler(error_code?.toString() as EAuthenticationErrorCodes); - if (errorhandler) { - setErrorInfo(errorhandler); - } - } - }, [error_code]); - - const password = resetFormData?.password ?? ""; - const confirmPassword = resetFormData?.confirm_password ?? ""; - const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length; - - return ( - - - - {errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && ( - setErrorInfo(undefined)} - /> - )} -
- -
- - - - -
-
- - - handleFormChange("password", e.target.value)} - placeholder={t("auth.common.password.placeholder")} - minLength={8} - onFocus={() => setIsPasswordInputFocused(true)} - onBlur={() => setIsPasswordInputFocused(false)} - autoComplete="new-password" - autoFocus - /> - - - -
-
- - - handleFormChange("confirm_password", e.target.value)} - placeholder={t("auth.common.password.confirm_password.placeholder")} - onFocus={() => setIsRetryPasswordInputFocused(true)} - onBlur={() => setIsRetryPasswordInputFocused(false)} - autoComplete="new-password" - /> - - - {!!resetFormData.confirm_password && - resetFormData.password !== resetFormData.confirm_password && - renderPasswordMatchError && ( - {t("auth.common.password.errors.match")} - )} -
- -
-
- ); -}); diff --git a/apps/web/core/components/account/auth-forms/set-password.tsx b/apps/web/core/components/account/auth-forms/set-password.tsx deleted file mode 100644 index 044d9b084..000000000 --- a/apps/web/core/components/account/auth-forms/set-password.tsx +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { FormEvent } from "react"; -import { useEffect, useMemo, useState } from "react"; -import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -// icons -import { HideOutline, ShowOutline } from "@makeplane/propel/icons"; -// plane imports -import { Input, InputGroup } from "@makeplane/propel/components/input"; -import { E_PASSWORD_STRENGTH } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { PasswordStrengthIndicator } from "@plane/ui"; -// components -import { getPasswordStrength } from "@plane/utils"; -// hooks -import { useUser } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -// services -import { AuthService } from "@/services/auth.service"; -// local components -import { FormContainer } from "./common/container"; -import { AuthFormHeader } from "./common/header"; - -type TResetPasswordFormValues = { - email: string; - password: string; - confirm_password?: string; -}; - -const defaultValues: TResetPasswordFormValues = { - email: "", - password: "", -}; - -// services -const authService = new AuthService(); - -export const SetPasswordForm = observer(function SetPasswordForm() { - // router - const router = useAppRouter(); - // search params - const searchParams = useSearchParams(); - const email = searchParams.get("email"); - // states - const [showPassword, setShowPassword] = useState({ - password: false, - retypePassword: false, - }); - const [passwordFormData, setPasswordFormData] = useState({ - ...defaultValues, - email: email ? email.toString() : "", - }); - const [csrfToken, setCsrfToken] = useState(undefined); - const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); - const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false); - // plane hooks - const { t } = useTranslation(); - // hooks - const { data: user, handleSetPassword } = useUser(); - - useEffect(() => { - if (csrfToken === undefined) - authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); - }, [csrfToken]); - - const handleShowPassword = (key: keyof typeof showPassword) => - setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); - - const handleFormChange = (key: keyof TResetPasswordFormValues, value: string) => - setPasswordFormData((prev) => ({ ...prev, [key]: value })); - - const isButtonDisabled = useMemo( - () => - !!passwordFormData.password && - getPasswordStrength(passwordFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID && - passwordFormData.password === passwordFormData.confirm_password - ? false - : true, - [passwordFormData] - ); - - const handleSubmit = async (e: FormEvent) => { - try { - e.preventDefault(); - if (!csrfToken) throw new Error("csrf token not found"); - await handleSetPassword(csrfToken, { password: passwordFormData.password }); - router.push("/"); - } catch (error: unknown) { - let message = undefined; - if (error instanceof Error) { - const err = error as Error & { error?: string }; - message = err.error; - } - setToast({ - type: TOAST_TYPE.ERROR, - title: t("common.errors.default.title"), - message: message ?? t("common.errors.default.message"), - }); - } - }; - - const password = passwordFormData?.password ?? ""; - const confirmPassword = passwordFormData?.confirm_password ?? ""; - const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length; - - return ( - - -
handleSubmit(e)}> -
- - - - -
-
- - - handleFormChange("password", e.target.value)} - placeholder={t("auth.common.password.placeholder")} - minLength={8} - onFocus={() => setIsPasswordInputFocused(true)} - onBlur={() => setIsPasswordInputFocused(false)} - autoComplete="new-password" - autoFocus - /> - - - -
-
- - - handleFormChange("confirm_password", e.target.value)} - placeholder={t("auth.common.password.confirm_password.placeholder")} - onFocus={() => setIsRetryPasswordInputFocused(true)} - onBlur={() => setIsRetryPasswordInputFocused(false)} - autoComplete="new-password" - /> - - - {!!passwordFormData.confirm_password && - passwordFormData.password !== passwordFormData.confirm_password && - renderPasswordMatchError && ( - {t("auth.common.password.errors.match")} - )} -
- -
-
- ); -}); diff --git a/apps/web/core/components/account/auth-forms/unique-code.tsx b/apps/web/core/components/account/auth-forms/unique-code.tsx deleted file mode 100644 index d3a4569d5..000000000 --- a/apps/web/core/components/account/auth-forms/unique-code.tsx +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect, useState } from "react"; -import { CloseCircleOutline, TickCircleOutline } from "@makeplane/propel/icons"; -import { Input, InputGroup } from "@makeplane/propel/components/input"; -import { API_BASE_URL } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { Spinner } from "@plane/ui"; -// constants -// helpers -import { EAuthModes } from "@/helpers/authentication.helper"; -// hooks -import useTimer from "@/hooks/use-timer"; -// services -import { AuthService } from "@/services/auth.service"; - -// services -const authService = new AuthService(); - -type TAuthUniqueCodeForm = { - mode: EAuthModes; - email: string; - isExistingEmail: boolean; - handleEmailClear: () => void; - generateEmailUniqueCode: (email: string) => Promise<{ code: string } | undefined>; - nextPath: string | undefined; -}; - -type TUniqueCodeFormValues = { - email: string; - code: string; -}; - -const defaultValues: TUniqueCodeFormValues = { - email: "", - code: "", -}; - -export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) { - const { mode, email, handleEmailClear, generateEmailUniqueCode, nextPath } = props; - // derived values - const defaultResetTimerValue = 5; - // states - const [uniqueCodeFormData, setUniqueCodeFormData] = useState({ ...defaultValues, email }); - const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); - const [csrfToken, setCsrfToken] = useState(undefined); - const [isSubmitting, setIsSubmitting] = useState(false); - // timer - const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0); - // plane hooks - const { t } = useTranslation(); - - const handleFormChange = (key: keyof TUniqueCodeFormValues, value: string) => - setUniqueCodeFormData((prev) => ({ ...prev, [key]: value })); - - const generateNewCode = async (email: string) => { - try { - setIsRequestingNewCode(true); - const uniqueCode = await generateEmailUniqueCode(email); - setResendCodeTimer(defaultResetTimerValue); - handleFormChange("code", uniqueCode?.code || ""); - setIsRequestingNewCode(false); - } catch { - setResendCodeTimer(0); - console.error("Error while requesting new code"); - setIsRequestingNewCode(false); - } - }; - - useEffect(() => { - if (csrfToken === undefined) - authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); - }, [csrfToken]); - - const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0; - const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting; - - return ( -
{ - setIsSubmitting(true); - }} - onError={() => { - setIsSubmitting(false); - }} - > - - - {nextPath && } -
- - - handleFormChange("email", e.target.value)} - placeholder={t("auth.common.email.placeholder")} - autoComplete="off" - disabled - /> - {uniqueCodeFormData.email.length > 0 && ( - - )} - -
- -
- - - handleFormChange("code", e.target.value)} - placeholder={t("auth.common.unique_code.placeholder")} - autoComplete="off" - autoFocus - /> - -
-

- - {t("auth.common.unique_code.paste_code")} -

- -
-
- -
- -
-
- ); -} diff --git a/apps/web/core/components/account/deactivate-account-modal.tsx b/apps/web/core/components/account/deactivate-account-modal.tsx deleted file mode 100644 index ed18aba59..000000000 --- a/apps/web/core/components/account/deactivate-account-modal.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/** - * 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 { useTranslation } from "@plane/i18n"; -// ui -import { Button } from "@plane/propel/button"; -import { DeleteOutline } from "@makeplane/propel/icons"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; -// hooks -import { useUser } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; - -type Props = { - isOpen: boolean; - onClose: () => void; -}; - -export function DeactivateAccountModal(props: Props) { - const router = useAppRouter(); - const { isOpen, onClose } = props; - // hooks - const { t } = useTranslation(); - const { deactivateAccount, signOut } = useUser(); - - // states - const [isDeactivating, setIsDeactivating] = useState(false); - - const handleClose = () => { - setIsDeactivating(false); - onClose(); - }; - - const handleDeleteAccount = async () => { - setIsDeactivating(true); - - await deactivateAccount() - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: "Account deactivated successfully.", - }); - signOut(); - router.push("/"); - handleClose(); - return; - }) - .catch((err: any) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: err?.error, - }); - }) - .finally(() => setIsDeactivating(false)); - }; - - return ( - -
-
-
-
-
-
-

{t("deactivate_your_account")}

-

- {t("deactivate_your_account_description")} -

-
-
-
-
-
- - -
-
- ); -} diff --git a/apps/web/core/components/account/terms-and-conditions.tsx b/apps/web/core/components/account/terms-and-conditions.tsx deleted file mode 100644 index f6be59634..000000000 --- a/apps/web/core/components/account/terms-and-conditions.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -import Link from "next/link"; -import { EAuthModes } from "@plane/constants"; - -interface TermsAndConditionsProps { - authType?: EAuthModes; -} - -// Constants for better maintainability -const LEGAL_LINKS = { - termsOfService: "https://plane.so/legals/terms-and-conditions", - privacyPolicy: "https://plane.so/legals/privacy-policy", -} as const; - -const MESSAGES = { - [EAuthModes.SIGN_UP]: "By creating an account", - [EAuthModes.SIGN_IN]: "By signing in", -} as const; - -// Reusable link component to reduce duplication -function LegalLink({ href, children }: { href: string; children: React.ReactNode }) { - return ( - - {children} - - ); -} - -export function TermsAndConditions({ authType = EAuthModes.SIGN_IN }: TermsAndConditionsProps) { - return ( -
-

- {`${MESSAGES[authType]}, you understand and agree to \n our `} - Terms of Service and{" "} - Privacy Policy. -

-
- ); -} diff --git a/apps/web/core/components/active-cycles/index.ts b/apps/web/core/components/active-cycles/index.ts deleted file mode 100644 index f7c490a86..000000000 --- a/apps/web/core/components/active-cycles/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./workspace-active-cycles-upgrade"; -export * from "./workspace-active-cycles-list"; diff --git a/apps/web/core/components/active-cycles/workspace-active-cycles-list.tsx b/apps/web/core/components/active-cycles/workspace-active-cycles-list.tsx deleted file mode 100644 index ebeeee872..000000000 --- a/apps/web/core/components/active-cycles/workspace-active-cycles-list.tsx +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { useParams } from "next/navigation"; -// icons -import { CalendarOutline, CyclesOutline } from "@makeplane/propel/icons"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { Button } from "@plane/propel/button"; -import { ContentWrapper } from "@plane/ui"; -// hooks -import { useCycle } from "@/hooks/store/use-cycle"; -import { useProject } from "@/hooks/store/use-project"; -// utils & types -import { calculateCycleProgress, renderFormattedDate } from "@plane/utils"; -import type { ICycle } from "@plane/types"; - -export const WorkspaceActiveCyclesList = observer(function WorkspaceActiveCyclesList() { - const { workspaceSlug } = useParams(); - const { fetchWorkspaceCycles } = useCycle(); - const { getProjectById } = useProject(); - - const [isLoading, setIsLoading] = useState(true); - const [activeCycles, setActiveCycles] = useState([]); - - useEffect(() => { - if (!workspaceSlug) return; - const loadCycles = async () => { - setIsLoading(true); - try { - const cycles = await fetchWorkspaceCycles(workspaceSlug.toString()); - const filtered = (cycles || []).filter((c) => { - const status = c.status?.toLowerCase(); - if (status === "current") return true; - if (!status && c.start_date && c.end_date) { - const now = new Date(); - const start = new Date(c.start_date); - const end = new Date(c.end_date); - return start <= now && end >= now; - } - return false; - }); - setActiveCycles(filtered); - } catch (err) { - console.error("Failed to fetch active cycles:", err); - } finally { - setIsLoading(false); - } - }; - void loadCycles(); - }, [workspaceSlug, fetchWorkspaceCycles]); - - if (isLoading) { - return ( -
- -
- ); - } - - if (activeCycles.length === 0) { - return ( - -
- -
-
-

No active cycles

-

- There are no active cycles currently running in your projects. Start a cycle in any of your projects to see - its snapshot and progress here. -

-
- - - -
- ); - } - - return ( - -
- {activeCycles.map((cycle) => { - const projectId = cycle.project_id || cycle.project_detail?.id; - const project = projectId ? getProjectById(projectId) : undefined; - const projectName = - project?.name || (cycle.project_detail as { name?: string } | undefined)?.name || "Project"; - const progress = Math.round(calculateCycleProgress(cycle)); - const totalIssues = cycle.total_issues ?? 0; - const completedIssues = cycle.completed_issues ?? 0; - const cycleLink = `/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`; - - return ( -
-
-
- {projectName} - - Active - -
- -
- -

{cycle.name}

- - {cycle.start_date && cycle.end_date && ( -
- - - {renderFormattedDate(cycle.start_date)} - {renderFormattedDate(cycle.end_date)} - -
- )} -
- -
-
- Progress - {progress}% -
-
-
-
-
- -
- - {totalIssues} total work items - - - {completedIssues} completed - -
-
- -
- - - -
-
- ); - })} -
- - ); -}); diff --git a/apps/web/core/components/active-cycles/workspace-active-cycles-upgrade.tsx b/apps/web/core/components/active-cycles/workspace-active-cycles-upgrade.tsx deleted file mode 100644 index 6977d894c..000000000 --- a/apps/web/core/components/active-cycles/workspace-active-cycles-upgrade.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { CircleDashed, Microscope } from "lucide-react"; -import { AlertOctagonOutline, BarOutline, FolderOutline, SearchOutline } from "@makeplane/propel/icons"; -// plane imports -import { MARKETING_PRICING_PAGE_LINK } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { getButtonStyling } from "@plane/propel/button"; -import { ContentWrapper } from "@plane/ui"; -import { cn } from "@plane/utils"; -// assets -import ctaL1Dark from "@/app/assets/workspace-active-cycles/cta-l-1-dark.webp?url"; -import ctaL1Light from "@/app/assets/workspace-active-cycles/cta-l-1-light.webp?url"; -import ctaR1Dark from "@/app/assets/workspace-active-cycles/cta-r-1-dark.webp?url"; -import ctaR1Light from "@/app/assets/workspace-active-cycles/cta-r-1-light.webp?url"; -import ctaR2Dark from "@/app/assets/workspace-active-cycles/cta-r-2-dark.webp?url"; -import ctaR2Light from "@/app/assets/workspace-active-cycles/cta-r-2-light.webp?url"; -// components -import { ProIcon } from "@/components/common/pro-icon"; -// hooks -import { useUser } from "@/hooks/store/user"; - -export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [ - { - key: "10000_feet_view", - title: "10,000-feet view of all active cycles.", - description: - "Zoom out to see running cycles across all your projects at once instead of going from Cycle to Cycle in each project.", - icon: FolderOutline, - }, - { - key: "get_snapshot_of_each_active_cycle", - title: "Get a snapshot of each active cycle.", - description: - "Track high-level metrics for all active cycles, see their state of progress, and get a sense of scope against deadlines.", - icon: CircleDashed, - }, - { - key: "compare_burndowns", - title: "Compare burndowns.", - description: "Monitor how each of your teams are performing with a peek into each cycle’s burndown report.", - icon: BarOutline, - }, - { - key: "quickly_see_make_or_break_issues", - title: "Quickly see make-or-break work items. ", - description: - "Preview high-priority work items for each cycle against due dates. See all of them per cycle in one click.", - icon: AlertOctagonOutline, - }, - { - key: "zoom_into_cycles_that_need_attention", - title: "Zoom into cycles that need attention. ", - description: "Investigate the state of any cycle that doesn’t conform to expectations in one click.", - icon: SearchOutline, - }, - { - key: "stay_ahead_of_blockers", - title: "Stay ahead of blockers.", - description: - "Spot challenges from one project to another and see inter-cycle dependencies that aren’t obvious from any other view.", - icon: Microscope, - }, -]; - -export const WorkspaceActiveCyclesUpgrade = observer(function WorkspaceActiveCyclesUpgrade() { - const { t } = useTranslation(); - // store hooks - const { - userProfile: { data: userProfile }, - } = useUser(); - - const isDarkMode = userProfile?.theme.theme === "dark"; - - return ( - -
-
-
-

{t("on_demand_snapshots_of_all_your_cycles")}

-

{t("active_cycles_description")}

-
- - - l-1 - -
-
- - r-1 - - - r-2 - -
-
-
- {WORKSPACE_ACTIVE_CYCLES_DETAILS.map((item) => ( -
-
-

{t(item.key)}

- -
- {t(`${item.key}_description`)} -
- ))} -
-
- ); -}); diff --git a/apps/web/core/components/analytics/analytics-filter-actions.tsx b/apps/web/core/components/analytics/analytics-filter-actions.tsx deleted file mode 100644 index 1f9a3d206..000000000 --- a/apps/web/core/components/analytics/analytics-filter-actions.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane web components -import { observer } from "mobx-react"; -// hooks -import { useAnalytics } from "@/hooks/store/use-analytics"; -import { useProject } from "@/hooks/store/use-project"; -// components -import { ProjectSelect } from "./select/project"; - -const AnalyticsFilterActions = observer(function AnalyticsFilterActions() { - const { selectedProjects, updateSelectedProjects } = useAnalytics(); - const { joinedProjectIds } = useProject(); - return ( -
- { - updateSelectedProjects(val ?? []); - }} - projectIds={joinedProjectIds} - /> - {/* { - updateSelectedDuration(val); - }} - dropdownArrow - /> */} -
- ); -}); - -export default AnalyticsFilterActions; diff --git a/apps/web/core/components/analytics/analytics-section-wrapper.tsx b/apps/web/core/components/analytics/analytics-section-wrapper.tsx deleted file mode 100644 index d46ce74b4..000000000 --- a/apps/web/core/components/analytics/analytics-section-wrapper.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { cn } from "@plane/utils"; - -type Props = { - title?: string; - children: React.ReactNode; - className?: string; - subtitle?: string | null; - actions?: React.ReactNode; - headerClassName?: string; -}; - -function AnalyticsSectionWrapper(props: Props) { - const { title, children, className, actions, headerClassName } = props; - return ( -
-
- {title && ( -
-

{title}

- {/* {subtitle &&

• {subtitle}

} */} -
- )} - {actions} -
- {children} -
- ); -} - -export default AnalyticsSectionWrapper; diff --git a/apps/web/core/components/analytics/analytics-wrapper.tsx b/apps/web/core/components/analytics/analytics-wrapper.tsx deleted file mode 100644 index 6cd886b04..000000000 --- a/apps/web/core/components/analytics/analytics-wrapper.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -// plane package imports -import { useTranslation } from "@plane/i18n"; -import { cn } from "@plane/utils"; - -type Props = { - i18nTitle: string; - children: React.ReactNode; - className?: string; -}; - -function AnalyticsWrapper(props: Props) { - const { i18nTitle, children, className } = props; - const { t } = useTranslation(); - return ( -
-

{t(i18nTitle)}

- {children} -
- ); -} - -export default AnalyticsWrapper; diff --git a/apps/web/core/components/analytics/empty-state.tsx b/apps/web/core/components/analytics/empty-state.tsx deleted file mode 100644 index 8c2a72193..000000000 --- a/apps/web/core/components/analytics/empty-state.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useTheme } from "next-themes"; -// plane package imports -import { cn } from "@plane/utils"; -// assets -import darkBackgroundAsset from "@/app/assets/empty-state/analytics/empty-grid-background-dark.webp?url"; -import lightBackgroundAsset from "@/app/assets/empty-state/analytics/empty-grid-background-light.webp?url"; - -type Props = { - title: string; - description?: string; - assetPath?: string; - className?: string; -}; - -function AnalyticsEmptyState({ title, description, assetPath, className }: Props) { - // theme hook - const { resolvedTheme } = useTheme(); - const backgroundReolvedPath = resolvedTheme === "light" ? lightBackgroundAsset : darkBackgroundAsset; - - return ( -
-
- {assetPath && ( -
- {title} -
- {title} -
-
- )} -
-

{title}

- {description &&

{description}

} -
-
-
- ); -} - -export default AnalyticsEmptyState; diff --git a/apps/web/core/components/analytics/export.ts b/apps/web/core/components/analytics/export.ts deleted file mode 100644 index c77d1783e..000000000 --- a/apps/web/core/components/analytics/export.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { ColumnDef, Row } from "@tanstack/react-table"; -import { download, generateCsv, mkConfig } from "export-to-csv"; - -export const csvConfig = (workspaceSlug: string) => - mkConfig({ - fieldSeparator: ",", - filename: `${workspaceSlug}-analytics`, - decimalSeparator: ".", - useKeysAsHeaders: true, - }); - -export const exportCSV = (rows: Row[], columns: ColumnDef[], workspaceSlug: string) => { - const rowData = rows.map((row) => { - const exportColumns = columns.map((col) => col.meta?.export); - const cells = exportColumns.reduce((acc: Record, col) => { - if (col) { - const cell = col?.value(row) ?? "-"; - acc[col.label ?? col.key] = cell; - } - return acc; - }, {}); - return cells; - }); - const csv = generateCsv(csvConfig(workspaceSlug))(rowData); - download(csvConfig(workspaceSlug))(csv); -}; diff --git a/apps/web/core/components/analytics/insight-card.tsx b/apps/web/core/components/analytics/insight-card.tsx deleted file mode 100644 index d8a30a1c9..000000000 --- a/apps/web/core/components/analytics/insight-card.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane package imports -import React from "react"; -import type { IAnalyticsResponseFields } from "@plane/types"; -import { Loader } from "@plane/ui"; - -export type InsightCardProps = { - data?: IAnalyticsResponseFields; - label: string; - isLoading?: boolean; -}; - -function InsightCard(props: InsightCardProps) { - const { data, label, isLoading = false } = props; - const count = data?.count ?? 0; - - return ( -
-
{label}
- {!isLoading ? ( -
-
{count}
-
- ) : ( - - )} -
- ); -} - -export default InsightCard; diff --git a/apps/web/core/components/analytics/insight-table/data-table.tsx b/apps/web/core/components/analytics/insight-table/data-table.tsx deleted file mode 100644 index 679e98afd..000000000 --- a/apps/web/core/components/analytics/insight-table/data-table.tsx +++ /dev/null @@ -1,175 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import * as React from "react"; -import type { - ColumnDef, - ColumnFiltersState, - SortingState, - VisibilityState, - Table as TanstackTable, -} from "@tanstack/react-table"; -import { - flexRender, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { useTranslation } from "@plane/i18n"; -import { EmptyStateCompact } from "@plane/propel/empty-state"; -import { CloseOutline, SearchOutline } from "@makeplane/propel/icons"; -// plane package imports -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table"; -import { cn } from "@plane/utils"; -// plane web components - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - searchPlaceholder: string; - actions?: (table: TanstackTable) => React.ReactNode; -} - -export function DataTable({ columns, data, searchPlaceholder, actions }: DataTableProps) { - const [rowSelection, _setRowSelection] = React.useState({}); - const [columnVisibility, setColumnVisibility] = React.useState({}); - const [columnFilters, setColumnFilters] = React.useState([]); - const [sorting, _setSorting] = React.useState([]); - const { t } = useTranslation(); - const inputRef = React.useRef(null); - const [isSearchOpen, setIsSearchOpen] = React.useState(false); - - const table = useReactTable({ - data, - columns, - state: { - sorting, - columnVisibility, - rowSelection, - columnFilters, - }, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - }); - - return ( -
-
-
- {table.getHeaderGroups()?.[0]?.headers?.[0]?.id && ( -
- {searchPlaceholder} -
- )} - {!isSearchOpen && ( - - )} -
- - { - const columnId = table.getHeaderGroups()?.[0]?.headers?.[0]?.id; - if (columnId) table.getColumn(columnId)?.setFilterValue(e.target.value); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - setIsSearchOpen(true); - } - }} - /> - {isSearchOpen && ( - - )} -
-
- {actions &&
{actions(table)}
} -
- -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : (flexRender(header.column.columnDef.header, header.getContext()) as any)} - - ))} - - ))} - - - {table.getRowModel().rows?.length > 0 ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext()) as any} - - ))} - - )) - ) : ( - - - - - - )} - -
-
-
- ); -} diff --git a/apps/web/core/components/analytics/insight-table/index.ts b/apps/web/core/components/analytics/insight-table/index.ts deleted file mode 100644 index d98033459..000000000 --- a/apps/web/core/components/analytics/insight-table/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./root"; diff --git a/apps/web/core/components/analytics/insight-table/loader.tsx b/apps/web/core/components/analytics/insight-table/loader.tsx deleted file mode 100644 index e07da2374..000000000 --- a/apps/web/core/components/analytics/insight-table/loader.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import * as React from "react"; -import type { ColumnDef } from "@tanstack/react-table"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table"; -import { Loader } from "@plane/ui"; - -interface TableSkeletonProps { - columns: ColumnDef[]; - rows: number; -} - -export function TableLoader({ columns, rows }: TableSkeletonProps) { - return ( - - - - {columns.map((column, index) => ( - - {typeof column.header === "string" ? column.header : ""} - - ))} - - - - {Array.from({ length: rows }).map((_, rowIndex) => ( - - {columns.map((_, colIndex) => ( - - - - ))} - - ))} - -
- ); -} diff --git a/apps/web/core/components/analytics/insight-table/root.tsx b/apps/web/core/components/analytics/insight-table/root.tsx deleted file mode 100644 index 4a368be04..000000000 --- a/apps/web/core/components/analytics/insight-table/root.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { ColumnDef, Row, Table } from "@tanstack/react-table"; -import { DownloadOutline } from "@makeplane/propel/icons"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import type { AnalyticsTableDataMap, TAnalyticsTabsBase } from "@plane/types"; -import { DataTable } from "./data-table"; -import { TableLoader } from "./loader"; -interface InsightTableProps> { - analyticsType: T; - data?: AnalyticsTableDataMap[T][]; - isLoading?: boolean; - columns: ColumnDef[]; - columnsLabels?: Record; - headerText: string; - onExport?: (rows: Row[]) => void; -} - -export function InsightTable>( - props: InsightTableProps -): React.ReactElement { - const { data, isLoading, columns, headerText, onExport } = props; - const { t } = useTranslation(); - if (isLoading) { - return ; - } - - return ( -
- ) => ( - - )} - /> -
- ); -} diff --git a/apps/web/core/components/analytics/loaders.tsx b/apps/web/core/components/analytics/loaders.tsx deleted file mode 100644 index c298dd001..000000000 --- a/apps/web/core/components/analytics/loaders.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { Loader } from "@plane/ui"; - -export function ProjectInsightsLoader() { - return ( -
- - - -
- - - - - - -
-
- ); -} - -export function ChartLoader() { - return ( - - - - ); -} diff --git a/apps/web/core/components/analytics/overview/active-project-item.tsx b/apps/web/core/components/analytics/overview/active-project-item.tsx deleted file mode 100644 index 14e97fd0f..000000000 --- a/apps/web/core/components/analytics/overview/active-project-item.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane package imports -import { Logo } from "@plane/propel/emoji-icon-picker"; -import { ProjectsOutline } from "@makeplane/propel/icons"; -import { Tooltip } from "@makeplane/propel/components/tooltip"; -import { cn } from "@plane/utils"; -// plane web hooks -import { useProject } from "@/hooks/store/use-project"; - -type Props = { - project: { - id: string; - completed_issues?: number; - total_issues?: number; - }; - isLoading?: boolean; -}; - -function CompletionPercentage({ percentage }: { percentage: number }) { - const percentageColor = - percentage > 50 ? "bg-success-subtle text-success-primary" : "bg-danger-subtle text-danger-primary"; - return ( -
- {percentage}% -
- ); -} - -function ActiveProjectItem(props: Props) { - const { project } = props; - const { getProjectById } = useProject(); - const { id, completed_issues, total_issues } = project; - - const projectDetails = getProjectById(id); - - if (!projectDetails) return null; - - return ( -
-
-
- - {projectDetails?.logo_props ? ( - - ) : ( - - - - )} - -
- -

{projectDetails?.name}

-
-
- -
- ); -} - -export default ActiveProjectItem; diff --git a/apps/web/core/components/analytics/overview/active-projects.tsx b/apps/web/core/components/analytics/overview/active-projects.tsx deleted file mode 100644 index 1ad22dc54..000000000 --- a/apps/web/core/components/analytics/overview/active-projects.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import useSWR from "swr"; -// plane package imports -import { useTranslation } from "@plane/i18n"; -import { Loader } from "@plane/ui"; -// plane web hooks -import { useAnalytics } from "@/hooks/store/use-analytics"; -import { useProject } from "@/hooks/store/use-project"; -// plane web components -import AnalyticsSectionWrapper from "../analytics-section-wrapper"; -import ActiveProjectItem from "./active-project-item"; - -const ActiveProjects = observer(function ActiveProjects() { - const { t } = useTranslation(); - const { fetchProjectAnalyticsCount } = useProject(); - const { workspaceSlug } = useParams(); - const { selectedDurationLabel } = useAnalytics(); - const { data: projectAnalyticsCount, isLoading: isProjectAnalyticsCountLoading } = useSWR( - workspaceSlug ? ["projectAnalyticsCount", workspaceSlug] : null, - workspaceSlug - ? () => - fetchProjectAnalyticsCount(workspaceSlug.toString(), { - fields: "total_work_items,total_completed_work_items", - }) - : null - ); - return ( - -
- {isProjectAnalyticsCountLoading && - Array.from({ length: 5 }).map((_, index) => )} - {!isProjectAnalyticsCountLoading && - projectAnalyticsCount?.map((project) => )} -
-
- ); -}); - -export default ActiveProjects; diff --git a/apps/web/core/components/analytics/overview/index.ts b/apps/web/core/components/analytics/overview/index.ts deleted file mode 100644 index d98033459..000000000 --- a/apps/web/core/components/analytics/overview/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./root"; diff --git a/apps/web/core/components/analytics/overview/project-insights.tsx b/apps/web/core/components/analytics/overview/project-insights.tsx deleted file mode 100644 index 6987ce194..000000000 --- a/apps/web/core/components/analytics/overview/project-insights.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { lazy, Suspense } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import useSWR from "swr"; -// plane package imports -import { useTranslation } from "@plane/i18n"; -import { EmptyStateCompact } from "@plane/propel/empty-state"; -import type { TChartData } from "@plane/types"; -// hooks -import { useAnalytics } from "@/hooks/store/use-analytics"; -// services -import { AnalyticsService } from "@/services/analytics.service"; -// plane web components -import AnalyticsSectionWrapper from "../analytics-section-wrapper"; -import { ProjectInsightsLoader } from "../loaders"; - -const RadarChart = lazy(function RadarChart() { - return import("@plane/propel/charts/radar-chart").then((mod) => ({ - default: mod.RadarChart, - })); -}); - -const analyticsService = new AnalyticsService(); - -const ProjectInsights = observer(function ProjectInsights() { - const params = useParams(); - const { t } = useTranslation(); - const workspaceSlug = params.workspaceSlug.toString(); - const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } = - useAnalytics(); - - const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR( - `radar-chart-project-insights-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, - () => - analyticsService.getAdvanceAnalyticsCharts[]>( - workspaceSlug, - "projects", - { - // date_filter: selectedDuration, - ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), - ...(selectedCycle ? { cycle_id: selectedCycle } : {}), - ...(selectedModule ? { module_id: selectedModule } : {}), - }, - isPeekView - ) - ); - - return ( - - {isLoadingProjectInsight ? ( - - ) : projectInsightsData && projectInsightsData?.length == 0 ? ( - - ) : ( -
- {projectInsightsData && ( - }> - - - )} -
-
{t("workspace_analytics.summary_of_projects")}
-
{t("workspace_analytics.all_projects")}
-
-
-
{t("workspace_analytics.trend_on_charts")}
-
{t("common.work_items")}
-
- {projectInsightsData?.map((item) => ( -
-
{item.name}
-
- {/* */} -
{item.count}
-
-
- ))} -
-
-
- )} -
- ); -}); - -export default ProjectInsights; diff --git a/apps/web/core/components/analytics/overview/root.tsx b/apps/web/core/components/analytics/overview/root.tsx deleted file mode 100644 index 4b191ca2f..000000000 --- a/apps/web/core/components/analytics/overview/root.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -import AnalyticsWrapper from "../analytics-wrapper"; -import TotalInsights from "../total-insights"; -import ActiveProjects from "./active-projects"; -import ProjectInsights from "./project-insights"; - -function Overview() { - return ( - -
- -
- - -
-
-
- ); -} - -export { Overview }; diff --git a/apps/web/core/components/analytics/select/analytics-params.tsx b/apps/web/core/components/analytics/select/analytics-params.tsx deleted file mode 100644 index 0f266fb7a..000000000 --- a/apps/web/core/components/analytics/select/analytics-params.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useMemo } from "react"; -import { observer } from "mobx-react"; -import type { Control, UseFormSetValue } from "react-hook-form"; -import { Controller } from "react-hook-form"; -import { CalendarOutline, PreferencesOutline } from "@makeplane/propel/icons"; -// plane package imports -import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "@plane/constants"; -import type { IAnalyticsParams } from "@plane/types"; -import { ChartYAxisMetric } from "@plane/types"; -import { cn } from "@plane/utils"; -// plane web components -import { SelectXAxis } from "./select-x-axis"; -import { SelectYAxis } from "./select-y-axis"; - -type Props = { - control: Control; - setValue: UseFormSetValue; - params: IAnalyticsParams; - workspaceSlug: string; - classNames?: string; - isEpic?: boolean; -}; - -export const AnalyticsSelectParams = observer(function AnalyticsSelectParams(props: Props) { - const { control, params, classNames, isEpic } = props; - const xAxisOptions = useMemo( - () => ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), - [params.group_by] - ); - const groupByOptions = useMemo( - () => ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), - [params.x_axis] - ); - - return ( -
-
- ( - { - onChange(val); - }} - options={ANALYTICS_Y_AXIS_VALUES} - hiddenOptions={[ - ChartYAxisMetric.ESTIMATE_POINT_COUNT, - isEpic ? ChartYAxisMetric.WORK_ITEM_COUNT : ChartYAxisMetric.EPIC_WORK_ITEM_COUNT, - ]} - /> - )} - /> - ( - { - onChange(val); - }} - label={ -
- - - {xAxisOptions.find((v) => v.value === value)?.label || "Add Property"} - -
- } - options={xAxisOptions} - /> - )} - /> - ( - { - onChange(val); - }} - label={ -
- - - {groupByOptions.find((v) => v.value === value)?.label || "Add Property"} - -
- } - options={groupByOptions} - placeholder="Group By" - allowNoValue - /> - )} - /> -
-
- ); -}); diff --git a/apps/web/core/components/analytics/select/duration.tsx b/apps/web/core/components/analytics/select/duration.tsx deleted file mode 100644 index 7748484fc..000000000 --- a/apps/web/core/components/analytics/select/duration.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane package imports -import type { ReactNode } from "react"; -import React from "react"; -import { CalendarOutline } from "@makeplane/propel/icons"; -// plane package imports -import { ANALYTICS_DURATION_FILTER_OPTIONS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { CustomSearchSelect } from "@plane/ui"; -// types -import type { TDropdownProps } from "@/components/dropdowns/types"; - -type Props = TDropdownProps & { - value: string | null; - onChange: (val: (typeof ANALYTICS_DURATION_FILTER_OPTIONS)[number]["value"]) => void; - //optional - button?: ReactNode; - dropdownArrow?: boolean; - dropdownArrowClassName?: string; - onClose?: () => void; - renderByDefault?: boolean; - tabIndex?: number; -}; - -function DurationDropdown({ placeholder = "Duration", onChange, value }: Props) { - useTranslation(); - - const options = ANALYTICS_DURATION_FILTER_OPTIONS.map((option) => ({ - value: option.value, - query: option.name, - content: ( -
- {option.name} -
- ), - })); - return ( - - - {value ? ANALYTICS_DURATION_FILTER_OPTIONS.find((opt) => opt.value === value)?.name : placeholder} -
- } - /> - ); -} - -export default DurationDropdown; diff --git a/apps/web/core/components/analytics/select/project.tsx b/apps/web/core/components/analytics/select/project.tsx deleted file mode 100644 index cc0896c86..000000000 --- a/apps/web/core/components/analytics/select/project.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane package imports -import { getButtonStyling } from "@plane/propel/button"; -import { Logo } from "@plane/propel/emoji-icon-picker"; -import { ChevronDownOutline, ProjectsOutline } from "@makeplane/propel/icons"; -import { CustomSearchSelect } from "@plane/ui"; -import { cn } from "@plane/utils"; -// hooks -import { useProject } from "@/hooks/store/use-project"; - -type Props = { - value: string[] | undefined; - onChange: (val: string[] | null) => void; - projectIds: string[] | undefined; -}; - -export const ProjectSelect = observer(function ProjectSelect(props: Props) { - const { value, onChange, projectIds } = props; - const { getProjectById } = useProject(); - - const options = projectIds?.map((projectId) => { - const projectDetails = getProjectById(projectId); - - return { - value: projectDetails?.id, - query: `${projectDetails?.name} ${projectDetails?.identifier}`, - content: ( -
- {projectDetails?.logo_props ? ( - - ) : ( - - )} - {projectDetails?.name} -
- ), - }; - }); - - return ( - onChange(val)} - options={options} - className="border-none p-0" - customButton={ -
- - {value && value.length > 3 - ? `3+ projects` - : value && value.length > 0 - ? projectIds - ?.filter((p) => value.includes(p)) - .map((p) => getProjectById(p)?.name) - .join(", ") - : "All projects"} -
- } - customButtonClassName="border-none p-0 bg-transparent hover:bg-transparent w-auto h-auto" - multiple - /> - ); -}); diff --git a/apps/web/core/components/analytics/select/select-x-axis.tsx b/apps/web/core/components/analytics/select/select-x-axis.tsx deleted file mode 100644 index 6effcfa4d..000000000 --- a/apps/web/core/components/analytics/select/select-x-axis.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane package imports -import type { ChartXAxisProperty } from "@plane/types"; -import { CustomSelect } from "@plane/ui"; - -type Props = { - value?: ChartXAxisProperty; - onChange: (val: ChartXAxisProperty | null) => void; - options: { value: ChartXAxisProperty; label: string }[]; - placeholder?: string; - hiddenOptions?: ChartXAxisProperty[]; - allowNoValue?: boolean; - label?: string | React.ReactNode; -}; - -export function SelectXAxis(props: Props) { - const { value, onChange, options, hiddenOptions, allowNoValue, label } = props; - return ( - - {allowNoValue && No value} - {options.map((item) => { - if (hiddenOptions?.includes(item.value)) return null; - return ( - - {item.label} - - ); - })} - - ); -} diff --git a/apps/web/core/components/analytics/select/select-y-axis.tsx b/apps/web/core/components/analytics/select/select-y-axis.tsx deleted file mode 100644 index 0d0470eeb..000000000 --- a/apps/web/core/components/analytics/select/select-y-axis.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import { EEstimateSystem } from "@plane/constants"; -import { ProjectsOutline } from "@makeplane/propel/icons"; -import type { ChartYAxisMetric } from "@plane/types"; -// plane package imports -import { CustomSelect } from "@plane/ui"; -// hooks -import { useProjectEstimates } from "@/hooks/store/estimates"; -// plane web constants -type Props = { - value: ChartYAxisMetric; - onChange: (val: ChartYAxisMetric | null) => void; - hiddenOptions?: ChartYAxisMetric[]; - options: { value: ChartYAxisMetric; label: string }[]; -}; - -export const SelectYAxis = observer(function SelectYAxis({ value, onChange, hiddenOptions, options }: Props) { - // hooks - const { projectId } = useParams(); - const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates(); - - const isEstimateEnabled = (analyticsOption: string) => { - if (analyticsOption === "estimate") { - if ( - projectId && - currentActiveEstimateId && - areEstimateEnabledByProjectId(projectId.toString()) && - estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS - ) { - return true; - } else { - return false; - } - } - - return true; - }; - - return ( - - - {options.find((v) => v.value === value)?.label ?? "Add Metric"} -
- } - onChange={onChange} - maxHeight="lg" - > - {options.map((item) => { - if (hiddenOptions?.includes(item.value)) return null; - return ( - isEstimateEnabled(item.value) && ( - - {item.label} - - ) - ); - })} - - ); -}); diff --git a/apps/web/core/components/analytics/tabs.tsx b/apps/web/core/components/analytics/tabs.tsx deleted file mode 100644 index 1fdbc0cbc..000000000 --- a/apps/web/core/components/analytics/tabs.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import type { AnalyticsTab } from "@plane/types"; -import { Overview } from "@/components/analytics/overview"; -import { WorkItems } from "@/components/analytics/work-items"; - -export const getAnalyticsTabs = (t: (key: string, params?: Record) => string): AnalyticsTab[] => [ - { key: "overview", label: t("common.overview"), content: Overview, isDisabled: false }, - { key: "work-items", label: t("sidebar.work_items"), content: WorkItems, isDisabled: false }, -]; diff --git a/apps/web/core/components/analytics/total-insights.tsx b/apps/web/core/components/analytics/total-insights.tsx deleted file mode 100644 index 98e5ed0ee..000000000 --- a/apps/web/core/components/analytics/total-insights.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane package imports -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import useSWR from "swr"; -import type { IInsightField } from "@plane/constants"; -import { ANALYTICS_INSIGHTS_FIELDS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import type { IAnalyticsResponse, TAnalyticsTabsBase } from "@plane/types"; -import { cn } from "@plane/utils"; -// hooks -import { useAnalytics } from "@/hooks/store/use-analytics"; -// services -import { AnalyticsService } from "@/services/analytics.service"; -// local imports -import InsightCard from "./insight-card"; - -const analyticsService = new AnalyticsService(); - -const getInsightLabel = ( - analyticsType: TAnalyticsTabsBase, - item: IInsightField, - isEpic: boolean | undefined, - t: (key: string, params?: Record) => string -) => { - if (analyticsType === "work-items") { - return isEpic - ? t(item.i18nKey, { entity: t("common.epics") }) - : t(item.i18nKey, { entity: t("common.work_items") }); - } - - // Get the base translation with entity - const baseTranslation = t(item.i18nKey, { - ...item.i18nProps, - entity: item.i18nProps?.entity && t(item.i18nProps?.entity), - }); - - // Add prefix if available - const prefix = item.i18nProps?.prefix ? `${t(item.i18nProps.prefix)} ` : ""; - - // Add suffix if available - const suffix = item.i18nProps?.suffix ? ` ${t(item.i18nProps.suffix)}` : ""; - - // Combine prefix, base translation, and suffix - return `${prefix}${baseTranslation}${suffix}`; -}; - -const TotalInsights = observer(function TotalInsights({ - analyticsType, - peekView, -}: { - analyticsType: TAnalyticsTabsBase; - peekView?: boolean; -}) { - const params = useParams(); - const workspaceSlug = params.workspaceSlug.toString(); - const { t } = useTranslation(); - const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView, isEpic } = useAnalytics(); - const { data: totalInsightsData, isLoading } = useSWR( - `total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isEpic}`, - () => - analyticsService.getAdvanceAnalytics( - workspaceSlug, - analyticsType, - { - // date_filter: selectedDuration, - ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), - ...(selectedCycle ? { cycle_id: selectedCycle } : {}), - ...(selectedModule ? { module_id: selectedModule } : {}), - ...(isEpic ? { epic: true } : {}), - }, - isPeekView - ) - ); - return ( -
- {ANALYTICS_INSIGHTS_FIELDS[analyticsType]?.map((item) => ( - - ))} -
- ); -}); - -export default TotalInsights; diff --git a/apps/web/core/components/analytics/trend-piece.tsx b/apps/web/core/components/analytics/trend-piece.tsx deleted file mode 100644 index 744e40cbf..000000000 --- a/apps/web/core/components/analytics/trend-piece.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane package imports -import React from "react"; -import { TrendingDown, TrendingUp } from "lucide-react"; -import { cn } from "@plane/utils"; -// plane web components - -type Props = { - percentage: number; - className?: string; - size?: "xs" | "sm" | "md" | "lg"; - trendIconVisible?: boolean; - variant?: "simple" | "outlined" | "tinted"; -}; - -const sizeConfig = { - xs: { - text: "text-11", - icon: "w-3 h-3", - }, - sm: { - text: "text-13", - icon: "w-4 h-4", - }, - md: { - text: "text-14", - icon: "w-5 h-5", - }, - lg: { - text: "text-16", - icon: "w-6 h-6", - }, -} as const; - -const variants: Record, Record<"ontrack" | "offtrack" | "atrisk", string>> = { - simple: { - ontrack: "text-success-primary", - offtrack: "text-yellow-500", - atrisk: "text-danger-primary", - }, - outlined: { - ontrack: "text-success-primary border border-success-strong", - offtrack: "text-yellow-500 border border-yellow-500", - atrisk: "text-danger-primary border border-danger-strong", - }, - tinted: { - ontrack: "text-success-primary bg-success-subtle", - offtrack: "text-yellow-500 bg-yellow-500/10", - atrisk: "text-danger-primary bg-danger-subtle", - }, -} as const; - -function TrendPiece(props: Props) { - const { percentage, className, trendIconVisible = true, size = "sm", variant = "simple" } = props; - const isOnTrack = percentage >= 66; - const isOffTrack = percentage >= 33 && percentage < 66; - const config = sizeConfig[size]; - - return ( -
- {trendIconVisible && - (isOnTrack ? ( - - ) : isOffTrack ? ( - - ) : ( - - ))} - {Math.round(Math.abs(percentage))}% -
- ); -} - -export default TrendPiece; diff --git a/apps/web/core/components/analytics/use-analytics-tabs.tsx b/apps/web/core/components/analytics/use-analytics-tabs.tsx deleted file mode 100644 index d022d74f0..000000000 --- a/apps/web/core/components/analytics/use-analytics-tabs.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useMemo } from "react"; -import { useTranslation } from "@plane/i18n"; -import { getAnalyticsTabs } from "./tabs"; - -export const useAnalyticsTabs = (_workspaceSlug: string) => { - const { t } = useTranslation(); - - const analyticsTabs = useMemo(() => getAnalyticsTabs(t), [t]); - - return analyticsTabs; -}; diff --git a/apps/web/core/components/analytics/work-items/created-vs-resolved.tsx b/apps/web/core/components/analytics/work-items/created-vs-resolved.tsx deleted file mode 100644 index c4723c07b..000000000 --- a/apps/web/core/components/analytics/work-items/created-vs-resolved.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useMemo } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import useSWR from "swr"; -// plane package imports -import { useTranslation } from "@plane/i18n"; -import { AreaChart } from "@plane/propel/charts/area-chart"; -import { EmptyStateCompact } from "@plane/propel/empty-state"; -import type { IChartResponse, TChartData } from "@plane/types"; -import { renderFormattedDate } from "@plane/utils"; -// hooks -import { useAnalytics } from "@/hooks/store/use-analytics"; -// services -import { AnalyticsService } from "@/services/analytics.service"; -// plane web components -import AnalyticsSectionWrapper from "../analytics-section-wrapper"; -import { ChartLoader } from "../loaders"; - -const analyticsService = new AnalyticsService(); -const CreatedVsResolved = observer(function CreatedVsResolved() { - const { - selectedDuration, - selectedDurationLabel, - selectedProjects, - selectedCycle, - selectedModule, - isPeekView, - isEpic, - } = useAnalytics(); - const params = useParams(); - const { t } = useTranslation(); - const workspaceSlug = params.workspaceSlug.toString(); - const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR( - `created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}-${isEpic}`, - () => - analyticsService.getAdvanceAnalyticsCharts( - workspaceSlug, - "work-items", - { - // date_filter: selectedDuration, - ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), - ...(selectedCycle ? { cycle_id: selectedCycle } : {}), - ...(selectedModule ? { module_id: selectedModule } : {}), - ...(isEpic ? { epic: true } : {}), - }, - isPeekView - ) - ); - const parsedData: TChartData[] = useMemo(() => { - if (!createdVsResolvedData?.data) return []; - return createdVsResolvedData.data.map((datum) => ({ - ...datum, - [datum.key]: datum.count, - name: renderFormattedDate(datum.key) ?? datum.key, - })); - }, [createdVsResolvedData]); - - const areas = useMemo( - () => [ - { - key: "completed_issues", - label: "Resolved", - fill: "#19803833", - fillOpacity: 1, - stackId: "bar-one", - showDot: false, - smoothCurves: true, - strokeColor: "#198038", - strokeOpacity: 1, - }, - { - key: "created_issues", - label: "Created", - fill: "#1192E833", - fillOpacity: 1, - stackId: "bar-one", - showDot: false, - smoothCurves: true, - strokeColor: "#1192E8", - strokeOpacity: 1, - }, - ], - [] - ); - - return ( - - {isCreatedVsResolvedLoading ? ( - - ) : parsedData && parsedData.length > 0 ? ( - - ) : ( - - )} - - ); -}); - -export default CreatedVsResolved; diff --git a/apps/web/core/components/analytics/work-items/customized-insights.tsx b/apps/web/core/components/analytics/work-items/customized-insights.tsx deleted file mode 100644 index c40a853b4..000000000 --- a/apps/web/core/components/analytics/work-items/customized-insights.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import { useForm } from "react-hook-form"; -// plane package imports -import { useTranslation } from "@plane/i18n"; -import type { IAnalyticsParams } from "@plane/types"; -import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/types"; -import { cn } from "@plane/utils"; -// plane web components -import AnalyticsSectionWrapper from "../analytics-section-wrapper"; -import { AnalyticsSelectParams } from "../select/analytics-params"; -import PriorityChart from "./priority-chart"; - -const CustomizedInsights = observer(function CustomizedInsights({ - peekView, - isEpic, -}: { - peekView?: boolean; - isEpic?: boolean; -}) { - const { t } = useTranslation(); - const { workspaceSlug } = useParams(); - const { control, watch, setValue } = useForm({ - defaultValues: { - x_axis: ChartXAxisProperty.PRIORITY, - y_axis: isEpic ? ChartYAxisMetric.EPIC_WORK_ITEM_COUNT : ChartYAxisMetric.WORK_ITEM_COUNT, - }, - }); - - const params = { - x_axis: watch("x_axis"), - y_axis: watch("y_axis"), - group_by: watch("group_by"), - }; - - return ( - - } - > - - - ); -}); - -export default CustomizedInsights; diff --git a/apps/web/core/components/analytics/work-items/index.ts b/apps/web/core/components/analytics/work-items/index.ts deleted file mode 100644 index d98033459..000000000 --- a/apps/web/core/components/analytics/work-items/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -export * from "./root"; diff --git a/apps/web/core/components/analytics/work-items/modal/content.tsx b/apps/web/core/components/analytics/work-items/modal/content.tsx deleted file mode 100644 index 259dec20c..000000000 --- a/apps/web/core/components/analytics/work-items/modal/content.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import { Tab } from "@headlessui/react"; -// plane package imports -import type { ICycle, IModule, IProject } from "@plane/types"; -import { Spinner } from "@plane/ui"; -// hooks -import { useAnalytics } from "@/hooks/store/use-analytics"; -// plane web components -import TotalInsights from "../../total-insights"; -import CreatedVsResolved from "../created-vs-resolved"; -import CustomizedInsights from "../customized-insights"; -import WorkItemsInsightTable from "../workitems-insight-table"; - -type Props = { - fullScreen: boolean; - projectDetails: IProject | undefined; - cycleDetails: ICycle | undefined; - moduleDetails: IModule | undefined; - isEpic?: boolean; -}; - -export const WorkItemsModalMainContent = observer(function WorkItemsModalMainContent(props: Props) { - const { projectDetails, cycleDetails, moduleDetails, fullScreen, isEpic } = props; - const { updateSelectedProjects, updateSelectedCycle, updateSelectedModule, updateIsPeekView } = useAnalytics(); - const [isModalConfigured, setIsModalConfigured] = useState(false); - - useEffect(() => { - updateIsPeekView(true); - - // Handle project selection - if (projectDetails?.id) { - updateSelectedProjects([projectDetails.id]); - } - - // Handle cycle selection - if (cycleDetails?.id) { - updateSelectedCycle(cycleDetails.id); - } - - // Handle module selection - if (moduleDetails?.id) { - updateSelectedModule(moduleDetails.id); - } - setIsModalConfigured(true); - - // Cleanup fields - return () => { - updateSelectedProjects([]); - updateSelectedCycle(""); - updateSelectedModule(""); - updateIsPeekView(false); - }; - }, [ - projectDetails?.id, - cycleDetails?.id, - moduleDetails?.id, - updateSelectedProjects, - updateSelectedCycle, - updateSelectedModule, - updateIsPeekView, - ]); - - if (!isModalConfigured) - return ( -
- -
- ); - - return ( - -
- - - - -
-
- ); -}); diff --git a/apps/web/core/components/analytics/work-items/modal/header.tsx b/apps/web/core/components/analytics/work-items/modal/header.tsx deleted file mode 100644 index 92704524e..000000000 --- a/apps/web/core/components/analytics/work-items/modal/header.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { observer } from "mobx-react"; -// plane package imports -import { ArrowCollapseOutline, CloseOutline, FullScreenOutline } from "@makeplane/propel/icons"; -import type { ICycle, IModule } from "@plane/types"; -// icons - -type Props = { - fullScreen: boolean; - handleClose: () => void; - setFullScreen: React.Dispatch>; - title: string; - cycle?: ICycle; - module?: IModule; -}; - -export const WorkItemsModalHeader = observer(function WorkItemsModalHeader(props: Props) { - const { fullScreen, handleClose, setFullScreen, title, cycle, module } = props; - - return ( -
-

- Analytics for {title} {cycle && `in ${cycle.name}`} {module && `in ${module.name}`} -

-
- - -
-
- ); -}); diff --git a/apps/web/core/components/analytics/work-items/modal/index.tsx b/apps/web/core/components/analytics/work-items/modal/index.tsx deleted file mode 100644 index a284ba93c..000000000 --- a/apps/web/core/components/analytics/work-items/modal/index.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React, { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -// plane package imports -import { ModalPortal, EPortalWidth, EPortalPosition } from "@plane/propel/portal"; -import type { ICycle, IModule, IProject } from "@plane/types"; -import { useAnalytics } from "@/hooks/store/use-analytics"; -// plane web components -import { WorkItemsModalMainContent } from "./content"; -import { WorkItemsModalHeader } from "./header"; - -type Props = { - isOpen: boolean; - onClose: () => void; - projectDetails?: IProject | undefined; - cycleDetails?: ICycle | undefined; - moduleDetails?: IModule | undefined; - isEpic?: boolean; -}; - -export const WorkItemsModal = observer(function WorkItemsModal(props: Props) { - const { isOpen, onClose, projectDetails, moduleDetails, cycleDetails, isEpic } = props; - const { updateIsEpic, isPeekView } = useAnalytics(); - const [fullScreen, setFullScreen] = useState(false); - - const handleClose = () => { - setFullScreen(false); - onClose(); - }; - - useEffect(() => { - updateIsEpic(isPeekView ? (isEpic ?? false) : false); - }, [isEpic, updateIsEpic, isPeekView]); - - return ( - -
- - -
-
- ); -}); diff --git a/apps/web/core/components/analytics/work-items/priority-chart.tsx b/apps/web/core/components/analytics/work-items/priority-chart.tsx deleted file mode 100644 index 97adc633a..000000000 --- a/apps/web/core/components/analytics/work-items/priority-chart.tsx +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useMemo } from "react"; -import type { ColumnDef, Row, RowData, Table } from "@tanstack/react-table"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import { useTheme } from "next-themes"; -import useSWR from "swr"; -// plane package imports -import { DownloadOutline } from "@makeplane/propel/icons"; -import type { ChartXAxisDateGrouping } from "@plane/constants"; -import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, CHART_COLOR_PALETTES, EChartModels } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { BarChart } from "@plane/propel/charts/bar-chart"; -import { EmptyStateCompact } from "@plane/propel/empty-state"; -import type { TBarItem, TChart, TChartDatum, ChartXAxisProperty, ChartYAxisMetric } from "@plane/types"; -// plane web components -import { generateExtendedColors, parseChartData } from "@/components/chart/utils"; -// hooks -import { useAnalytics } from "@/hooks/store/use-analytics"; -import { useProjectState } from "@/hooks/store/use-project-state"; -import { AnalyticsService } from "@/services/analytics.service"; -import { exportCSV } from "../export"; -import { DataTable } from "../insight-table/data-table"; -import { ChartLoader } from "../loaders"; -import { generateBarColor } from "./utils"; - -declare module "@tanstack/react-table" { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface ColumnMeta { - export: { - key: string; - value: (row: Row) => string | number; - label?: string; - }; - } -} - -interface Props { - x_axis: ChartXAxisProperty; - y_axis: ChartYAxisMetric; - group_by?: ChartXAxisProperty; - x_axis_date_grouping?: ChartXAxisDateGrouping; -} - -const analyticsService = new AnalyticsService(); -const PriorityChart = observer(function PriorityChart(props: Props) { - const { x_axis, y_axis, group_by } = props; - const { t } = useTranslation(); - // store hooks - const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView, isEpic } = useAnalytics(); - const { workspaceStates } = useProjectState(); - const { resolvedTheme } = useTheme(); - // router - const params = useParams(); - const workspaceSlug = params.workspaceSlug.toString(); - - const { data: priorityChartData, isLoading: priorityChartLoading } = useSWR( - `customized-insights-chart-${workspaceSlug}-${selectedDuration}- - ${selectedProjects}-${selectedCycle}-${selectedModule}-${props.x_axis}-${props.y_axis}-${props.group_by}-${isPeekView}-${isEpic}`, - () => - analyticsService.getAdvanceAnalyticsCharts( - workspaceSlug, - "custom-work-items", - { - // date_filter: selectedDuration, - ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), - ...(selectedCycle ? { cycle_id: selectedCycle } : {}), - ...(selectedModule ? { module_id: selectedModule } : {}), - ...(isEpic ? { epic: true } : {}), - ...props, - }, - isPeekView - ) - ); - const parsedData = useMemo( - () => - priorityChartData && parseChartData(priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping), - [priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping] - ); - const chart_model = props.group_by ? EChartModels.STACKED : EChartModels.BASIC; - - const bars: TBarItem[] = useMemo(() => { - if (!parsedData) return []; - let parsedBars: TBarItem[]; - const schemaKeys = Object.keys(parsedData.schema); - const baseColors = CHART_COLOR_PALETTES[0]?.[resolvedTheme === "dark" ? "dark" : "light"]; - const extendedColors = generateExtendedColors(baseColors ?? [], schemaKeys.length); - if (chart_model === EChartModels.BASIC) { - parsedBars = [ - { - key: "count", - label: "Count", - stackId: "bar-one", - fill: (payload) => generateBarColor(payload.key, { x_axis, y_axis, group_by }, baseColors, workspaceStates), - textClassName: "", - showPercentage: false, - showTopBorderRadius: () => true, - showBottomBorderRadius: () => true, - }, - ]; - } else if (chart_model === EChartModels.STACKED && parsedData.schema) { - const parsedExtremes: { - [key: string]: { - top: string | null; - bottom: string | null; - }; - } = {}; - parsedData.data.forEach((datum) => { - let top = null; - let bottom = null; - for (let i = 0; i < schemaKeys.length; i++) { - const key = schemaKeys[i]; - if (datum[key] === 0) continue; - if (!bottom) bottom = key; - top = key; - } - parsedExtremes[datum.key] = { top, bottom }; - }); - - parsedBars = schemaKeys.map((key, index) => ({ - key: key, - label: parsedData.schema[key], - stackId: "bar-one", - fill: extendedColors[index], - textClassName: "", - showPercentage: false, - showTopBorderRadius: (value, payload: TChartDatum) => parsedExtremes[payload.key].top === value, - showBottomBorderRadius: (value, payload: TChartDatum) => parsedExtremes[payload.key].bottom === value, - })); - } else { - parsedBars = []; - } - return parsedBars; - }, [chart_model, group_by, parsedData, resolvedTheme, workspaceStates, x_axis, y_axis]); - - const yAxisLabel = useMemo( - () => ANALYTICS_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, - [props.y_axis] - ); - const xAxisLabel = useMemo( - () => ANALYTICS_X_AXIS_VALUES.find((item) => item.value === props.x_axis)?.label ?? props.x_axis, - [props.x_axis] - ); - - const defaultColumns: ColumnDef[] = useMemo( - () => [ - { - accessorKey: "name", - header: () => xAxisLabel, - meta: { - export: { - key: xAxisLabel, - value: (row) => row.original.name, - label: xAxisLabel, - }, - }, - }, - { - accessorKey: "count", - header: () =>
Count
, - cell: ({ row }) =>
{row.original.count}
, - meta: { - export: { - key: "Count", - value: (row) => row.original.count, - label: "Count", - }, - }, - }, - ], - [xAxisLabel] - ); - - const columns: ColumnDef[] = useMemo( - () => - parsedData - ? Object.keys(parsedData?.schema ?? {}).map((key) => ({ - accessorKey: key, - header: () =>
{parsedData.schema[key]}
, - cell: ({ row }) =>
{row.original[key]}
, - meta: { - export: { - key, - value: (row) => row.original[key], - label: parsedData.schema[key], - }, - }, - })) - : [], - [parsedData] - ); - - return ( -
- {priorityChartLoading ? ( - - ) : parsedData?.data && parsedData.data.length > 0 ? ( - <> - - ) => ( - - )} - /> - - ) : ( - - )} -
- ); -}); - -export default PriorityChart; diff --git a/apps/web/core/components/analytics/work-items/root.tsx b/apps/web/core/components/analytics/work-items/root.tsx deleted file mode 100644 index 55fb767f9..000000000 --- a/apps/web/core/components/analytics/work-items/root.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -import AnalyticsWrapper from "../analytics-wrapper"; -import TotalInsights from "../total-insights"; -import CreatedVsResolved from "./created-vs-resolved"; -import CustomizedInsights from "./customized-insights"; -import WorkItemsInsightTable from "./workitems-insight-table"; - -function WorkItems() { - return ( - -
- - - - -
-
- ); -} - -export { WorkItems }; diff --git a/apps/web/core/components/analytics/work-items/utils.ts b/apps/web/core/components/analytics/work-items/utils.ts deleted file mode 100644 index 38535a1bf..000000000 --- a/apps/web/core/components/analytics/work-items/utils.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -// plane package imports -import type { ChartYAxisMetric, IState } from "@plane/types"; -import { ChartXAxisProperty } from "@plane/types"; - -interface ParamsProps { - x_axis: ChartXAxisProperty; - y_axis: ChartYAxisMetric; - group_by?: ChartXAxisProperty; -} - -export const generateBarColor = ( - value: string | null | undefined, - params: ParamsProps, - baseColors: string[], - workspaceStates?: IState[] -): string => { - if (!value) return baseColors[0]; - let color = baseColors[0]; - // Priority - if (params.x_axis === ChartXAxisProperty.PRIORITY) { - color = - value === "urgent" - ? "#ef4444" - : value === "high" - ? "#f97316" - : value === "medium" - ? "#eab308" - : value === "low" - ? "#22c55e" - : "#ced4da"; - } - - // State - if (params.x_axis === ChartXAxisProperty.STATES) { - if (workspaceStates && workspaceStates.length > 0) { - const state = workspaceStates.find((s) => s.id === value); - if (state) { - color = state.color; - } else { - const index = Math.abs(value.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)) % baseColors.length; - color = baseColors[index]; - } - } - } - - return color; -}; diff --git a/apps/web/core/components/analytics/work-items/workitems-insight-table.tsx b/apps/web/core/components/analytics/work-items/workitems-insight-table.tsx deleted file mode 100644 index 60dbef70d..000000000 --- a/apps/web/core/components/analytics/work-items/workitems-insight-table.tsx +++ /dev/null @@ -1,210 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import { useMemo } from "react"; -import type { ColumnDef, Row, RowData } from "@tanstack/react-table"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import useSWR from "swr"; -import { ProjectsOutline, UserOutline } from "@makeplane/propel/icons"; -import { Avatar } from "@makeplane/propel/components/avatar"; -import { useTranslation } from "@plane/i18n"; -import { Logo } from "@plane/propel/emoji-icon-picker"; -// plane package imports -import type { AnalyticsTableDataMap, WorkItemInsightColumns } from "@plane/types"; -// plane web components - -import { getFileURL } from "@plane/utils"; -// hooks -import { useAnalytics } from "@/hooks/store/use-analytics"; -import { useProject } from "@/hooks/store/use-project"; -import { AnalyticsService } from "@/services/analytics.service"; -// plane web components -import { exportCSV } from "../export"; -import { InsightTable } from "../insight-table"; - -const analyticsService = new AnalyticsService(); - -declare module "@tanstack/react-table" { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface ColumnMeta { - export: { - key: string; - value: (row: Row) => string | number; - label?: string; - }; - } -} - -const WorkItemsInsightTable = observer(function WorkItemsInsightTable() { - // router - const params = useParams(); - const workspaceSlug = params.workspaceSlug.toString(); - const { t } = useTranslation(); - // store hooks - const { getProjectById } = useProject(); - const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView, isEpic } = useAnalytics(); - const { data: workItemsData, isLoading } = useSWR( - `insights-table-work-items-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}-${isEpic}`, - () => - analyticsService.getAdvanceAnalyticsStats( - workspaceSlug, - "work-items", - { - // date_filter: selectedDuration, - ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), - ...(selectedCycle ? { cycle_id: selectedCycle } : {}), - ...(selectedModule ? { module_id: selectedModule } : {}), - ...(isEpic ? { epic: true } : {}), - }, - isPeekView - ) - ); - // derived values - const columnsLabels: Record, string> = - useMemo( - () => ({ - backlog_work_items: t("workspace_projects.state.backlog"), - started_work_items: t("workspace_projects.state.started"), - un_started_work_items: t("workspace_projects.state.unstarted"), - completed_work_items: t("workspace_projects.state.completed"), - cancelled_work_items: t("workspace_projects.state.cancelled"), - project__name: t("common.project"), - display_name: t("common.assignee"), - }), - [t] - ); - const columns: ColumnDef[] = useMemo( - () => [ - !isPeekView - ? { - accessorKey: "project__name", - header: () =>
{columnsLabels["project__name"]}
, - cell: ({ row }) => { - const project = getProjectById(row.original.project_id); - return ( -
- {project?.logo_props ? ( - - ) : ( - - )} - {project?.name} -
- ); - }, - meta: { - export: { - key: columnsLabels["project__name"], - value: (row) => row.original.project__name?.toString() ?? "", - }, - }, - } - : { - accessorKey: "display_name", - header: () =>
{columnsLabels["display_name"]}
, - cell: ({ row }: { row: Row }) => ( -
-
- {row.original.avatar_url && row.original.avatar_url !== "" ? ( - - ) : ( -
- {row.original.display_name ? ( - row.original.display_name?.[0] - ) : ( - - )} -
- )} - {row.original.display_name ?? t(`Unassigned`)} -
-
- ), - meta: { - export: { - key: columnsLabels["display_name"], - value: (row) => row.original.display_name?.toString() ?? "", - }, - }, - }, - { - accessorKey: "backlog_work_items", - header: () =>
{columnsLabels["backlog_work_items"]}
, - cell: ({ row }) =>
{row.original.backlog_work_items}
, - meta: { - export: { - key: columnsLabels["backlog_work_items"], - value: (row) => row.original.backlog_work_items.toString(), - }, - }, - }, - { - accessorKey: "started_work_items", - header: () =>
{columnsLabels["started_work_items"]}
, - cell: ({ row }) =>
{row.original.started_work_items}
, - meta: { - export: { - key: columnsLabels["started_work_items"], - value: (row) => row.original.started_work_items.toString(), - }, - }, - }, - { - accessorKey: "un_started_work_items", - header: () =>
{columnsLabels["un_started_work_items"]}
, - cell: ({ row }) =>
{row.original.un_started_work_items}
, - meta: { - export: { - key: columnsLabels["un_started_work_items"], - value: (row) => row.original.un_started_work_items.toString(), - }, - }, - }, - { - accessorKey: "completed_work_items", - header: () =>
{columnsLabels["completed_work_items"]}
, - cell: ({ row }) =>
{row.original.completed_work_items}
, - meta: { - export: { - key: columnsLabels["completed_work_items"], - value: (row) => row.original.completed_work_items.toString(), - }, - }, - }, - { - accessorKey: "cancelled_work_items", - header: () =>
{columnsLabels["cancelled_work_items"]}
, - cell: ({ row }) =>
{row.original.cancelled_work_items}
, - meta: { - export: { - key: columnsLabels["cancelled_work_items"], - value: (row) => row.original.cancelled_work_items.toString(), - }, - }, - }, - ], - [columnsLabels, getProjectById, isPeekView, t] - ); - return ( - - analyticsType="work-items" - data={workItemsData} - isLoading={isLoading} - columns={columns} - columnsLabels={columnsLabels} - headerText={isPeekView ? t("common.assignee") : t("common.projects")} - onExport={(rows) => workItemsData && exportCSV(rows, columns, workspaceSlug)} - /> - ); -}); - -export default WorkItemsInsightTable; diff --git a/apps/web/core/components/api-token/delete-token-modal.tsx b/apps/web/core/components/api-token/delete-token-modal.tsx deleted file mode 100644 index 58a93394c..000000000 --- a/apps/web/core/components/api-token/delete-token-modal.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/** - * 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 { mutate } from "swr"; -// types -import { useTranslation } from "@plane/i18n"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { APITokenService } from "@plane/services"; -import type { IApiToken } from "@plane/types"; -// ui -import { AlertModalCore } from "@plane/ui"; -// fetch-keys -import { API_TOKENS_LIST } from "@plane/constants"; - -type Props = { - isOpen: boolean; - onClose: () => void; - tokenId: string; -}; - -const apiTokenService = new APITokenService(); - -export function DeleteApiTokenModal(props: Props) { - const { isOpen, onClose, tokenId } = props; - // states - const [deleteLoading, setDeleteLoading] = useState(false); - // router params - const { t } = useTranslation(); - - const handleClose = () => { - onClose(); - setDeleteLoading(false); - }; - - const handleDeletion = async () => { - setDeleteLoading(true); - - await apiTokenService - .destroy(tokenId) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: t("workspace_settings.settings.api_tokens.delete.success.title"), - message: t("workspace_settings.settings.api_tokens.delete.success.message"), - }); - - mutate( - API_TOKENS_LIST, - (prevData) => (prevData ?? []).filter((token) => token.id !== tokenId), - false - ); - - handleClose(); - setDeleteLoading(false); - }) - .catch((err) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: t("workspace_settings.settings.api_tokens.delete.error.title"), - message: err?.message ?? t("workspace_settings.settings.api_tokens.delete.error.message"), - }); - setDeleteLoading(false); - }); - }; - - return ( - {t("workspace_settings.settings.api_tokens.delete.description")} } - /> - ); -} diff --git a/apps/web/core/components/api-token/empty-state.tsx b/apps/web/core/components/api-token/empty-state.tsx deleted file mode 100644 index 944d19e34..000000000 --- a/apps/web/core/components/api-token/empty-state.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2023-present Plane Software, Inc. and contributors - * SPDX-License-Identifier: AGPL-3.0-only - * See the LICENSE file for details. - */ - -import React from "react"; -// ui -import { Button } from "@plane/propel/button"; -// assets -import emptyApiTokens from "@/app/assets/empty-state/api-token.svg?url"; - -type Props = { - onClick: () => void; -}; - -export function ApiTokenEmptyState(props: Props) { - const { onClick } = props; - - return ( -
-
- empty -
No API tokens
-

- Create API tokens for safe and easy data sharing with external apps, maintaining control and security. -

- -
-
- ); -} diff --git a/apps/web/core/components/api-token/modal/create-token-modal.tsx b/apps/web/core/components/api-token/modal/create-token-modal.tsx deleted file mode 100644 index f9d4872d3..000000000 --- a/apps/web/core/components/api-token/modal/create-token-modal.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/** - * 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 { mutate } from "swr"; -// plane imports -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { APITokenService } from "@plane/services"; -import type { IApiToken } from "@plane/types"; -import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; -import { renderFormattedDate, csvDownload } from "@plane/utils"; -// constants -import { API_TOKENS_LIST } from "@plane/constants"; -// local imports -import { CreateApiTokenForm } from "./form"; -import { GeneratedTokenDetails } from "./generated-token-details"; - -type Props = { - isOpen: boolean; - onClose: () => void; -}; - -// services -const apiTokenService = new APITokenService(); - -export function CreateApiTokenModal(props: Props) { - const { isOpen, onClose } = props; - // states - const [neverExpires, setNeverExpires] = useState(false); - const [generatedToken, setGeneratedToken] = useState(null); - - const handleClose = () => { - onClose(); - - setTimeout(() => { - setNeverExpires(false); - setGeneratedToken(null); - }, 350); - }; - - const downloadSecretKey = (data: IApiToken) => { - const csvData = { - Title: data.label, - Description: data.description, - Expiry: data.expired_at ? (renderFormattedDate(data.expired_at)?.replace(",", " ") ?? "") : "Never expires", - "Secret key": data.token ?? "", - }; - - csvDownload(csvData, `secret-key-${Date.now()}`); - }; - - const handleCreateToken = async (data: Partial) => { - // make the request to generate the token - await apiTokenService - .create(data) - .then((res) => { - setGeneratedToken(res); - downloadSecretKey(res); - - mutate( - API_TOKENS_LIST, - (prevData) => { - if (!prevData) return; - - return [res, ...prevData]; - }, - false - ); - }) - .catch((err) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: err.message || err.detail, - }); - - throw err; - }); - }; - - return ( - {}} position={EModalPosition.TOP} width={EModalWidth.XXL}> - {generatedToken ? ( - - ) : ( - setNeverExpires((prevData) => !prevData)} - onSubmit={handleCreateToken} - /> - )} - - ); -} diff --git a/apps/web/core/components/api-token/modal/form.tsx b/apps/web/core/components/api-token/modal/form.tsx deleted file mode 100644 index 3cd990300..000000000 --- a/apps/web/core/components/api-token/modal/form.tsx +++ /dev/null @@ -1,279 +0,0 @@ -/** - * 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 { add } from "date-fns"; -import { Controller, useForm } from "react-hook-form"; -import { CalendarOutline } from "@makeplane/propel/icons"; -// types -import { Field } from "@makeplane/propel/components/field"; -import { Input, InputGroup } from "@makeplane/propel/components/input"; -import { TextArea, TextAreaGroup } from "@makeplane/propel/components/text-area"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import type { IApiToken } from "@plane/types"; -// ui -import { Switch } from "@makeplane/propel/components/switch"; -import { CustomSelect } from "@plane/ui"; -import { cn, renderFormattedDate, renderFormattedTime } from "@plane/utils"; -// components -import { DateDropdown } from "@/components/dropdowns/date"; -// helpers -type Props = { - handleClose: () => void; - neverExpires: boolean; - toggleNeverExpires: () => void; - onSubmit: (data: Partial) => Promise; -}; - -const EXPIRY_DATE_OPTIONS = [ - { - key: "1_week", - label: "1 week", - value: { weeks: 1 }, - }, - { - key: "1_month", - label: "1 month", - value: { months: 1 }, - }, - { - key: "3_months", - label: "3 months", - value: { months: 3 }, - }, - { - key: "1_year", - label: "1 year", - value: { years: 1 }, - }, -]; - -const defaultValues: Partial = { - label: "", - description: "", - expired_at: null, -}; - -const getExpiryDate = (val: string): Date | null | undefined => { - const today = new Date(); - const dateToAdd = EXPIRY_DATE_OPTIONS.find((option) => option.key === val)?.value; - if (dateToAdd) return add(today, dateToAdd); - return null; -}; - -const getFormattedDate = (date: Date): Date => { - const now = new Date(); - const hours = now.getHours(); - const minutes = now.getMinutes(); - const seconds = now.getSeconds(); - return add(date, { hours, minutes, seconds }); -}; - -export function CreateApiTokenForm(props: Props) { - const { handleClose, neverExpires, toggleNeverExpires, onSubmit } = props; - // states - const [customDate, setCustomDate] = useState(null); - // form - const { - control, - formState: { errors, isSubmitting }, - handleSubmit, - reset, - watch, - } = useForm({ defaultValues }); - // hooks - const { t } = useTranslation(); - - const handleFormSubmit = async (data: IApiToken) => { - // if never expires is toggled off, and the user has not selected a custom date or a predefined date, show an error - if (!neverExpires && (!data.expired_at || (data.expired_at === "custom" && !customDate))) - return setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Please select an expiration date.", - }); - - const payload: Partial = { - label: data.label, - description: data.description, - }; - - // if never expires is toggled on, set expired_at to null - if (neverExpires) payload.expired_at = null; - // if never expires is toggled off, and the user has selected a custom date, set expired_at to the custom date - else if (data.expired_at === "custom") { - payload.expired_at = customDate && getFormattedDate(customDate).toISOString(); - } - // if never expires is toggled off, and the user has selected a predefined date, set expired_at to the predefined date - else { - const expiryDate = getExpiryDate(data.expired_at ?? ""); - if (expiryDate) payload.expired_at = expiryDate.toISOString(); - } - - await onSubmit(payload).then(() => { - reset(defaultValues); - setCustomDate(null); - }); - }; - - const today = new Date(); - const tomorrow = add(today, { days: 1 }); - const expiredAt = watch("expired_at"); - const expiryDate = getExpiryDate(expiredAt ?? ""); - const customDateFormatted = customDate && getFormattedDate(customDate); - - return ( -
-
-

- {t("workspace_settings.settings.api_tokens.create_token")} -

-
-
- val.trim() !== "" || t("title_is_required"), - }} - render={({ field: { value, onChange } }) => ( - - - - - - )} - /> - {errors.label && {errors.label.message}} -
- ( - - -