Compare commits

..

2 Commits

Author SHA1 Message Date
Aaryan Khandelwal f32458eb7b chore: add reload logic 2026-01-29 14:38:05 +05:30
Aaryan Khandelwal 5a31c6449d feat: comments draft 2026-01-29 13:29:56 +05:30
1453 changed files with 11318 additions and 7924 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
contact_links:
- name: Help and support
about: Reach out to us on our Forum or GitHub discussions.
about: Reach out to us on our Discord server or GitHub discussions.
- name: Dedicated support
url: mailto:support@plane.so
about: Write to us if you'd like dedicated support using Plane
-55
View File
@@ -1,55 +0,0 @@
version: 2
updates:
# JavaScript/TypeScript dependencies (pnpm monorepo root)
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "javascript"
groups:
minor-and-patch:
update-types:
- "minor"
- "patch"
# Python dependencies
- package-ecosystem: "pip"
directory: "/apps/api"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "python"
groups:
minor-and-patch:
update-types:
- "minor"
- "patch"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "github-actions"
groups:
actions:
patterns:
- "*"
# Docker - API
- package-ecosystem: "docker"
directory: "/apps/api"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "docker"
+26 -3
View File
@@ -20,20 +20,43 @@ jobs:
fail-fast: false
matrix:
language: ["python", "javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
@@ -117,54 +117,16 @@ jobs:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
# Lint check - no build dependency, OxLint is a standalone Rust binary
check-lint:
name: check:lint
runs-on: ubuntu-latest
timeout-minutes: 10
if: |
github.event.pull_request.draft == false &&
github.event.pull_request.requested_reviewers != null
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
TURBO_SCM_HEAD: ${{ github.sha }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 50
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v6
- name: Enable Corepack and pnpm
run: corepack enable pnpm
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run check:lint
run: pnpm turbo run check:lint --affected
# Type check depends on build artifacts
check-types:
name: check:types
# Lint and type checks depend on build artifacts
check:
name: ${{ matrix.task }}
runs-on: ubuntu-latest
needs: build
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
task: [check:lint, check:types]
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
TURBO_SCM_HEAD: ${{ github.sha }}
@@ -203,5 +165,5 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run check:types
run: pnpm turbo run check:types --affected
- name: Run ${{ matrix.task }}
run: pnpm turbo run ${{ matrix.task }} --affected
-53
View File
@@ -1,53 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "jsx-a11y", "import", "promise", "unicorn", "oxc"],
"categories": {
"correctness": "warn",
"suspicious": "warn",
"perf": "warn"
},
"env": {
"browser": true,
"node": true,
"es2024": true
},
"settings": {
"react": {
"version": "18.3"
},
"jsx-a11y": {
"polymorphicPropName": "as"
}
},
"ignorePatterns": [
".cache/**",
".next/**",
".react-router/**",
".storybook/**",
".turbo/**",
".vite/**",
"*.config.{js,mjs,cjs,ts}",
"build/**",
"coverage/**",
"dist/**",
"**/public/**",
"storybook-static/**"
],
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"unicorn/filename-case": "off",
"unicorn/no-null": "off",
"unicorn/prevent-abbreviations": "off",
"no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"ignoreRestSiblings": true
}
]
}
}
+6 -8
View File
@@ -1,11 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5",
"sortTailwindcss": {
"stylesheet": "packages/tailwind-config/index.css",
"functions": ["cn", "clsx", "cva"]
},
"$schema": "https://json.schemastore.org/prettierrc",
"overrides": [
{
"files": ["packages/codemods/**/*"],
@@ -13,5 +7,9 @@
"printWidth": 80
}
}
]
],
"plugins": ["@prettier/plugin-oxc"],
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
+3 -3
View File
@@ -5,7 +5,7 @@
- `pnpm dev` - Start all dev servers (web:3000, admin:3001)
- `pnpm build` - Build all packages and apps
- `pnpm check` - Run all checks (format, lint, types)
- `pnpm check:lint` - OxLint across all packages
- `pnpm check:lint` - ESLint across all packages
- `pnpm check:types` - TypeScript type checking
- `pnpm fix` - Auto-fix format and lint issues
- `pnpm turbo run <command> --filter=<package>` - Target specific package/app
@@ -15,8 +15,8 @@
- **Imports**: Use `workspace:*` for internal packages, `catalog:` for external deps
- **TypeScript**: Strict mode enabled, all files must be typed
- **Formatting**: oxfmt, run `pnpm fix:format`
- **Linting**: OxLint with shared `.oxlintrc.json` config
- **Formatting**: Prettier with Tailwind plugin, run `pnpm fix:format`
- **Linting**: ESLint with shared config, max warnings vary by package
- **Naming**: camelCase for variables/functions, PascalCase for components/types
- **Error Handling**: Use try-catch with proper error types, log errors appropriately
- **State Management**: MobX stores in `packages/shared-state`, reactive patterns
+2 -2
View File
@@ -91,7 +91,7 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
- All features or bug fixes must be tested by one or more specs (unit-tests).
- We lint with [OxLint](https://oxc.rs/docs/guide/usage/linter) using the shared `.oxlintrc.json` and format with [oxfmt](https://oxc.rs/docs/guide/usage/formatter) using `.oxfmtrc.json`.
- We lint with [ESLint 9](https://eslint.org/docs/latest/) using the shared `eslint.config.mjs` (type-aware via `typescript-eslint`) and format with [Prettier](https://prettier.io/) using `prettier.config.cjs`.
## Ways to contribute
@@ -244,4 +244,4 @@ Happy translating! 🌍✨
## Need help? Questions and suggestions
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Forum](https://forum.plane.so).
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Discord Server](https://discord.com/invite/A92xrEGCge).
+11 -4
View File
@@ -7,9 +7,16 @@
</p>
<p align="center"><b>Modern project management for all teams</b></p>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
<img alt="Discord online members" src="https://img.shields.io/discord/1031547764020084846?color=5865F2&label=Discord&style=for-the-badge" />
</a>
<img alt="Commit activity per month" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
</p>
<p align="center">
<a href="https://plane.so/"><b>Website</b></a> •
<a href="https://forum.plane.so"><b>Forum</b></a> •
<a href="https://github.com/makeplane/plane/releases"><b>Releases</b></a> •
<a href="https://twitter.com/planepowers"><b>Twitter</b></a> •
<a href="https://docs.plane.so/"><b>Documentation</b></a>
</p>
@@ -26,7 +33,7 @@
Meet [Plane](https://plane.so/), an open-source project management tool to track issues, run ~sprints~ cycles, and manage product roadmaps without the chaos of managing the tool itself. 🧘‍♀️
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Forum](https://forum.plane.so) or raise a GitHub issue. We read everything and respond to most.
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Discord](https://discord.com/invite/A92xrEGCge) or raise a GitHub issue. We read everything and respond to most.
## 🚀 Installation
@@ -129,7 +136,7 @@ Explore Plane's [product documentation](https://docs.plane.so/) and [developer d
## ❤️ Community
Join the Plane community on [GitHub Discussions](https://github.com/orgs/makeplane/discussions) and our [Forum](https://forum.plane.so). We follow a [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) in all our community channels.
Join the Plane community on [GitHub Discussions](https://github.com/orgs/makeplane/discussions) and our [Discord server](https://discord.com/invite/A92xrEGCge). We follow a [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) in all our community channels.
Feel free to ask questions, report bugs, participate in discussions, share ideas, request features, or showcase your projects. Wed love to hear from you!
@@ -145,7 +152,7 @@ There are many ways you can contribute to Plane:
- Report [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) or submit [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+).
- Review the [documentation](https://docs.plane.so/) and submit [pull requests](https://github.com/makeplane/docs) to improve it—whether it's fixing typos or adding new content.
- Talk or write about Plane or any other ecosystem integration and [let us know](https://forum.plane.so)!
- Talk or write about Plane or any other ecosystem integration and [let us know](https://discord.com/invite/A92xrEGCge)!
- Show your support by upvoting [popular feature requests](https://github.com/makeplane/plane/issues).
Please read [CONTRIBUTING.md](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md) for details on the process for submitting pull requests to us.
+1 -1
View File
@@ -13,7 +13,7 @@ RUN corepack enable pnpm
FROM base AS builder
RUN pnpm add -g turbo@2.8.12
RUN pnpm add -g turbo@2.6.3
COPY . .
+3 -3
View File
@@ -120,16 +120,16 @@ export function InstanceAIForm(props: IInstanceAIForm) {
</div>
</div>
<div className="flex flex-col items-start gap-4">
<div className="flex flex-col gap-4 items-start">
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving" : "Save changes"}
</Button>
<div className="relative inline-flex items-center gap-1.5 rounded-sm border border-accent-subtle bg-accent-subtle px-4 py-2 text-caption-sm-regular text-accent-secondary">
<div className="relative inline-flex items-center gap-1.5 rounded-sm border border-accent-subtle bg-accent-subtle px-4 py-2 text-caption-sm-regular text-accent-secondary ">
<Lightbulb className="size-4" />
<div>
If you have a preferred AI models vendor, please get in{" "}
<a className="font-medium underline" href="https://plane.so/contact">
<a className="underline font-medium" href="https://plane.so/contact">
touch with us.
</a>
</div>
+1 -1
View File
@@ -34,7 +34,7 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="40%" />
<div className="grid w-2/3 grid-cols-2 gap-x-8 gap-y-4">
<div className="w-2/3 grid grid-cols-2 gap-x-8 gap-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</div>
@@ -176,8 +176,8 @@ export function InstanceGiteaConfigForm(props: Props) {
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">Gitea-provided details for Plane</div>
{GITEA_FORM_FIELDS.map((field) => (
<ControllerInput
@@ -211,7 +211,7 @@ export function InstanceGiteaConfigForm(props: Props) {
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 pt-1.5 pb-4">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-1 rounded-lg">
<div className="pt-2 text-18 font-medium">Plane-provided details for Gitea</div>
{GITEA_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
@@ -197,8 +197,8 @@ export function InstanceGithubConfigForm(props: Props) {
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">GitHub-provided details for Plane</div>
{GITHUB_FORM_FIELDS.map((field) => (
<ControllerInput
@@ -231,24 +231,24 @@ export function InstanceGithubConfigForm(props: Props) {
</div>
</div>
</div>
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
<div className="pt-2 text-18 font-medium">Plane-provided details for GitHub</div>
<div className="flex flex-col gap-y-4">
{/* common service details */}
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 py-4">
<div className="flex flex-col gap-y-4 px-6 py-4 bg-layer-1 rounded-lg">
{GITHUB_COMMON_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
{/* web service details */}
<div className="flex flex-col overflow-hidden rounded-lg">
<div className="flex items-center gap-x-3 bg-layer-3 px-6 py-3 text-11 font-medium text-secondary uppercase">
<Monitor className="h-3 w-3" />
<div className="flex flex-col rounded-lg overflow-hidden">
<div className="px-6 py-3 bg-layer-3 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary">
<Monitor className="w-3 h-3" />
Web
</div>
<div className="flex flex-col gap-y-4 bg-layer-1 px-6 py-4">
<div className="px-6 py-4 flex flex-col gap-y-4 bg-layer-1">
{GITHUB_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -180,8 +180,8 @@ export function InstanceGitlabConfigForm(props: Props) {
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">GitLab-provided details for Plane</div>
{GITLAB_FORM_FIELDS.map((field) => (
<ControllerInput
@@ -215,7 +215,7 @@ export function InstanceGitlabConfigForm(props: Props) {
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-3 px-6 pt-1.5 pb-4">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-3 rounded-lg">
<div className="pt-2 text-18 font-medium">Plane-provided details for GitLab</div>
{GITLAB_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
@@ -185,8 +185,8 @@ export function InstanceGoogleConfigForm(props: Props) {
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-18 font-medium">Google-provided details for Plane</div>
{GOOGLE_FORM_FIELDS.map((field) => (
<ControllerInput
@@ -219,24 +219,24 @@ export function InstanceGoogleConfigForm(props: Props) {
</div>
</div>
</div>
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
<div className="pt-2 text-18 font-medium">Plane-provided details for Google</div>
<div className="flex flex-col gap-y-4">
{/* common service details */}
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 py-4">
<div className="flex flex-col gap-y-4 px-6 py-4 bg-layer-1 rounded-lg">
{GOOGLE_COMMON_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
{/* web service details */}
<div className="flex flex-col overflow-hidden rounded-lg">
<div className="flex items-center gap-x-3 bg-layer-3 px-6 py-3 text-11 font-medium text-secondary uppercase">
<Monitor className="h-3 w-3" />
<div className="flex flex-col rounded-lg overflow-hidden">
<div className="px-6 py-3 bg-layer-3 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary">
<Monitor className="w-3 h-3" />
Web
</div>
<div className="flex flex-col gap-y-4 bg-layer-1 px-6 py-4">
<div className="px-6 py-4 flex flex-col gap-y-4 bg-layer-1">
{GOOGLE_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
@@ -117,11 +117,11 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
>
{formattedConfig ? (
<div className="space-y-3">
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
<div className={cn("w-full flex items-center gap-14 rounded-sm")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="pb-1 text-16 font-medium">Allow anyone to sign up even without an invite</div>
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
<div className="text-16 font-medium pb-1">Allow anyone to sign up even without an invite</div>
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
Toggling this off will only let users sign up when they are invited.
</div>
</div>
@@ -143,7 +143,7 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
</div>
</div>
</div>
<div className="text-lg pt-6 font-medium">Available authentication modes</div>
<div className="text-lg font-medium pt-6">Available authentication modes</div>
{authenticationModes.map((method) => (
<AuthenticationMethodCard
key={method.key}
@@ -179,7 +179,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
</CustomSelect>
</div>
</div>
<div className="my-6 flex flex-col gap-6 border-t border-subtle pt-4">
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-subtle">
<div className="flex w-full max-w-xl flex-col gap-y-10 px-1">
<div className="mr-8 flex items-center gap-10 pt-4">
<div className="grow">
@@ -207,7 +207,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
</div>
</div>
</div>
<div className="flex max-w-4xl items-center gap-4 py-1">
<div className="flex max-w-4xl items-center py-1 gap-4">
<Button
variant="primary"
size="lg"
@@ -91,8 +91,8 @@ export function SendTestEmailModal(props: Props) {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative w-full transform rounded-lg bg-surface-1 p-5 px-4 text-left shadow-raised-200 transition-all sm:max-w-xl">
<h3 className="text-16 leading-6 font-medium text-primary">
<Dialog.Panel className="relative transform rounded-lg bg-surface-1 p-5 px-4 text-left shadow-raised-200 transition-all w-full sm:max-w-xl">
<h3 className="text-16 font-medium leading-6 text-primary">
{sendEmailStep === ESendEmailSteps.SEND_EMAIL
? "Send test email"
: sendEmailStep === ESendEmailSteps.SUCCESS
@@ -121,7 +121,7 @@ export function SendTestEmailModal(props: Props) {
</div>
)}
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-13">{error}</div>}
<div className="mt-5 flex items-center justify-end gap-2">
<div className="flex items-center gap-2 justify-end mt-5">
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={2}>
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Cancel" : "Close"}
</Button>
@@ -112,18 +112,18 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
</div>
<div className="space-y-6">
<div className="border-b border-subtle pb-1.5 text-16 font-medium text-primary">Chat + telemetry</div>
<div className="text-16 font-medium text-primary pb-1.5 border-b border-subtle">Chat + telemetry</div>
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
<div className="flex items-center gap-14">
<div className="flex grow items-center gap-4">
<div className="grow flex items-center gap-4">
<div className="shrink-0">
<div className="flex size-11 items-center justify-center rounded-lg bg-layer-1">
<div className="flex items-center justify-center size-11 bg-layer-1 rounded-lg">
<Telescope className="size-5 text-tertiary" />
</div>
</div>
<div className="grow">
<div className="text-13 leading-5 font-medium text-primary">Let Plane collect anonymous usage data</div>
<div className="text-11 leading-5 font-regular text-tertiary">
<div className="text-13 font-medium text-primary leading-5">Let Plane collect anonymous usage data</div>
<div className="text-11 font-regular text-tertiary leading-5">
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
in line with{" "}
<a
@@ -56,16 +56,16 @@ export const IntercomConfig = observer(function IntercomConfig(props: TIntercomC
return (
<>
<div className="flex items-center gap-14">
<div className="flex grow items-center gap-4">
<div className="grow flex items-center gap-4">
<div className="shrink-0">
<div className="flex size-11 items-center justify-center rounded-lg bg-layer-1">
<MessageSquare className="size-5 p-0.5 text-tertiary" />
<div className="flex items-center justify-center size-11 bg-layer-1 rounded-lg">
<MessageSquare className="size-5 text-tertiary p-0.5" />
</div>
</div>
<div className="grow">
<div className="text-13 leading-5 font-medium text-primary">Chat with us</div>
<div className="text-11 leading-5 font-regular text-tertiary">
<div className="text-13 font-medium text-primary leading-5">Chat with us</div>
<div className="text-11 font-regular text-tertiary leading-5">
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
automatically.
</div>
+2 -2
View File
@@ -11,7 +11,7 @@ import { Outlet } from "react-router";
// components
import { AdminHeader } from "@/components/common/header";
import { LogoSpinner } from "@/components/common/logo-spinner";
import { NewUserPopup } from "@/components/common/new-user-popup";
import { NewUserPopup } from "@/components/new-user-popup";
// hooks
import { useUser } from "@/hooks/store";
// local components
@@ -42,7 +42,7 @@ function AdminLayout(_props: Route.ComponentProps) {
<AdminSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
<AdminHeader />
<div className="vertical-scrollbar scrollbar-md h-full w-full overflow-hidden overflow-y-scroll">
<div className="h-full w-full overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md">
<Outlet />
</div>
</main>
@@ -39,14 +39,14 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
const getSidebarMenuItems = () => (
<Menu.Items
className={cn(
"shadow-lg absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-subtle rounded-md border border-subtle bg-surface-1 px-1 py-2 text-11 outline-none",
"absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-subtle rounded-md border border-subtle bg-surface-1 px-1 py-2 text-11 shadow-lg outline-none",
{
"left-4": isSidebarCollapsed,
}
)}
>
<div className="flex flex-col gap-2.5 pb-2">
<span className="truncate px-2 text-secondary">{currentUser?.email}</span>
<span className="px-2 text-secondary truncate">{currentUser?.email}</span>
</div>
<div className="py-2">
<Menu.Item
@@ -7,11 +7,11 @@
import { useState, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { HelpCircle, MessageSquare, MoveLeft } from "lucide-react";
import { HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
import { WEB_BASE_URL } from "@plane/constants";
// plane internal packages
import { GithubIcon, NewTabIcon, PageIcon } from "@plane/propel/icons";
import { DiscordIcon, GithubIcon, NewTabIcon, PageIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// hooks
@@ -25,9 +25,9 @@ const helpOptions = [
Icon: PageIcon,
},
{
name: "Join our Forum",
href: "https://forum.plane.so",
Icon: MessageSquare,
name: "Join our Discord",
href: "https://discord.com/invite/A92xrEGCge",
Icon: DiscordIcon,
},
{
name: "Report a bug",
@@ -50,9 +50,9 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
return (
<div
className={cn(
"flex h-14 w-full flex-shrink-0 items-center justify-between gap-1 self-baseline border-t border-subtle bg-surface-1 px-4",
"flex w-full items-center justify-between gap-1 self-baseline border-t border-subtle bg-surface-1 px-4 h-14 flex-shrink-0",
{
"h-auto flex-col py-1.5": isSidebarCollapsed,
"flex-col h-auto py-1.5": isSidebarCollapsed,
}
)}
>
@@ -60,7 +60,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
<Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<a
href={redirectionLink}
className={`relative flex items-center gap-1 rounded-sm bg-layer-1 px-2 py-1 text-body-xs-medium whitespace-nowrap text-secondary`}
className={`relative px-2 py-1 flex items-center gap-1 rounded-sm bg-layer-1 text-body-xs-medium text-secondary whitespace-nowrap`}
>
<NewTabIcon width={14} height={14} />
{!isSidebarCollapsed && "Redirect to Plane"}
@@ -101,9 +101,9 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
leaveTo="transform opacity-0 scale-95"
>
<div
className={`absolute bottom-2 z-[15] min-w-[10rem] ${
className={`absolute bottom-2 min-w-[10rem] z-[15] ${
isSidebarCollapsed ? "left-full" : "-left-[75px]"
} divide-y divide-subtle-1 rounded-sm bg-surface-1 p-1 whitespace-nowrap shadow-raised-100`}
} divide-y divide-subtle-1 whitespace-nowrap rounded-sm bg-surface-1 p-1 shadow-raised-100`}
ref={helpOptionsRef}
>
<div className="space-y-1 pb-2">
@@ -134,7 +134,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
);
})}
</div>
<div className="px-2 pt-2 pb-1 text-10">Version: v{instance?.current_version}</div>
<div className="px-2 pb-1 pt-2 text-10">Version: v{instance?.current_version}</div>
</div>
</Transition>
</div>
@@ -29,7 +29,7 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
};
return (
<div className="vertical-scrollbar flex scrollbar-sm h-full w-full flex-col gap-2.5 overflow-y-scroll px-4 py-4">
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 py-4">
{sidebarMenu.map((item, index) => {
const isActive = item.href === pathName || pathName?.includes(item.href);
return (
@@ -38,9 +38,9 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
<div
className={cn(
"group flex w-full items-center gap-3 rounded-md px-3 py-2 transition-colors outline-none",
"group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none transition-colors",
{
"!bg-layer-transparent-active text-primary": isActive,
"text-primary !bg-layer-transparent-active": isActive,
"text-secondary hover:bg-layer-transparent-hover active:bg-layer-transparent-active": !isActive,
},
isSidebarCollapsed ? "justify-center" : "w-[260px]"
@@ -48,7 +48,7 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
>
{<item.Icon className="h-4 w-4 flex-shrink-0" />}
{!isSidebarCollapsed && (
<div className="w-full">
<div className="w-full ">
<div className={cn(`text-body-xs-medium transition-colors`)}>{item.name}</div>
<div className={cn(`text-caption-sm-regular transition-colors`)}>{item.description}</div>
</div>
+7 -1
View File
@@ -44,7 +44,13 @@ export const AdminSidebar = observer(function AdminSidebar() {
return (
<div
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-subtle bg-surface-1 duration-300 md:relative ${isSidebarCollapsed ? "-ml-[290px]" : ""} sm:${isSidebarCollapsed ? "-ml-[290px]" : ""} md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} `}
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-subtle bg-surface-1 duration-300
fixed md:relative
${isSidebarCollapsed ? "-ml-[290px]" : ""}
sm:${isSidebarCollapsed ? "-ml-[290px]" : ""}
md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"}
lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"}
`}
>
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
<AdminSidebarDropdown />
@@ -14,7 +14,6 @@ import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { InstanceWorkspaceService } from "@plane/services";
import type { IWorkspace } from "@plane/types";
import { validateSlug, validateWorkspaceName } from "@plane/utils";
// components
import { CustomSelect, Input } from "@plane/ui";
// hooks
@@ -97,7 +96,14 @@ export function WorkspaceCreateForm() {
control={control}
name="name"
rules={{
validate: (value) => validateWorkspaceName(value, true),
required: "This is a required field.",
validate: (value) =>
/^[\w\s-]*$/.test(value) ||
`Workspaces names can contain only (" "), ( - ), ( _ ) and alphanumeric characters.`,
maxLength: {
value: 80,
message: "Limit your name to 80 characters.",
},
}}
render={({ field: { value, ref, onChange } }) => (
<Input
@@ -123,13 +129,17 @@ export function WorkspaceCreateForm() {
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">Set your workspace&apos;s URL</h4>
<div className="flex w-full items-center gap-0.5 rounded-md border-[0.5px] border-subtle px-3">
<span className="text-13 whitespace-nowrap text-secondary">{workspaceBaseURL}</span>
<div className="flex gap-0.5 w-full items-center rounded-md border-[0.5px] border-subtle px-3">
<span className="whitespace-nowrap text-13 text-secondary">{workspaceBaseURL}</span>
<Controller
control={control}
name="slug"
rules={{
validate: (value) => validateSlug(value),
required: "The URL is a required field.",
maxLength: {
value: 48,
message: "Limit your URL to 48 characters.",
},
}}
render={({ field: { onChange, value, ref } }) => (
<Input
@@ -188,7 +198,7 @@ export function WorkspaceCreateForm() {
</div>
</div>
</div>
<div className="flex max-w-4xl items-center gap-4 py-1">
<div className="flex max-w-4xl items-center py-1 gap-4">
<Button
variant="primary"
size="lg"
@@ -83,11 +83,11 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
>
<div className="space-y-3">
{formattedConfig ? (
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
<div className={cn("w-full flex items-center gap-14 rounded-sm")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="pb-1 text-16 font-medium">Prevent anyone else from creating a workspace.</div>
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
<div className="text-16 font-medium pb-1">Prevent anyone else from creating a workspace.</div>
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
Toggling this on will let only you create workspaces. You will have to invite users to new workspaces.
</div>
</div>
@@ -116,15 +116,15 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
)}
{workspaceLoader !== "init-loader" ? (
<>
<div className="flex items-center justify-between gap-2 pt-6">
<div className="pt-6 flex items-center justify-between gap-2">
<div className="flex flex-col items-start gap-x-2">
<div className="flex items-center gap-2 text-16 font-medium">
All workspaces on this instance <span className="text-tertiary"> {workspaceIds.length}</span>
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
<LoaderIcon className="h-4 w-4 animate-spin" />
<LoaderIcon className="w-4 h-4 animate-spin" />
)}
</div>
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
You can&apos;t yet delete workspaces and you can only go to the workspace if you are an Admin or a
Member.
</div>
@@ -149,7 +149,7 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
disabled={workspaceLoader === "pagination"}
>
Load more
{workspaceLoader === "pagination" && <LoaderIcon className="h-3 w-3 animate-spin" />}
{workspaceLoader === "pagination" && <LoaderIcon className="w-3 h-3 animate-spin" />}
</Button>
</div>
)}
+4 -4
View File
@@ -20,16 +20,16 @@ export function AuthBanner(props: TAuthBanner) {
if (!bannerData) return <></>;
return (
<div className="relative flex items-center gap-2 rounded-md border border-accent-strong/50 bg-accent-primary/10 p-2">
<div className="relative flex h-4 w-4 flex-shrink-0 items-center justify-center">
<div className="relative flex items-center p-2 rounded-md gap-2 border border-accent-strong/50 bg-accent-primary/10">
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
<Info size={16} className="text-accent-primary" />
</div>
<div className="w-full text-13 font-medium text-accent-primary">{bannerData?.message}</div>
<div
className="relative ml-auto flex h-6 w-6 cursor-pointer items-center justify-center rounded-xs text-accent-primary transition-all hover:bg-accent-primary/20"
className="relative ml-auto w-6 h-6 rounded-xs flex justify-center items-center transition-all cursor-pointer hover:bg-accent-primary/20 text-accent-primary"
onClick={() => handleBannerData && handleBannerData(undefined)}
>
<CloseIcon className="h-4 w-4 flex-shrink-0" />
<CloseIcon className="w-4 h-4 flex-shrink-0" />
</div>
</div>
);
+1 -1
View File
@@ -9,7 +9,7 @@ import { PlaneLockup } from "@plane/propel/icons";
export function AuthHeader() {
return (
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6">
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
<Link href="/">
<PlaneLockup height={20} width={95} className="text-primary" />
</Link>
+2 -2
View File
@@ -50,7 +50,7 @@ const errorCodeMessages: {
message: () => (
<div>
Admin user already exists.&nbsp;
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
@@ -62,7 +62,7 @@ const errorCodeMessages: {
message: () => (
<div>
Admin user does not exist.&nbsp;
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
+1 -1
View File
@@ -22,7 +22,7 @@ function RootLayout() {
}, [replace, isUserLoggedIn]);
return (
<div className="relative z-10 flex h-screen w-screen flex-col items-center overflow-hidden overflow-y-auto bg-surface-1 px-8 pt-6 pb-10">
<div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8 bg-surface-1">
<Outlet />
</div>
);
+1 -1
View File
@@ -22,7 +22,7 @@ function HomePage() {
// if instance is not fetched, show loading
if (!instance && !error) {
return (
<div className="flex h-screen w-full items-center justify-center">
<div className="flex items-center justify-center h-screen w-full">
<LogoSpinner />
</div>
);
+9 -9
View File
@@ -16,7 +16,7 @@ import { Input, Spinner } from "@plane/ui";
// components
import { Banner } from "@/components/common/banner";
// local components
import { FormHeader } from "@/components/instance/form-header";
import { FormHeader } from "../../../core/components/instance/form-header";
import { AuthBanner } from "./auth-banner";
import { AuthHeader } from "./auth-header";
import { authErrorHandler } from "./auth-helpers";
@@ -111,8 +111,8 @@ export function InstanceSignInForm() {
return (
<>
<AuthHeader />
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
<FormHeader
heading="Manage your Plane instance"
subHeading="Configure instance-wide settings to secure your instance"
@@ -134,7 +134,7 @@ export function InstanceSignInForm() {
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="email">
<label className="text-13 text-tertiary font-medium" htmlFor="email">
Email <span className="text-danger-primary">*</span>
</label>
<Input
@@ -146,13 +146,13 @@ export function InstanceSignInForm() {
placeholder="name@company.com"
value={formData.email}
onChange={(e) => handleFormChange("email", e.target.value)}
autoComplete="off"
autoComplete="on"
autoFocus
/>
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="password">
<label className="text-13 text-tertiary font-medium" htmlFor="password">
Password <span className="text-danger-primary">*</span>
</label>
<div className="relative">
@@ -165,12 +165,12 @@ export function InstanceSignInForm() {
placeholder="Enter your password"
value={formData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
autoComplete="off"
autoComplete="on"
/>
{showPassword ? (
<button
type="button"
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
@@ -178,7 +178,7 @@ export function InstanceSignInForm() {
) : (
<button
type="button"
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
+21
View File
@@ -4,10 +4,31 @@
* See the LICENSE file for details.
*/
import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
Sentry.init({
dsn: process.env.VITE_SENTRY_DSN,
environment: process.env.VITE_SENTRY_ENVIRONMENT,
sendDefaultPii: process.env.VITE_SENTRY_SEND_DEFAULT_PII ? process.env.VITE_SENTRY_SEND_DEFAULT_PII === "1" : false,
release: process.env.VITE_APP_VERSION,
tracesSampleRate: process.env.VITE_SENTRY_TRACES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_TRACES_SAMPLE_RATE)
: 0.1,
profilesSampleRate: process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE)
: 0.1,
replaysSessionSampleRate: process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE)
: 0.1,
replaysOnErrorSampleRate: process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE)
: 1.0,
integrations: [],
});
startTransition(() => {
hydrateRoot(
document,
+6 -1
View File
@@ -7,6 +7,7 @@
import type { ReactNode } from "react";
import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router";
import * as Sentry from "@sentry/react-router";
import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url";
import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url";
import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url";
@@ -74,7 +75,7 @@ export const meta: Route.MetaFunction = () => [
export default function Root() {
return (
<div className="min-h-screen bg-canvas">
<div className="bg-canvas min-h-screen">
<Outlet />
</div>
);
@@ -89,6 +90,10 @@ export function HydrateFallback() {
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (error) {
Sentry.captureException(error);
}
return (
<div>
<p>Something went wrong.</p>
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { enableStaticRendering } from "mobx-react";
// stores
import { CoreRootStore } from "@/store/root.store";
enableStaticRendering(typeof window === "undefined");
export class RootStore extends CoreRootStore {
constructor() {
super();
}
hydrate(initialData: any) {
super.hydrate(initialData);
}
resetOnSignOut() {
super.resetOnSignOut();
}
}
@@ -22,8 +22,8 @@ export function AuthenticationMethodCard(props: Props) {
return (
<div
className={cn("flex w-full items-center gap-14 rounded-lg bg-layer-2", {
"border border-subtle px-4 py-3": withBorder,
className={cn("w-full flex items-center gap-14 rounded-lg bg-layer-2", {
"px-4 py-3 border border-subtle": withBorder,
})}
>
<div
@@ -36,7 +36,7 @@ export function AuthenticationMethodCard(props: Props) {
</div>
<div className="grow">
<div
className={cn("leading-5 font-medium text-primary", {
className={cn("font-medium leading-5 text-primary", {
"text-13": withBorder,
"text-18": !withBorder,
})}
@@ -44,7 +44,7 @@ export function AuthenticationMethodCard(props: Props) {
{name}
</div>
<div
className={cn("leading-5 font-regular text-tertiary", {
className={cn("font-regular leading-5 text-tertiary", {
"text-11": withBorder,
"text-13": !withBorder,
})}
@@ -16,12 +16,12 @@ export function Banner(props: TBanner) {
return (
<div
className={`w-full rounded-md border p-2 ${type === "error" ? "border-danger-strong bg-danger-subtle" : "border-success-strong bg-success-subtle"}`}
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-danger-subtle border-danger-strong" : "bg-success-subtle border-success-strong"}`}
>
<div className="flex items-center justify-center">
<div className="flex-shrink-0">
{type === "error" ? (
<span className="flex h-6 w-6 items-center justify-center rounded-full">
<span className="flex items-center justify-center h-6 w-6 rounded-full">
<AlertCircle className="h-5 w-5 text-danger-primary" aria-hidden="true" />
</span>
) : (
@@ -22,12 +22,12 @@ export function BreadcrumbLink(props: Props) {
{href ? (
<Link className="flex items-center gap-1 text-13 font-medium text-tertiary hover:text-primary" href={href}>
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-16">{icon}</div>}
<div className="relative line-clamp-1 block max-w-[150px] truncate overflow-hidden">{label}</div>
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
</Link>
) : (
<div className="flex cursor-default items-center gap-1 text-13 font-medium text-primary">
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden">{icon}</div>}
<div className="relative line-clamp-1 block max-w-[150px] truncate overflow-hidden">{label}</div>
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
</div>
)}
</div>
@@ -16,9 +16,9 @@ export function CodeBlock({ children, className, darkerShade }: TProps) {
return (
<span
className={cn(
"rounded-md border border-subtle bg-surface-2 px-0.5 text-11 font-semibold text-tertiary",
"px-0.5 text-11 text-tertiary bg-surface-2 font-semibold rounded-md border border-subtle",
{
"border-subtle bg-layer-1 text-secondary": darkerShade,
"text-secondary bg-layer-1 border-subtle": darkerShade,
},
className
)}
@@ -46,10 +46,10 @@ export function ConfirmDiscardModal(props: Props) {
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]">
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-16 leading-6 font-medium text-tertiary">
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-tertiary">
You have unsaved changes
</Dialog.Title>
<div className="mt-2">
@@ -60,7 +60,7 @@ export function ConfirmDiscardModal(props: Props) {
</div>
</div>
</div>
<div className="flex items-center justify-end gap-2 p-4 sm:px-6">
<div className="flex justify-end items-center p-4 sm:px-6 gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Keep editing
</Button>
@@ -67,7 +67,7 @@ export function ControllerInput(props: Props) {
(showPassword ? (
<button
tabIndex={-1}
className="absolute top-2.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-2.5 flex items-center justify-center text-placeholder"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
@@ -75,7 +75,7 @@ export function ControllerInput(props: Props) {
) : (
<button
tabIndex={-1}
className="absolute top-2.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-2.5 flex items-center justify-center text-placeholder"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
@@ -25,7 +25,7 @@ export function EmptyState({ title, description, image, primaryButton, secondary
<div className={`flex h-full w-full items-center justify-center`}>
<div className="flex w-full flex-col items-center text-center">
{image && <img src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
<h6 className="mt-6 mb-3 text-18 font-semibold sm:mt-8">{title}</h6>
<h6 className="mb-3 mt-6 text-18 font-semibold sm:mt-8">{title}</h6>
{description && <p className="mb-7 px-5 text-tertiary sm:mb-8">{description}</p>}
<div className="flex items-center gap-4">
{primaryButton && (
@@ -10,7 +10,7 @@ import { Menu, Settings } from "lucide-react";
// icons
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "../breadcrumb-link";
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
// hooks
import { useTheme } from "@/hooks/store";
// local imports
@@ -21,10 +21,10 @@ export const HamburgerToggle = observer(function HamburgerToggle() {
const { isSidebarCollapsed, toggleSidebar } = useTheme();
return (
<button
className="group flex size-7 cursor-pointer items-center justify-center rounded-sm bg-layer-1 transition-all hover:bg-layer-1-hover md:hidden"
className="size-7 rounded-sm flex justify-center items-center bg-layer-1 transition-all hover:bg-layer-1-hover cursor-pointer group md:hidden"
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<Menu size={14} className="text-secondary transition-all group-hover:text-primary" />
<Menu size={14} className="text-secondary group-hover:text-primary transition-all" />
</button>
);
});
@@ -24,25 +24,25 @@ export const PageWrapper = (props: TPageWrapperProps) => {
return (
<div
className={cn("mx-auto h-full w-full space-y-6 py-4", {
"max-w-[1000px] md:px-4 2xl:max-w-[1200px]": size === "md",
className={cn("mx-auto w-full h-full space-y-6 py-4", {
"md:px-4 max-w-[1000px] 2xl:max-w-[1200px]": size === "md",
"px-4 lg:px-12": size === "lg",
})}
>
{customHeader ? (
<div className="mx-4 shrink-0 space-y-1 border-b border-subtle py-4">{customHeader}</div>
<div className="border-b border-subtle mx-4 py-4 space-y-1 shrink-0">{customHeader}</div>
) : (
header && (
<div className="mx-4 flex shrink-0 items-center justify-between gap-4 space-y-1 border-b border-subtle py-4">
<div className="flex items-center justify-between gap-4 border-b border-subtle mx-4 py-4 space-y-1 shrink-0">
<div className={header.actions ? "flex flex-col gap-1" : "space-y-1"}>
<div className="text-h5-semibold text-primary">{header.title}</div>
<div className="text-body-sm-regular text-secondary">{header.description}</div>
<div className="text-primary text-h5-semibold">{header.title}</div>
<div className="text-secondary text-body-sm-regular">{header.description}</div>
</div>
{header.actions && <div className="shrink-0">{header.actions}</div>}
</div>
)
)}
<div className="vertical-scrollbar scrollbar-sm flex-grow overflow-hidden overflow-y-scroll px-4 pb-4">
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 pb-4">
{children}
</div>
</div>
@@ -24,12 +24,12 @@ export const InstanceFailureView = observer(function InstanceFailureView() {
return (
<>
<AuthHeader />
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
<div className="relative flex flex-col items-center justify-center space-y-4">
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
<div className="relative flex flex-col justify-center items-center space-y-4">
<img src={instanceImage} alt="Instance failure illustration" />
<h3 className="text-center text-20 font-medium text-on-color">Unable to fetch instance details.</h3>
<p className="text-center text-14 font-medium">
<h3 className="font-medium text-20 text-on-color text-center">Unable to fetch instance details.</h3>
<p className="font-medium text-14 text-center">
We were unable to fetch the details of the instance. Fret not, it might just be a connectivity issue.
</p>
</div>
@@ -7,8 +7,8 @@
export function FormHeader({ heading, subHeading }: { heading: string; subHeading: string }) {
return (
<div className="flex flex-col gap-1">
<span className="text-20 leading-7 font-semibold text-primary">{heading}</span>
<span className="text-16 leading-7 font-semibold text-placeholder">{subHeading}</span>
<span className="text-20 font-semibold text-primary leading-7">{heading}</span>
<span className="text-16 font-semibold text-placeholder leading-7">{subHeading}</span>
</div>
);
}
@@ -11,12 +11,12 @@ import PlaneTakeOffImage from "@/app/assets/images/plane-takeoff.png?url";
export function InstanceNotReady() {
return (
<div className="relative container mx-auto flex h-full w-full items-center justify-center px-5">
<div className="relative w-auto max-w-2xl space-y-8 py-10">
<div className="relative flex flex-col items-center justify-center space-y-4">
<h1 className="pb-3 text-24 font-bold">Welcome aboard Plane!</h1>
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<h1 className="text-24 font-bold pb-3">Welcome aboard Plane!</h1>
<img src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="text-14 font-medium text-placeholder">Get started by setting up your instance and workspace</p>
<p className="font-medium text-14 text-placeholder">Get started by setting up your instance and workspace</p>
</div>
<div>
@@ -13,11 +13,11 @@ import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { AuthService } from "@plane/services";
import { Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
import { getPasswordStrength, validatePersonName, validateCompanyName } from "@plane/utils";
import { getPasswordStrength } from "@plane/utils";
// components
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
import { Banner } from "../common/banner";
import { FormHeader } from "./form-header";
import { Banner } from "@/components/common/banner";
import { FormHeader } from "@/components/instance/form-header";
// service initialization
const authService = new AuthService();
@@ -139,8 +139,8 @@ export function InstanceSetupForm() {
return (
<>
<AuthHeader />
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
<FormHeader
heading="Setup your Plane Instance"
subHeading="Post setup you will be able to manage this Plane instance."
@@ -160,9 +160,9 @@ export function InstanceSetupForm() {
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<input type="hidden" name="is_telemetry_enabled" value={formData.is_telemetry_enabled ? "True" : "False"} />
<div className="flex flex-col items-center gap-4 sm:flex-row">
<div className="flex flex-col sm:flex-row items-center gap-4">
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="first_name">
<label className="text-13 text-tertiary font-medium" htmlFor="first_name">
First name <span className="text-danger-primary">*</span>
</label>
<Input
@@ -173,19 +173,13 @@ export function InstanceSetupForm() {
inputSize="md"
placeholder="Wilber"
value={formData.first_name}
onChange={(e) => {
const validation = validatePersonName(e.target.value);
if (validation === true || e.target.value === "") {
handleFormChange("first_name", e.target.value);
}
}}
autoComplete="off"
onChange={(e) => handleFormChange("first_name", e.target.value)}
autoComplete="on"
autoFocus
maxLength={50}
/>
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="last_name">
<label className="text-13 text-tertiary font-medium" htmlFor="last_name">
Last name <span className="text-danger-primary">*</span>
</label>
<Input
@@ -196,20 +190,14 @@ export function InstanceSetupForm() {
inputSize="md"
placeholder="Wright"
value={formData.last_name}
onChange={(e) => {
const validation = validatePersonName(e.target.value);
if (validation === true || e.target.value === "") {
handleFormChange("last_name", e.target.value);
}
}}
autoComplete="off"
maxLength={50}
onChange={(e) => handleFormChange("last_name", e.target.value)}
autoComplete="on"
/>
</div>
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="email">
<label className="text-13 text-tertiary font-medium" htmlFor="email">
Email <span className="text-danger-primary">*</span>
</label>
<Input
@@ -222,7 +210,7 @@ export function InstanceSetupForm() {
value={formData.email}
onChange={(e) => handleFormChange("email", e.target.value)}
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false}
autoComplete="off"
autoComplete="on"
/>
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
<p className="px-1 text-11 text-danger-primary">{errorData.message}</p>
@@ -230,7 +218,7 @@ export function InstanceSetupForm() {
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="company_name">
<label className="text-13 text-tertiary font-medium" htmlFor="company_name">
Company name <span className="text-danger-primary">*</span>
</label>
<Input
@@ -241,18 +229,12 @@ export function InstanceSetupForm() {
inputSize="md"
placeholder="Company name"
value={formData.company_name}
onChange={(e) => {
const validation = validateCompanyName(e.target.value, false);
if (validation === true || e.target.value === "") {
handleFormChange("company_name", e.target.value);
}
}}
maxLength={80}
onChange={(e) => handleFormChange("company_name", e.target.value)}
/>
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="password">
<label className="text-13 text-tertiary font-medium" htmlFor="password">
Set a password <span className="text-danger-primary">*</span>
</label>
<div className="relative">
@@ -268,13 +250,13 @@ export function InstanceSetupForm() {
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="new-password"
autoComplete="on"
/>
{showPassword.password ? (
<button
type="button"
tabIndex={-1}
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => handleShowPassword("password")}
>
<EyeOff className="h-4 w-4" />
@@ -283,7 +265,7 @@ export function InstanceSetupForm() {
<button
type="button"
tabIndex={-1}
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => handleShowPassword("password")}
>
<Eye className="h-4 w-4" />
@@ -297,7 +279,7 @@ export function InstanceSetupForm() {
</div>
<div className="w-full space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="confirm_password">
<label className="text-13 text-tertiary font-medium" htmlFor="confirm_password">
Confirm password <span className="text-danger-primary">*</span>
</label>
<div className="relative">
@@ -312,13 +294,12 @@ export function InstanceSetupForm() {
className="w-full border border-subtle !bg-surface-1 pr-12 placeholder:text-placeholder"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
autoComplete="new-password"
/>
{showPassword.retypePassword ? (
<button
type="button"
tabIndex={-1}
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => handleShowPassword("retypePassword")}
>
<EyeOff className="h-4 w-4" />
@@ -327,7 +308,7 @@ export function InstanceSetupForm() {
<button
type="button"
tabIndex={-1}
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
onClick={() => handleShowPassword("retypePassword")}
>
<Eye className="h-4 w-4" />
@@ -344,21 +325,21 @@ export function InstanceSetupForm() {
<div className="relative flex gap-2">
<div>
<Checkbox
className="h-4 w-4"
className="w-4 h-4"
iconClassName="w-3 h-3"
id="is_telemetry_enabled"
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
checked={formData.is_telemetry_enabled}
/>
</div>
<label className="cursor-pointer text-13 font-medium text-tertiary" htmlFor="is_telemetry_enabled">
<label className="text-13 text-tertiary font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
Allow Plane to anonymously collect usage events.{" "}
<a
tabIndex={-1}
href="https://developers.plane.so/self-hosting/telemetry"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-600 flex-shrink-0 text-13 font-medium"
className="text-13 font-medium text-blue-500 hover:text-blue-600 flex-shrink-0"
>
See More
</a>
@@ -24,7 +24,7 @@ export const NewUserPopup = observer(function NewUserPopup() {
if (!isNewUserPopup) return <></>;
return (
<div className="shadow-md absolute right-8 bottom-8 w-96 rounded-lg border border-subtle bg-surface-1 p-6">
<div className="absolute bottom-8 right-8 p-6 w-96 border border-subtle shadow-md rounded-lg bg-surface-1">
<div className="flex gap-4">
<div className="grow">
<div className="text-14 font-semibold">Create workspace</div>
@@ -41,7 +41,7 @@ export const NewUserPopup = observer(function NewUserPopup() {
</Button>
</div>
</div>
<div className="flex shrink-0 items-center justify-center">
<div className="shrink-0 flex items-center justify-center">
<img
src={resolveGeneralTheme(resolvedTheme) === "dark" ? TakeoffIconDark : TakeoffIconLight}
height={80}
@@ -30,19 +30,19 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
key={workspaceId}
href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`}
target="_blank"
className="group flex items-center justify-between gap-2.5 truncate rounded-lg border border-subtle bg-layer-1 p-3 hover:border-subtle-1 hover:bg-layer-1-hover hover:shadow-raised-100"
className="group flex items-center justify-between p-3 gap-2.5 truncate border border-subtle hover:border-subtle-1 bg-layer-1 hover:bg-layer-1-hover hover:shadow-raised-100 rounded-lg"
rel="noreferrer"
>
<div className="flex items-start gap-4">
<span
className={`relative mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 text-11 uppercase ${
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 mt-1 text-11 uppercase ${
!workspace?.logo_url && "rounded-lg bg-accent-primary text-on-color"
}`}
>
{workspace?.logo_url && workspace.logo_url !== "" ? (
<img
src={getFileURL(workspace.logo_url)}
className="absolute top-0 left-0 h-full w-full rounded-sm object-cover"
className="absolute left-0 top-0 h-full w-full rounded-sm object-cover"
alt="Workspace Logo"
/>
) : (
@@ -50,7 +50,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
)}
</span>
<div className="flex flex-col items-start gap-1">
<div className="flex w-full flex-wrap items-center gap-2.5">
<div className="flex flex-wrap w-full items-center gap-2.5">
<h3 className={`text-14 font-medium capitalize`}>{workspace.name}</h3>/
<Tooltip tooltipContent="The unique URL of your workspace">
<h4 className="text-13 text-tertiary">[{workspace.slug}]</h4>
@@ -58,14 +58,14 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
</div>
{workspace.owner.email && (
<div className="flex items-center gap-1 text-11">
<h3 className="font-medium text-secondary">Owned by:</h3>
<h3 className="text-secondary font-medium">Owned by:</h3>
<h4 className="text-tertiary">{workspace.owner.email}</h4>
</div>
)}
<div className="flex items-center gap-2.5 text-11">
{workspace.total_projects !== null && (
<span className="flex items-center gap-1">
<h3 className="font-medium text-secondary">Total projects:</h3>
<h3 className="text-secondary font-medium">Total projects:</h3>
<h4 className="text-tertiary">{workspace.total_projects}</h4>
</span>
)}
@@ -73,7 +73,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
<>
<span className="flex items-center gap-1">
<h3 className="font-medium text-secondary">Total members:</h3>
<h3 className="text-secondary font-medium">Total members:</h3>
<h4 className="text-tertiary">{workspace.total_members}</h4>
</span>
</>
@@ -6,7 +6,7 @@
import { createContext } from "react";
// plane admin store
import { RootStore } from "../store/root.store";
import { RootStore } from "@/plane-admin/store/root.store";
let rootStore = new RootStore();
@@ -19,7 +19,7 @@ import type {
IInstanceConfig,
} from "@plane/types";
// root store
import type { RootStore } from "@/store/root.store";
import type { CoreRootStore } from "@/store/root.store";
export interface IInstanceStore {
// issues
@@ -53,7 +53,7 @@ export class InstanceStore implements IInstanceStore {
// service
instanceService;
constructor(private store: RootStore) {
constructor(private store: CoreRootStore) {
makeObservable(this, {
// observable
isLoading: observable.ref,
@@ -17,7 +17,7 @@ import { WorkspaceStore } from "./workspace.store";
enableStaticRendering(typeof window === "undefined");
export class RootStore {
export abstract class CoreRootStore {
theme: IThemeStore;
instance: IInstanceStore;
user: IUserStore;
@@ -6,7 +6,7 @@
import { action, observable, makeObservable } from "mobx";
// root store
import type { RootStore } from "./root.store";
import type { CoreRootStore } from "@/store/root.store";
type TTheme = "dark" | "light";
export interface IThemeStore {
@@ -27,7 +27,7 @@ export class ThemeStore implements IThemeStore {
isSidebarCollapsed: boolean | undefined = undefined;
theme: string | undefined = undefined;
constructor(private store: RootStore) {
constructor(private store: CoreRootStore) {
makeObservable(this, {
// observables
isNewUserPopup: observable.ref,
@@ -11,7 +11,7 @@ import { EUserStatus } from "@plane/constants";
import { AuthService, UserService } from "@plane/services";
import type { IUser } from "@plane/types";
// root store
import type { RootStore } from "@/store/root.store";
import type { CoreRootStore } from "@/store/root.store";
export interface IUserStore {
// observables
@@ -36,7 +36,7 @@ export class UserStore implements IUserStore {
userService;
authService;
constructor(private store: RootStore) {
constructor(private store: CoreRootStore) {
makeObservable(this, {
// observables
isLoading: observable.ref,
@@ -10,7 +10,7 @@ import { action, observable, runInAction, makeObservable, computed } from "mobx"
import { InstanceWorkspaceService } from "@plane/services";
import type { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
// root store
import type { RootStore } from "@/store/root.store";
import type { CoreRootStore } from "@/store/root.store";
export interface IWorkspaceStore {
// observables
@@ -37,7 +37,7 @@ export class WorkspaceStore implements IWorkspaceStore {
// services
instanceWorkspaceService;
constructor(private store: RootStore) {
constructor(private store: CoreRootStore) {
makeObservable(this, {
// observables
loader: observable,
+7
View File
@@ -0,0 +1,7 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export * from "ce/store/root.store";
+7 -6
View File
@@ -1,9 +1,9 @@
{
"name": "admin",
"version": "1.2.0",
"private": true,
"description": "Admin UI for Plane",
"version": "1.2.0",
"license": "AGPL-3.0",
"private": true,
"type": "module",
"scripts": {
"dev": "react-router dev --port 3001",
@@ -11,11 +11,11 @@
"preview": "react-router build && serve -s build/client -l 3001",
"start": "serve -s build/client -l 3001",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist && rm -rf build",
"check:lint": "oxlint --max-warnings=759 .",
"check:lint": "eslint . --cache --cache-location node_modules/.cache/eslint/ --max-warnings=485",
"check:types": "react-router typegen && tsc --noEmit",
"check:format": "oxfmt --check .",
"fix:lint": "oxlint --fix .",
"fix:format": "oxfmt ."
"check:format": "prettier . --cache --check",
"fix:lint": "eslint . --cache --cache-location node_modules/.cache/eslint/ --fix --max-warnings=485",
"fix:format": "prettier . --cache --write"
},
"dependencies": {
"@bprogress/core": "catalog:",
@@ -31,6 +31,7 @@
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@react-router/node": "catalog:",
"@sentry/react-router": "catalog:",
"@tanstack/react-virtual": "^3.13.12",
"@tanstack/virtual-core": "^3.13.12",
"axios": "catalog:",
+6 -1
View File
@@ -9,7 +9,12 @@
"types": ["vite/client"],
"paths": {
"package.json": ["./package.json"],
"@/*": ["./*"]
"ce/*": ["./ce/*"],
"@/app/*": ["./app/*"],
"@/*": ["./core/*"],
"@/plane-admin/*": ["./ce/*"],
"@/ce/*": ["./ce/*"],
"@/styles/*": ["./styles/*"]
}
},
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
-31
View File
@@ -6,10 +6,6 @@
import random
from rest_framework import serializers
# Python imports
import re
# Module imports
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate
@@ -105,15 +101,6 @@ class ProjectCreateSerializer(BaseSerializer):
]
def validate(self, data):
project_name = data.get("name", None)
project_identifier = data.get("identifier", None)
if project_name is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_name):
raise serializers.ValidationError("Project name cannot contain special characters.")
if project_identifier is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_identifier):
raise serializers.ValidationError("Project identifier cannot contain special characters.")
if data.get("project_lead", None) is not None:
# Check if the project lead is a member of the workspace
if not WorkspaceMember.objects.filter(
@@ -173,15 +160,6 @@ class ProjectUpdateSerializer(ProjectCreateSerializer):
read_only_fields = ProjectCreateSerializer.Meta.read_only_fields
def update(self, instance, validated_data):
project_name = validated_data.get("name", None)
project_identifier = validated_data.get("identifier", None)
if project_name is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_name):
raise serializers.ValidationError("Project name cannot contain special characters.")
if project_identifier is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_identifier):
raise serializers.ValidationError("Project identifier cannot contain special characters.")
"""Update a project"""
if (
validated_data.get("default_state", None) is not None
@@ -232,15 +210,6 @@ class ProjectSerializer(BaseSerializer):
]
def validate(self, data):
project_name = data.get("name", None)
project_identifier = data.get("identifier", None)
if project_name is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_name):
raise serializers.ValidationError("Project name cannot contain special characters.")
if project_identifier is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_identifier):
raise serializers.ValidationError("Project identifier cannot contain special characters.")
# Check project lead should be a member of the workspace
if (
data.get("project_lead", None) is not None
-6
View File
@@ -8,7 +8,6 @@ from plane.api.views import (
ProjectListCreateAPIEndpoint,
ProjectDetailAPIEndpoint,
ProjectArchiveUnarchiveAPIEndpoint,
ProjectSummaryAPIEndpoint,
)
urlpatterns = [
@@ -27,9 +26,4 @@ urlpatterns = [
ProjectArchiveUnarchiveAPIEndpoint.as_view(http_method_names=["post", "delete"]),
name="project-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/summary/",
ProjectSummaryAPIEndpoint.as_view(http_method_names=["get"]),
name="project-summary",
),
]
-1
View File
@@ -6,7 +6,6 @@ from .project import (
ProjectListCreateAPIEndpoint,
ProjectDetailAPIEndpoint,
ProjectArchiveUnarchiveAPIEndpoint,
ProjectSummaryAPIEndpoint,
)
from .state import (
+1 -1
View File
@@ -414,7 +414,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
{"error": "Archived module cannot be edited"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ModuleUpdateSerializer(module, data=request.data, context={"project_id": project_id}, partial=True)
serializer = ModuleSerializer(module, data=request.data, context={"project_id": project_id}, partial=True)
if serializer.is_valid():
if (
request.data.get("external_id")
+6 -127
View File
@@ -7,8 +7,7 @@ import json
# Django imports
from django.db import IntegrityError
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery, Count
from django.db.models.functions import Coalesce
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
@@ -23,6 +22,7 @@ from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
from plane.db.models import (
Cycle,
Intake,
ProjectUserProperty,
Module,
Project,
DeployBoard,
@@ -31,11 +31,6 @@ from plane.db.models import (
DEFAULT_STATES,
Workspace,
UserFavorite,
Label,
Issue,
StateGroup,
IntakeIssue,
ProjectPage,
)
from plane.bgtasks.webhook_task import model_activity, webhook_activity
from .base import BaseAPIView
@@ -45,7 +40,7 @@ from plane.api.serializers import (
ProjectCreateSerializer,
ProjectUpdateSerializer,
)
from plane.app.permissions import ProjectBasePermission, WorkSpaceAdminPermission
from plane.app.permissions import ProjectBasePermission
from plane.utils.openapi import (
project_docs,
PROJECT_ID_PARAMETER,
@@ -188,9 +183,9 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
return self.paginate(
request=request,
queryset=(projects),
on_results=lambda projects: (
ProjectSerializer(projects, many=True, fields=self.fields, expand=self.expand).data
),
on_results=lambda projects: ProjectSerializer(
projects, many=True, fields=self.fields, expand=self.expand
).data,
)
@project_docs(
@@ -554,119 +549,3 @@ class ProjectArchiveUnarchiveAPIEndpoint(BaseAPIView):
project.archived_at = None
project.save()
return Response(status=status.HTTP_204_NO_CONTENT)
ALLOWED_PROJECT_SUMMARY_FIELDS = [
"members",
"states",
"labels",
"cycles",
"modules",
"issues",
"intakes",
"pages",
]
class ProjectSummaryAPIEndpoint(BaseAPIView):
permission_classes = [WorkSpaceAdminPermission]
use_read_replica = True
def get(self, request, slug, project_id):
"""Get project summary
Get the summary of a project
"""
project = Project.objects.filter(pk=project_id, workspace__slug=slug).first()
if not project:
return Response({"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND)
fields = request.GET.get("fields", "").split(",")
requested_fields = set(filter(None, (f.strip() for f in fields))) & set(ALLOWED_PROJECT_SUMMARY_FIELDS)
if not requested_fields:
requested_fields = set(ALLOWED_PROJECT_SUMMARY_FIELDS)
# Single DB round-trip with only requested count subqueries
counts = self._get_all_summary_counts(project_id, requested_fields)
counts_dict = {field: counts[field] for field in requested_fields}
summary = {
"id": project.id,
"name": project.name,
"identifier": project.identifier,
"counts": counts_dict,
}
return Response(summary, status=status.HTTP_200_OK)
# Getting all summary counts in one ORM query; only runs subqueries for requested fields.
def _get_all_summary_counts(self, project_id, requested_fields):
"""Return requested summary counts in one ORM query; only runs subqueries for requested fields."""
# Using a different annotation name for 'pages' to avoid conflict with Project.pages (M2M from Page)
def _annotation_name(field):
return "pages_count" if field == "pages" else field
subquery_builders = {
"members": lambda: (
ProjectMember.objects.filter(project_id=OuterRef("pk"), is_active=True)
.values("project_id")
.annotate(count=Count("*"))
.values("count")
),
"states": lambda: (
State.objects.filter(project_id=OuterRef("pk"))
.values("project_id")
.annotate(count=Count("*"))
.values("count")
),
"labels": lambda: (
Label.objects.filter(project_id=OuterRef("pk"))
.values("project_id")
.annotate(count=Count("*"))
.values("count")
),
"cycles": lambda: (
Cycle.objects.filter(project_id=OuterRef("pk"))
.values("project_id")
.annotate(count=Count("*"))
.values("count")
),
"modules": lambda: (
Module.objects.filter(project_id=OuterRef("pk"))
.values("project_id")
.annotate(count=Count("*"))
.values("count")
),
"issues": lambda: (
Issue.objects.filter(project_id=OuterRef("pk"))
.exclude(state__group=StateGroup.TRIAGE.value)
.values("project_id")
.annotate(count=Count("*"))
.values("count")
),
"intakes": lambda: (
IntakeIssue.objects.filter(project_id=OuterRef("pk"))
.values("project_id")
.annotate(count=Count("*"))
.values("count")
),
"pages": lambda: (
ProjectPage.objects.filter(project_id=OuterRef("pk"))
.values("project_id")
.annotate(count=Count("*"))
.values("count")
),
}
# Build annotations dictionary for the requested fields
annotations = {
_annotation_name(field): Coalesce(Subquery(subquery_builders[field]()), 0) for field in requested_fields
}
# Prepare values list for the annotation names
fields_list = sorted(requested_fields)
values_list = [_annotation_name(f) for f in fields_list]
# Execute the query and get the result
query_result = Project.objects.filter(pk=project_id).annotate(**annotations).values(*values_list).first()
if not query_result:
return {field: 0 for field in requested_fields}
# Return the result as a dictionary
return {field: query_result[_annotation_name(field)] for field in requested_fields}

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