├── apiserver ├── plane │ ├── api │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── serializers │ │ │ ├── base.py │ │ │ ├── integration │ │ │ │ ├── __init__.py │ │ │ │ ├── slack.py │ │ │ │ └── base.py │ │ │ ├── api_token.py │ │ │ ├── asset.py │ │ │ ├── notification.py │ │ │ ├── importer.py │ │ │ ├── state.py │ │ │ └── analytic.py │ │ ├── permissions │ │ │ └── __init__.py │ │ └── views │ │ │ ├── integration │ │ │ └── __init__.py │ │ │ └── release.py │ ├── db │ │ ├── __init__.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ └── wait_for_db.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ ├── 0017_alter_workspace_unique_together.py │ │ │ ├── 0030_alter_estimatepoint_unique_together.py │ │ │ ├── 0008_label_colour.py │ │ │ ├── 0004_alter_state_sequence.py │ │ │ ├── 0014_alter_workspacememberinvite_unique_together.py │ │ │ ├── 0036_alter_workspace_organization_size.py │ │ │ ├── 0026_alter_projectmember_view_props.py │ │ │ ├── 0006_alter_cycle_status.py │ │ │ ├── 0007_label_parent.py │ │ │ ├── 0032_auto_20230520_2015.py │ │ │ ├── 0005_auto_20221114_2127.py │ │ │ ├── 0019_auto_20230131_0049.py │ │ │ ├── 0003_auto_20221109_2320.py │ │ │ ├── 0009_auto_20221208_0310.py │ │ │ ├── 0015_auto_20230107_1636.py │ │ │ ├── 0016_auto_20230107_1735.py │ │ │ ├── 0034_auto_20230628_1046.py │ │ │ ├── 0010_auto_20221213_0037.py │ │ │ └── 0013_auto_20230107_0041.py │ │ ├── apps.py │ │ └── models │ │ │ ├── integration │ │ │ ├── __init__.py │ │ │ └── slack.py │ │ │ ├── analytic.py │ │ │ ├── social_connection.py │ │ │ └── api_token.py │ ├── static │ │ ├── humans.txt │ │ ├── css │ │ │ └── style.css │ │ └── js │ │ │ └── script.js │ ├── utils │ │ ├── __init__.py │ │ ├── importers │ │ │ └── __init__.py │ │ ├── integrations │ │ │ └── __init__.py │ │ ├── markdown.py │ │ ├── ip_address.py │ │ ├── html_processor.py │ │ ├── issue_search.py │ │ └── imports.py │ ├── web │ │ ├── __init__.py │ │ ├── views.py │ │ ├── apps.py │ │ └── urls.py │ ├── analytics │ │ ├── __init__.py │ │ └── apps.py │ ├── bgtasks │ │ ├── __init__.py │ │ ├── apps.py │ │ └── user_welcome_task.py │ ├── middleware │ │ ├── __init__.py │ │ ├── apps.py │ │ └── user_middleware.py │ ├── settings │ │ ├── __init__.py │ │ ├── redis.py │ │ └── test.py │ ├── tests │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── test_asset.py │ │ │ ├── test_cycle.py │ │ │ ├── test_issue.py │ │ │ ├── test_people.py │ │ │ ├── test_shortcut.py │ │ │ ├── test_state.py │ │ │ ├── test_project.py │ │ │ ├── test_view.py │ │ │ ├── test_oauth.py │ │ │ ├── test_auth_extended.py │ │ │ └── base.py │ │ ├── __init__.py │ │ └── apps.py │ ├── __init__.py │ ├── wsgi.py │ ├── asgi.py │ ├── urls.py │ └── celery.py ├── runtime.txt ├── requirements │ ├── local.txt │ ├── test.txt │ ├── production.txt │ └── base.txt ├── bin │ ├── beat │ ├── worker │ ├── takeoff │ └── user_script.py ├── gunicorn.config.py ├── requirements.txt ├── templates │ ├── about.html │ ├── index.html │ ├── emails │ │ ├── auth │ │ │ ├── email_verification.html │ │ │ └── forgot_password.html │ │ └── exports │ │ │ └── analytics.html │ └── admin │ │ └── base_site.html ├── Procfile └── manage.py ├── apps └── app │ ├── components │ ├── gantt-chart │ │ ├── index.ts │ │ ├── views │ │ │ └── index.ts │ │ ├── hooks │ │ │ └── index.tsx │ │ ├── root.tsx │ │ └── types │ │ │ └── index.ts │ ├── integration │ │ ├── slack │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── github │ │ │ ├── index.ts │ │ │ ├── auth.tsx │ │ │ └── import-confirm.tsx │ │ └── jira │ │ │ └── index.ts │ ├── auth-screens │ │ ├── project │ │ │ └── index.ts │ │ ├── workspace │ │ │ └── index.ts │ │ └── index.ts │ ├── onboarding │ │ ├── tour │ │ │ └── index.ts │ │ └── index.ts │ ├── issues │ │ ├── comment │ │ │ └── index.ts │ │ ├── view-select │ │ │ └── index.ts │ │ ├── select │ │ │ └── index.ts │ │ ├── sidebar-select │ │ │ └── index.ts │ │ └── index.ts │ ├── modules │ │ ├── select │ │ │ └── index.ts │ │ ├── sidebar-select │ │ │ └── index.ts │ │ └── index.ts │ ├── core │ │ ├── list-view │ │ │ └── index.ts │ │ ├── theme │ │ │ └── index.ts │ │ ├── sidebar │ │ │ └── index.ts │ │ ├── board-view │ │ │ └── index.ts │ │ ├── calendar-view │ │ │ └── index.ts │ │ ├── filters │ │ │ └── index.ts │ │ ├── spreadsheet-view │ │ │ └── index.ts │ │ ├── modals │ │ │ └── index.ts │ │ ├── index.ts │ │ └── gantt-chart-view │ │ │ └── index.tsx │ ├── ui │ │ ├── buttons │ │ │ ├── index.ts │ │ │ ├── type.d.ts │ │ │ ├── secondary-button.tsx │ │ │ ├── primary-button.tsx │ │ │ └── danger-button.tsx │ │ ├── graphs │ │ │ ├── index.ts │ │ │ ├── types.d.ts │ │ │ ├── pie-graph.tsx │ │ │ ├── scatter-plot-graph.tsx │ │ │ └── line-graph.tsx │ │ ├── dropdowns │ │ │ ├── index.ts │ │ │ └── types.d.ts │ │ ├── icon.tsx │ │ ├── text-area │ │ │ └── types.d.ts │ │ ├── input │ │ │ └── types.d.ts │ │ ├── loader.tsx │ │ ├── index.ts │ │ ├── integration-and-import-export-banner.tsx │ │ ├── labels-list.tsx │ │ └── linear-progress-indicator.tsx │ ├── analytics │ │ ├── select │ │ │ ├── index.ts │ │ │ └── y-axis.tsx │ │ ├── index.ts │ │ ├── scope-and-demand │ │ │ └── index.ts │ │ └── custom-analytics │ │ │ └── index.ts │ ├── automation │ │ └── index.ts │ ├── pages │ │ ├── pages-list │ │ │ ├── types.ts │ │ │ ├── index.ts │ │ │ ├── all-pages-list.tsx │ │ │ ├── favorite-pages-list.tsx │ │ │ ├── my-pages-list.tsx │ │ │ └── other-pages-list.tsx │ │ └── index.ts │ ├── estimates │ │ └── index.tsx │ ├── notifications │ │ └── index.ts │ ├── icons │ │ ├── types.d.ts │ │ ├── css-file-icon.tsx │ │ ├── csv-file-icon.tsx │ │ ├── doc-file-icon.tsx │ │ ├── img-file-icon.tsx │ │ ├── jpg-file-icon.tsx │ │ ├── png-file-icon.tsx │ │ ├── svg-file-icon.tsx │ │ ├── txt-file-icon.tsx │ │ ├── js-file-icon.tsx │ │ ├── pdf-file-icon.tsx │ │ ├── html-file-icon.tsx │ │ ├── audio-file-icon.tsx │ │ ├── figma-file-icon.tsx │ │ ├── sheet-file-icon.tsx │ │ ├── video-file-icon.tsx │ │ ├── default-file-icon.tsx │ │ ├── cmd-icon.tsx │ │ ├── backlog-state-icon.tsx │ │ ├── completed-cycle-icon.tsx │ │ ├── upcoming-cycle-icon.tsx │ │ ├── heartbeat-icon.tsx │ │ ├── priority-icon.tsx │ │ ├── single-comment-icon.tsx │ │ ├── current-cycle-icon.tsx │ │ ├── state-group-icon.tsx │ │ ├── bolt-icon.tsx │ │ ├── pencil-scribble-icon.tsx │ │ ├── contrast-icon.tsx │ │ ├── comment-icon.tsx │ │ ├── cycle-icon.tsx │ │ ├── plus-icon.tsx │ │ ├── view-list-icon.tsx │ │ ├── check.tsx │ │ └── x-mark-icon.tsx │ ├── states │ │ └── index.ts │ ├── cycles │ │ ├── cycles-list │ │ │ ├── index.ts │ │ │ ├── all-cycles-list.tsx │ │ │ ├── draft-cycles-list.tsx │ │ │ ├── upcoming-cycles-list.tsx │ │ │ └── completed-cycles-list.tsx │ │ └── index.ts │ ├── account │ │ └── index.ts │ ├── views │ │ └── index.ts │ ├── labels │ │ └── index.ts │ ├── command-palette │ │ └── index.ts │ ├── project │ │ ├── index.ts │ │ └── settings-header.tsx │ ├── emoji-icon-picker │ │ ├── types.d.ts │ │ └── helpers.ts │ ├── inbox │ │ └── index.ts │ ├── workspace │ │ ├── index.ts │ │ └── settings-header.tsx │ ├── search-listbox │ │ └── types.d.ts │ ├── dnd │ │ └── StrictModeDroppable.tsx │ └── rich-text-editor │ │ └── toolbar │ │ └── index.tsx │ ├── public │ ├── user.png │ ├── services │ │ ├── jira.png │ │ ├── slack.png │ │ └── github.png │ ├── favicon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ └── site.webmanifest │ ├── attachment │ │ ├── css-icon.png │ │ ├── csv-icon.png │ │ ├── doc-icon.png │ │ ├── img-icon.png │ │ ├── jpg-icon.png │ │ ├── js-icon.png │ │ ├── pdf-icon.png │ │ ├── png-icon.png │ │ ├── svg-icon.png │ │ ├── txt-icon.png │ │ ├── audio-icon.png │ │ ├── excel-icon.png │ │ ├── figma-icon.png │ │ ├── html-icon.png │ │ ├── video-icon.png │ │ └── default-icon.png │ ├── logos │ │ ├── github-black.png │ │ ├── github-square.png │ │ └── github-white.png │ ├── plane-logos │ │ └── blue-without-text.png │ ├── site.webmanifest.json │ ├── mac-command.svg │ └── empty-state │ │ └── issue-archive.svg │ ├── types │ ├── waitlist.d.ts │ ├── calendar.ts │ ├── ai.d.ts │ ├── importer │ │ ├── github-importer.d.ts │ │ └── index.ts │ ├── state.d.ts │ ├── estimate.d.ts │ ├── index.d.ts │ └── views.d.ts │ ├── .prettierrc │ ├── sentry.properties │ ├── postcss.config.js │ ├── layouts │ ├── auth-layout │ │ ├── index.ts │ │ └── user-authorization-wrapper.tsx │ ├── default-layout │ │ └── index.tsx │ └── app-layout │ │ └── app-header.tsx │ ├── .eslintrc.js │ ├── next-env.d.ts │ ├── Dockerfile.dev │ ├── constants │ ├── state.ts │ ├── module.ts │ ├── seo-variables.ts │ ├── calendar.ts │ ├── graph.ts │ ├── project.ts │ ├── inbox.ts │ ├── due-dates.ts │ └── themes.ts │ ├── tsconfig.json │ ├── hooks │ ├── use-theme.tsx │ ├── use-toast.tsx │ ├── use-timer.tsx │ ├── use-debounce.tsx │ ├── use-outside-click-detector.tsx │ ├── use-issues.tsx │ ├── use-workspaces.tsx │ ├── gantt-chart │ │ ├── issue-view.tsx │ │ ├── cycle-issues-view.tsx │ │ ├── module-issues-view.tsx │ │ └── view-issues-view.tsx │ ├── use-reload-confirmation.tsx │ ├── use-project-details.tsx │ ├── use-sub-issue.tsx │ └── use-workspace-details.tsx │ ├── pages │ ├── error.tsx │ └── api │ │ ├── slack-redirect.ts │ │ ├── unsplash.ts │ │ └── track-event.ts │ ├── helpers │ ├── common.helper.ts │ ├── emoji.helper.ts │ ├── color.helper.ts │ ├── attachment.helper.ts │ ├── state.helper.ts │ └── graph.helper.ts │ ├── lib │ ├── redirect.ts │ └── cookie.ts │ ├── services │ ├── web-waitlist.service.ts │ └── ai.service.ts │ ├── sentry.client.config.js │ ├── sentry.server.config.js │ ├── sentry.edge.config.js │ ├── styles │ ├── nprogress.css │ └── command-pallette.css │ ├── manifest.json │ └── next.config.js ├── heroku.yml ├── .dockerignore ├── packages ├── ui │ ├── button │ │ └── index.tsx │ ├── tsconfig.json │ ├── package.json │ └── index.tsx ├── tsconfig │ ├── package.json │ ├── react-library.json │ ├── base.json │ └── nextjs.json └── eslint-config-custom │ ├── package.json │ └── index.js ├── deploy └── heroku │ └── Dockerfile ├── nginx ├── env.sh ├── Dockerfile ├── nginx.conf.template ├── nginx-single-docker-image.conf └── supervisor.conf ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── config.yaml │ └── --feature-request.yaml ├── start.sh ├── replace-env-vars.sh ├── setup.sh ├── package.json └── .gitignore /apiserver/plane/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/static/humans.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/analytics/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/bgtasks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/static/css/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/static/js/script.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.11.4 -------------------------------------------------------------------------------- /apiserver/plane/db/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/utils/importers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/utils/integrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/db/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apiserver/plane/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import * -------------------------------------------------------------------------------- /apps/app/components/gantt-chart/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./root"; 2 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: deploy/heroku/Dockerfile -------------------------------------------------------------------------------- /apiserver/plane/tests/api/test_asset.py: -------------------------------------------------------------------------------- 1 | # TODO: Tests for File Asset Uploads -------------------------------------------------------------------------------- /apiserver/plane/tests/api/test_cycle.py: -------------------------------------------------------------------------------- 1 | # TODO: Write Test for Cycle Endpoints -------------------------------------------------------------------------------- /apiserver/plane/tests/api/test_issue.py: -------------------------------------------------------------------------------- 1 | # TODO: Write Test for Issue Endpoints -------------------------------------------------------------------------------- /apiserver/plane/tests/api/test_people.py: -------------------------------------------------------------------------------- 1 | # TODO: Write Test for people Endpoint -------------------------------------------------------------------------------- /apiserver/plane/tests/api/test_shortcut.py: -------------------------------------------------------------------------------- 1 | # TODO: Write Test for shortcuts -------------------------------------------------------------------------------- /apiserver/plane/tests/api/test_state.py: -------------------------------------------------------------------------------- 1 | # TODO: Wrote test for state endpoints -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | *.pyc 3 | .env 4 | venv 5 | node_modules 6 | npm-debug.log -------------------------------------------------------------------------------- /apiserver/plane/tests/api/test_project.py: -------------------------------------------------------------------------------- 1 | # TODO: Write Tests for project endpoints -------------------------------------------------------------------------------- /apiserver/plane/tests/api/test_view.py: -------------------------------------------------------------------------------- 1 | # TODO: Write test for view endpoints 2 | -------------------------------------------------------------------------------- /apiserver/requirements/local.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | django-debug-toolbar==4.1.0 -------------------------------------------------------------------------------- /apps/app/components/integration/slack/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./select-channel"; -------------------------------------------------------------------------------- /apiserver/plane/tests/api/test_oauth.py: -------------------------------------------------------------------------------- 1 | #TODO: Tests for OAuth Authentication Endpoint -------------------------------------------------------------------------------- /apiserver/plane/utils/markdown.py: -------------------------------------------------------------------------------- 1 | import mistune 2 | 3 | markdown = mistune.Markdown() -------------------------------------------------------------------------------- /apiserver/requirements/test.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | pytest==7.1.2 4 | coverage==6.5.0 -------------------------------------------------------------------------------- /apps/app/components/auth-screens/project/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./join-project"; 2 | -------------------------------------------------------------------------------- /apps/app/components/auth-screens/workspace/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./not-a-member"; 2 | -------------------------------------------------------------------------------- /apiserver/plane/tests/api/test_auth_extended.py: -------------------------------------------------------------------------------- 1 | #TODO: Tests for ChangePassword and other Endpoints -------------------------------------------------------------------------------- /apiserver/plane/web/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /apps/app/components/onboarding/tour/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./root"; 2 | export * from "./sidebar"; 3 | -------------------------------------------------------------------------------- /apps/app/public/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/user.png -------------------------------------------------------------------------------- /apps/app/types/waitlist.d.ts: -------------------------------------------------------------------------------- 1 | export interface IWebWaitListResponse { 2 | status: string; 3 | } 4 | -------------------------------------------------------------------------------- /apiserver/plane/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = ('celery_app',) 4 | -------------------------------------------------------------------------------- /apps/app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/button/index.tsx: -------------------------------------------------------------------------------- 1 | export const Button = () => { 2 | return ; 3 | }; 4 | -------------------------------------------------------------------------------- /apiserver/bin/beat: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | python manage.py wait_for_db 5 | celery -A plane beat -l info -------------------------------------------------------------------------------- /apiserver/bin/worker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | python manage.py wait_for_db 5 | celery -A plane worker -l info -------------------------------------------------------------------------------- /apps/app/components/issues/comment/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./add-comment"; 2 | export * from "./comment-card"; 3 | -------------------------------------------------------------------------------- /apps/app/sentry.properties: -------------------------------------------------------------------------------- 1 | defaults.url=https://sentry.io/ 2 | defaults.org=plane 3 | defaults.project=plane-web 4 | -------------------------------------------------------------------------------- /apps/app/types/calendar.ts: -------------------------------------------------------------------------------- 1 | export interface ICalendarRange { 2 | startDate: Date; 3 | endDate: Date; 4 | } 5 | -------------------------------------------------------------------------------- /apps/app/public/services/jira.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/services/jira.png -------------------------------------------------------------------------------- /apps/app/public/services/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/services/slack.png -------------------------------------------------------------------------------- /deploy/heroku/Dockerfile: -------------------------------------------------------------------------------- 1 | # Deploy the Plane image 2 | FROM makeplane/plane 3 | 4 | LABEL maintainer="engineering@plane.so" -------------------------------------------------------------------------------- /apps/app/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/favicon/favicon.ico -------------------------------------------------------------------------------- /apps/app/public/services/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/services/github.png -------------------------------------------------------------------------------- /apiserver/plane/db/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DbConfig(AppConfig): 5 | name = "plane.db" 6 | -------------------------------------------------------------------------------- /apps/app/components/modules/select/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lead"; 2 | export * from "./members"; 3 | export * from "./status"; 4 | -------------------------------------------------------------------------------- /apps/app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/app/public/attachment/css-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/css-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/csv-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/csv-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/doc-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/doc-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/img-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/img-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/jpg-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/jpg-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/js-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/js-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/pdf-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/pdf-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/png-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/png-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/svg-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/svg-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/txt-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/txt-icon.png -------------------------------------------------------------------------------- /apps/app/public/logos/github-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/logos/github-black.png -------------------------------------------------------------------------------- /apps/app/public/logos/github-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/logos/github-square.png -------------------------------------------------------------------------------- /apps/app/public/logos/github-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/logos/github-white.png -------------------------------------------------------------------------------- /nginx/env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | envsubst < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf 4 | exec nginx -g 'daemon off;' 5 | -------------------------------------------------------------------------------- /apiserver/plane/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = "plane.api" 6 | -------------------------------------------------------------------------------- /apiserver/plane/tests/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = "plane.tests" 6 | -------------------------------------------------------------------------------- /apiserver/plane/web/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WebConfig(AppConfig): 5 | name = 'plane.web' 6 | -------------------------------------------------------------------------------- /apps/app/public/attachment/audio-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/audio-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/excel-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/excel-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/figma-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/figma-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/html-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/html-icon.png -------------------------------------------------------------------------------- /apps/app/public/attachment/video-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/video-icon.png -------------------------------------------------------------------------------- /apps/app/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /apps/app/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /apps/app/public/attachment/default-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/attachment/default-icon.png -------------------------------------------------------------------------------- /apps/app/public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /apiserver/plane/bgtasks/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BgtasksConfig(AppConfig): 5 | name = 'plane.bgtasks' 6 | -------------------------------------------------------------------------------- /apps/app/components/auth-screens/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./project"; 2 | export * from "./workspace"; 3 | export * from "./not-authorized-view"; 4 | -------------------------------------------------------------------------------- /apps/app/components/core/list-view/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./all-lists"; 2 | export * from "./single-issue"; 3 | export * from "./single-list"; 4 | -------------------------------------------------------------------------------- /apps/app/layouts/auth-layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./project-authorization-wrapper"; 2 | export * from "./workspace-authorization-wrapper"; 3 | -------------------------------------------------------------------------------- /apiserver/plane/analytics/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AnalyticsConfig(AppConfig): 5 | name = 'plane.analytics' 6 | -------------------------------------------------------------------------------- /apiserver/plane/middleware/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class Middleware(AppConfig): 5 | name = 'plane.middleware' 6 | -------------------------------------------------------------------------------- /apps/app/public/plane-logos/blue-without-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/plane-logos/blue-without-text.png -------------------------------------------------------------------------------- /apps/app/components/ui/buttons/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./danger-button"; 2 | export * from "./primary-button"; 3 | export * from "./secondary-button"; 4 | -------------------------------------------------------------------------------- /apps/app/public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/app/public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krshrimali/plane/develop/apps/app/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/app/components/core/theme/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./color-picker-input"; 2 | export * from "./custom-theme-selector"; 3 | export * from "./theme-switch"; 4 | -------------------------------------------------------------------------------- /apps/app/components/modules/sidebar-select/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./select-lead"; 2 | export * from "./select-members"; 3 | export * from "./select-status"; 4 | -------------------------------------------------------------------------------- /apps/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ["custom"], 4 | rules: { 5 | "@next/next/no-img-element": "off", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /apps/app/components/analytics/select/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./project"; 2 | export * from "./segment"; 3 | export * from "./x-axis"; 4 | export * from "./y-axis"; 5 | -------------------------------------------------------------------------------- /apps/app/components/core/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./links-list"; 2 | export * from "./sidebar-progress-stats"; 3 | export * from "./single-progress-stats"; 4 | -------------------------------------------------------------------------------- /apps/app/components/automation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auto-close-automation"; 2 | export * from "./auto-archive-automation"; 3 | export * from "./select-month-modal"; 4 | -------------------------------------------------------------------------------- /apps/app/components/pages/pages-list/types.ts: -------------------------------------------------------------------------------- 1 | import { TPageViewProps } from "types"; 2 | 3 | export type TPagesListProps = { 4 | viewType: TPageViewProps; 5 | }; 6 | -------------------------------------------------------------------------------- /apps/app/components/estimates/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./create-update-estimate-modal"; 2 | export * from "./single-estimate"; 3 | export * from "./delete-estimate-modal"; 4 | -------------------------------------------------------------------------------- /apps/app/components/notifications/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./notification-card"; 2 | export * from "./notification-popover"; 3 | export * from "./select-snooze-till-modal"; 4 | -------------------------------------------------------------------------------- /apps/app/components/icons/types.d.ts: -------------------------------------------------------------------------------- 1 | export type Props = { 2 | className?: string; 3 | width?: string | number; 4 | height?: string | number; 5 | color?: string; 6 | }; 7 | -------------------------------------------------------------------------------- /apiserver/gunicorn.config.py: -------------------------------------------------------------------------------- 1 | from psycogreen.gevent import patch_psycopg 2 | 3 | 4 | def post_fork(server, worker): 5 | patch_psycopg() 6 | worker.log.info("Made Psycopg2 Green") -------------------------------------------------------------------------------- /apiserver/requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is here because many Platforms as a Service look for 2 | # requirements.txt in the root directory of a project. 3 | -r requirements/production.txt -------------------------------------------------------------------------------- /apps/app/components/analytics/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./custom-analytics"; 2 | export * from "./scope-and-demand"; 3 | export * from "./select"; 4 | export * from "./project-modal"; 5 | -------------------------------------------------------------------------------- /apps/app/components/core/board-view/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./all-boards"; 2 | export * from "./board-header"; 3 | export * from "./single-board"; 4 | export * from "./single-issue"; 5 | -------------------------------------------------------------------------------- /apps/app/components/core/calendar-view/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./calendar-header"; 2 | export * from "./calendar"; 3 | export * from "./single-date"; 4 | export * from "./single-issue"; 5 | -------------------------------------------------------------------------------- /apiserver/templates/about.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'base.html' %} 3 | {% load static %} 4 | 5 | 6 | {% block content %} 7 |

Hello from plane!

8 |

Made with Django

9 | {% endblock content %} -------------------------------------------------------------------------------- /apiserver/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} {% load static %} {% block content %} 2 |
3 |

Hello from plane!

4 |
5 | {% endblock content %} -------------------------------------------------------------------------------- /apps/app/components/states/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create-update-state-inline"; 2 | export * from "./create-state-modal"; 3 | export * from "./delete-state-modal"; 4 | export * from "./single-state"; 5 | -------------------------------------------------------------------------------- /apiserver/plane/api/serializers/base.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | 4 | class BaseSerializer(serializers.ModelSerializer): 5 | id = serializers.PrimaryKeyRelatedField(read_only=True) 6 | -------------------------------------------------------------------------------- /apps/app/components/core/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./due-date-filter-modal"; 2 | export * from "./due-date-filter-select"; 3 | export * from "./filters-list"; 4 | export * from "./issues-view-filter"; 5 | -------------------------------------------------------------------------------- /apps/app/components/core/spreadsheet-view/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./spreadsheet-view"; 2 | export * from "./single-issue"; 3 | export * from "./spreadsheet-columns"; 4 | export * from "./spreadsheet-issues"; 5 | -------------------------------------------------------------------------------- /apps/app/components/onboarding/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./tour"; 2 | export * from "./invite-members"; 3 | export * from "./join-workspaces"; 4 | export * from "./user-details"; 5 | export * from "./workspace"; 6 | -------------------------------------------------------------------------------- /apps/app/components/cycles/cycles-list/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./all-cycles-list"; 2 | export * from "./completed-cycles-list"; 3 | export * from "./draft-cycles-list"; 4 | export * from "./upcoming-cycles-list"; 5 | -------------------------------------------------------------------------------- /apps/app/components/ui/graphs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bar-graph"; 2 | export * from "./calendar-graph"; 3 | export * from "./line-graph"; 4 | export * from "./pie-graph"; 5 | export * from "./scatter-plot-graph"; 6 | -------------------------------------------------------------------------------- /apiserver/plane/web/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.views.generic import TemplateView 3 | 4 | urlpatterns = [ 5 | path('about/', TemplateView.as_view(template_name='about.html')) 6 | 7 | ] 8 | -------------------------------------------------------------------------------- /apps/app/components/ui/dropdowns/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./context-menu"; 2 | export * from "./custom-menu"; 3 | export * from "./custom-search-select"; 4 | export * from "./custom-select"; 5 | export * from "./types.d"; 6 | -------------------------------------------------------------------------------- /apps/app/components/ui/graphs/types.d.ts: -------------------------------------------------------------------------------- 1 | import { Theme, Margin } from "@nivo/core"; 2 | 3 | export type TGraph = { 4 | height?: string; 5 | width?: string; 6 | margin?: Partial; 7 | theme?: Theme; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "files": [ 6 | "base.json", 7 | "nextjs.json", 8 | "react-library.json" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/app/components/analytics/scope-and-demand/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./demand"; 2 | export * from "./leaderboard"; 3 | export * from "./scope-and-demand"; 4 | export * from "./scope"; 5 | export * from "./year-wise-issues"; 6 | -------------------------------------------------------------------------------- /apps/app/components/issues/view-select/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./assignee"; 2 | export * from "./due-date"; 3 | export * from "./estimate"; 4 | export * from "./priority"; 5 | export * from "./state"; 6 | export * from "./label"; 7 | -------------------------------------------------------------------------------- /apiserver/plane/db/models/integration/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Integration, WorkspaceIntegration 2 | from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync 3 | from .slack import SlackProjectSync -------------------------------------------------------------------------------- /apps/app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/app/components/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./email-code-form"; 2 | export * from "./email-password-form"; 3 | export * from "./email-reset-password-form"; 4 | export * from "./github-login-button"; 5 | export * from "./google-login"; 6 | -------------------------------------------------------------------------------- /apps/app/components/views/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./delete-view-modal"; 2 | export * from "./form"; 3 | export * from "./gantt-chart"; 4 | export * from "./modal"; 5 | export * from "./select-filters"; 6 | export * from "./single-view-item"; 7 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"], 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": ["DOM"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/app/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | RUN apk add --no-cache libc6-compat 3 | # Set working directory 4 | WORKDIR /app 5 | 6 | 7 | COPY . . 8 | RUN yarn global add turbo 9 | RUN yarn install 10 | EXPOSE 3000 11 | CMD ["yarn","dev"] 12 | -------------------------------------------------------------------------------- /apps/app/components/core/modals/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bulk-delete-issues-modal"; 2 | export * from "./existing-issues-list-modal"; 3 | export * from "./gpt-assistant-modal"; 4 | export * from "./image-upload-modal"; 5 | export * from "./link-modal"; 6 | -------------------------------------------------------------------------------- /apps/app/components/issues/select/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./assignee"; 2 | export * from "./date"; 3 | export * from "./estimate"; 4 | export * from "./label"; 5 | export * from "./priority"; 6 | export * from "./project"; 7 | export * from "./state"; 8 | -------------------------------------------------------------------------------- /apps/app/constants/state.ts: -------------------------------------------------------------------------------- 1 | export const STATE_GROUP_COLORS: { 2 | [key: string]: string; 3 | } = { 4 | backlog: "#ced4da", 5 | unstarted: "#26b5ce", 6 | started: "#f7ae59", 7 | cancelled: "#d687ff", 8 | completed: "#09a953", 9 | }; 10 | -------------------------------------------------------------------------------- /apiserver/plane/api/permissions/__init__.py: -------------------------------------------------------------------------------- 1 | from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission 2 | from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission 3 | -------------------------------------------------------------------------------- /apps/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"], 5 | "compilerOptions": { 6 | "baseUrl": ".", 7 | "jsx": "preserve" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ["custom"], 5 | settings: { 6 | next: { 7 | rootDir: ["apps/*"], 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/app/components/analytics/custom-analytics/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./graph"; 2 | export * from "./create-update-analytics-modal"; 3 | export * from "./custom-analytics"; 4 | export * from "./select-bar"; 5 | export * from "./sidebar"; 6 | export * from "./table"; 7 | -------------------------------------------------------------------------------- /apps/app/components/pages/pages-list/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./all-pages-list"; 2 | export * from "./favorite-pages-list"; 3 | export * from "./my-pages-list"; 4 | export * from "./other-pages-list"; 5 | export * from "./recent-pages-list"; 6 | export * from "./types"; 7 | -------------------------------------------------------------------------------- /apiserver/requirements/production.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | dj-database-url==2.0.0 4 | gunicorn==20.1.0 5 | whitenoise==6.5.0 6 | django-storages==1.13.2 7 | boto3==1.27.0 8 | django-anymail==10.0 9 | django-debug-toolbar==4.1.0 10 | gevent==23.7.0 11 | psycogreen==1.0.2 -------------------------------------------------------------------------------- /apiserver/plane/utils/ip_address.py: -------------------------------------------------------------------------------- 1 | def get_client_ip(request): 2 | x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') 3 | if x_forwarded_for: 4 | ip = x_forwarded_for.split(',')[0] 5 | else: 6 | ip = request.META.get('REMOTE_ADDR') 7 | return ip -------------------------------------------------------------------------------- /apps/app/components/labels/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create-label-modal"; 2 | export * from "./create-update-label-inline"; 3 | export * from "./delete-label-modal"; 4 | export * from "./labels-list-modal"; 5 | export * from "./single-label-group"; 6 | export * from "./single-label"; 7 | -------------------------------------------------------------------------------- /apps/app/types/ai.d.ts: -------------------------------------------------------------------------------- 1 | import { IProjectLite, IWorkspaceLite } from "types"; 2 | 3 | export interface IGptResponse { 4 | response: string; 5 | response_html: string; 6 | count: number; 7 | project_detail: IProjectLite; 8 | workspace_detail: IWorkspaceLite; 9 | } 10 | -------------------------------------------------------------------------------- /apiserver/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile - 2 | worker: celery -A plane worker -l info 3 | beat: celery -A plane beat -l INFO -------------------------------------------------------------------------------- /apps/app/components/gantt-chart/views/index.ts: -------------------------------------------------------------------------------- 1 | // export * from "./hours-view"; 2 | // export * from "./day-view"; 3 | export * from "./week-view"; 4 | export * from "./bi-week-view"; 5 | export * from "./month-view"; 6 | export * from "./quater-view"; 7 | export * from "./year-view"; 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Help and support 3 | about: Reach out to us on our Discord server or GitHub discussions. 4 | - name: Dedicated support 5 | url: mailto:support@plane.so 6 | about: Write to us if you'd like dedicated support using Plane 7 | -------------------------------------------------------------------------------- /apps/app/components/command-palette/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./command-pallette"; 2 | export * from "./shortcuts-modal"; 3 | export * from "./change-issue-state"; 4 | export * from "./change-issue-priority"; 5 | export * from "./change-issue-assignee"; 6 | export * from "./change-interface-theme"; 7 | -------------------------------------------------------------------------------- /apps/app/hooks/use-theme.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { themeContext } from "contexts/theme.context"; 3 | 4 | const useTheme = () => { 5 | const themeContextData = useContext(themeContext); 6 | return themeContextData; 7 | }; 8 | 9 | export default useTheme; 10 | -------------------------------------------------------------------------------- /apps/app/hooks/use-toast.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { toastContext } from "contexts/toast.context"; 3 | 4 | const useToast = () => { 5 | const toastContextData = useContext(toastContext); 6 | return toastContextData; 7 | }; 8 | 9 | export default useToast; 10 | -------------------------------------------------------------------------------- /apps/app/public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react", 7 | "lib": ["ES2015"], 8 | "module": "ESNext", 9 | "target": "es6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.25.0-alpine 2 | 3 | RUN rm /etc/nginx/conf.d/default.conf 4 | COPY nginx.conf.template /etc/nginx/nginx.conf.template 5 | 6 | COPY ./env.sh /docker-entrypoint.sh 7 | 8 | RUN chmod +x /docker-entrypoint.sh 9 | # Update all environment variables 10 | CMD ["/docker-entrypoint.sh"] 11 | -------------------------------------------------------------------------------- /apps/app/components/ui/buttons/type.d.ts: -------------------------------------------------------------------------------- 1 | export type ButtonProps = { 2 | children: React.ReactNode; 3 | className?: string; 4 | onClick?: () => void; 5 | type?: "button" | "submit" | "reset"; 6 | disabled?: boolean; 7 | loading?: boolean; 8 | size?: "sm" | "md" | "lg"; 9 | outline?: boolean; 10 | }; 11 | -------------------------------------------------------------------------------- /apps/app/components/project/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create-project-modal"; 2 | export * from "./delete-project-modal"; 3 | export * from "./sidebar-list"; 4 | export * from "./settings-header" 5 | export * from "./single-integration-card"; 6 | export * from "./single-project-card"; 7 | export * from "./single-sidebar-project"; 8 | -------------------------------------------------------------------------------- /apiserver/templates/emails/auth/email_verification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Dear {{first_name}},

5 | Welcome! Your account has been created. 6 | Verify your email by clicking on the link below
7 | {{verification_url}} 8 | successfully.

9 |

10 | 11 | -------------------------------------------------------------------------------- /apiserver/bin/takeoff: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | python manage.py wait_for_db 4 | python manage.py migrate 5 | 6 | # Create a Default User 7 | python bin/user_script.py 8 | 9 | exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - 10 | -------------------------------------------------------------------------------- /apps/app/components/emoji-icon-picker/types.d.ts: -------------------------------------------------------------------------------- 1 | export type Props = { 2 | label: string | React.ReactNode; 3 | value: any; 4 | onChange: ( 5 | data: 6 | | string 7 | | { 8 | name: string; 9 | color: string; 10 | } 11 | ) => void; 12 | onIconColorChange?: (data: any) => void; 13 | }; 14 | -------------------------------------------------------------------------------- /apiserver/plane/api/serializers/integration/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import IntegrationSerializer, WorkspaceIntegrationSerializer 2 | from .github import ( 3 | GithubRepositorySerializer, 4 | GithubRepositorySyncSerializer, 5 | GithubIssueSyncSerializer, 6 | GithubCommentSyncSerializer, 7 | ) 8 | from .slack import SlackProjectSyncSerializer -------------------------------------------------------------------------------- /apps/app/components/integration/index.ts: -------------------------------------------------------------------------------- 1 | // layout 2 | export * from "./delete-import-modal"; 3 | export * from "./guide"; 4 | export * from "./single-import"; 5 | export * from "./single-integration-card"; 6 | 7 | // github 8 | export * from "./github"; 9 | // jira 10 | export * from "./jira"; 11 | // slack 12 | export * from "./slack"; 13 | -------------------------------------------------------------------------------- /apps/app/components/issues/sidebar-select/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./assignee"; 2 | export * from "./blocked"; 3 | export * from "./blocker"; 4 | export * from "./cycle"; 5 | export * from "./estimate"; 6 | export * from "./label"; 7 | export * from "./module"; 8 | export * from "./parent"; 9 | export * from "./priority"; 10 | export * from "./state"; 11 | -------------------------------------------------------------------------------- /apps/app/components/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./select"; 2 | export * from "./sidebar-select"; 3 | export * from "./delete-module-modal"; 4 | export * from "./form"; 5 | export * from "./gantt-chart"; 6 | export * from "./modal"; 7 | export * from "./modules-list-gantt-chart"; 8 | export * from "./sidebar"; 9 | export * from "./single-module-card"; 10 | -------------------------------------------------------------------------------- /apps/app/components/ui/icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = { 4 | iconName: string; 5 | className?: string; 6 | }; 7 | 8 | export const Icon: React.FC = ({ iconName, className = "" }) => ( 9 | 10 | {iconName} 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -x 3 | 4 | # Replace the statically built BUILT_NEXT_PUBLIC_API_BASE_URL with run-time NEXT_PUBLIC_API_BASE_URL 5 | # NOTE: if these values are the same, this will be skipped. 6 | /usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL" 7 | 8 | echo "Starting Plane Frontend.." 9 | node apps/app/server.js -------------------------------------------------------------------------------- /apps/app/components/integration/github/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./import-configure"; 3 | export * from "./import-confirm"; 4 | export * from "./import-data"; 5 | export * from "./import-users"; 6 | export * from "./repo-details"; 7 | export * from "./root"; 8 | export * from "./select-repository"; 9 | export * from "./single-user-select"; 10 | -------------------------------------------------------------------------------- /apiserver/plane/api/views/integration/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import IntegrationViewSet, WorkspaceIntegrationViewSet 2 | from .github import ( 3 | GithubRepositorySyncViewSet, 4 | GithubIssueSyncViewSet, 5 | BulkCreateGithubIssueSyncEndpoint, 6 | GithubCommentSyncViewSet, 7 | GithubRepositoriesEndpoint, 8 | ) 9 | from .slack import SlackProjectSyncViewSet 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/css-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import CssFileIcon from "public/attachment/css-icon.png"; 6 | 7 | export const CssIcon: React.FC = ({ width, height }) => ( 8 | CssFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/csv-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import CSVFileIcon from "public/attachment/csv-icon.png"; 6 | 7 | export const CsvIcon: React.FC = ({ width , height }) => ( 8 | CSVFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/doc-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import DocFileIcon from "public/attachment/doc-icon.png"; 6 | 7 | export const DocIcon: React.FC = ({ width , height }) => ( 8 | DocFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/img-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import ImgFileIcon from "public/attachment/img-icon.png"; 6 | 7 | export const ImgIcon: React.FC = ({ width , height }) => ( 8 | ImgFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/jpg-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import JpgFileIcon from "public/attachment/jpg-icon.png"; 6 | 7 | export const JpgIcon: React.FC = ({ width, height }) => ( 8 | JpgFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/png-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import PngFileIcon from "public/attachment/png-icon.png"; 6 | 7 | export const PngIcon: React.FC = ({ width, height }) => ( 8 | PngFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/svg-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import SvgFileIcon from "public/attachment/svg-icon.png"; 6 | 7 | export const SvgIcon: React.FC = ({ width, height }) => ( 8 | SvgFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/txt-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import TxtFileIcon from "public/attachment/txt-icon.png"; 6 | 7 | export const TxtIcon: React.FC = ({ width, height }) => ( 8 | TxtFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/js-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import JsFileIcon from "public/attachment/js-icon.png"; 6 | 7 | export const JavaScriptIcon: React.FC = ({ width, height }) => ( 8 | JsFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/pdf-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import PDFFileIcon from "public/attachment/pdf-icon.png"; 6 | 7 | export const PdfIcon: React.FC = ({ width , height }) => ( 8 | PDFFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/html-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import HtmlFileIcon from "public/attachment/html-icon.png"; 6 | 7 | export const HtmlIcon: React.FC = ({ width, height }) => ( 8 | HtmlFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/audio-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import AudioFileIcon from "public/attachment/audio-icon.png"; 6 | 7 | export const AudioIcon: React.FC = ({ width, height }) => ( 8 | AudioFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/figma-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import FigmaFileIcon from "public/attachment/figma-icon.png"; 6 | 7 | export const FigmaIcon: React.FC = ({ width , height }) => ( 8 | FigmaFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/sheet-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import SheetFileIcon from "public/attachment/excel-icon.png"; 6 | 7 | export const SheetIcon: React.FC = ({ width, height }) => ( 8 | SheetFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/components/icons/video-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import VideoFileIcon from "public/attachment/video-icon.png"; 6 | 7 | export const VideoIcon: React.FC = ({ width, height }) => ( 8 | VideoFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apps/app/layouts/default-layout/index.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | children: React.ReactNode; 3 | gradient?: boolean; 4 | }; 5 | 6 | const DefaultLayout: React.FC = ({ children, gradient = false }) => ( 7 |
8 | {children} 9 |
10 | ); 11 | 12 | export default DefaultLayout; 13 | -------------------------------------------------------------------------------- /apiserver/plane/api/serializers/api_token.py: -------------------------------------------------------------------------------- 1 | from .base import BaseSerializer 2 | from plane.db.models import APIToken 3 | 4 | 5 | class APITokenSerializer(BaseSerializer): 6 | class Meta: 7 | model = APIToken 8 | fields = [ 9 | "label", 10 | "user", 11 | "user_type", 12 | "workspace", 13 | "created_at", 14 | ] 15 | -------------------------------------------------------------------------------- /apps/app/components/icons/default-file-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import DefaultFileIcon from "public/attachment/default-icon.png"; 6 | 7 | export const DefaultIcon: React.FC = ({ width, height }) => ( 8 | DefaultFileIcon 9 | ); 10 | -------------------------------------------------------------------------------- /apiserver/plane/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for plane project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | """ 7 | 8 | import os 9 | 10 | from django.core.wsgi import get_wsgi_application 11 | 12 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 13 | 'plane.settings.production') 14 | 15 | application = get_wsgi_application() 16 | -------------------------------------------------------------------------------- /apps/app/components/icons/cmd-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | import type { Props } from "./types"; 5 | import CMDIcon from "public/mac-command.svg"; 6 | 7 | export const MacCommandIcon: React.FC = ({ width = "14", height = "14" }) => ( 8 | CMDIcon 9 | ); 10 | 11 | export default MacCommandIcon; 12 | -------------------------------------------------------------------------------- /apps/app/pages/error.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // layouts 4 | import DefaultLayout from "layouts/default-layout"; 5 | // types 6 | import type { NextPage } from "next"; 7 | 8 | const ErrorPage: NextPage = () => ( 9 | 10 |
11 |

Error!

12 |
13 |
14 | ); 15 | 16 | export default ErrorPage; 17 | -------------------------------------------------------------------------------- /apiserver/plane/api/serializers/asset.py: -------------------------------------------------------------------------------- 1 | from .base import BaseSerializer 2 | from plane.db.models import FileAsset 3 | 4 | 5 | class FileAssetSerializer(BaseSerializer): 6 | class Meta: 7 | model = FileAsset 8 | fields = "__all__" 9 | read_only_fields = [ 10 | "created_by", 11 | "updated_by", 12 | "created_at", 13 | "updated_at", 14 | ] 15 | -------------------------------------------------------------------------------- /apiserver/plane/api/serializers/notification.py: -------------------------------------------------------------------------------- 1 | # Module imports 2 | from .base import BaseSerializer 3 | from .user import UserLiteSerializer 4 | from plane.db.models import Notification 5 | 6 | class NotificationSerializer(BaseSerializer): 7 | triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by") 8 | 9 | class Meta: 10 | model = Notification 11 | fields = "__all__" 12 | 13 | -------------------------------------------------------------------------------- /replace-env-vars.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | FROM=$1 3 | TO=$2 4 | 5 | if [ "${FROM}" = "${TO}" ]; then 6 | echo "Nothing to replace, the value is already set to ${TO}." 7 | 8 | exit 0 9 | fi 10 | 11 | # Only peform action if $FROM and $TO are different. 12 | echo "Replacing all statically built instances of $FROM with this string $TO ." 13 | 14 | grep -R -la "${FROM}" apps/app/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}" 15 | -------------------------------------------------------------------------------- /apps/app/components/inbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./accept-issue-modal"; 2 | export * from "./decline-issue-modal"; 3 | export * from "./delete-issue-modal"; 4 | export * from "./filters-dropdown"; 5 | export * from "./filters-list"; 6 | export * from "./inbox-action-headers"; 7 | export * from "./inbox-issue-card"; 8 | export * from "./inbox-main-content"; 9 | export * from "./issues-list-sidebar"; 10 | export * from "./select-duplicate"; 11 | -------------------------------------------------------------------------------- /apps/app/components/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pages-list"; 2 | export * from "./create-update-block-inline"; 3 | export * from "./create-update-page-modal"; 4 | export * from "./delete-page-modal"; 5 | export * from "./page-form"; 6 | export * from "./pages-view"; 7 | export * from "./single-page-block"; 8 | export * from "./single-page-detailed-item"; 9 | export * from "./single-page-list-item"; 10 | export * from "./create-block"; 11 | -------------------------------------------------------------------------------- /apps/app/components/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./board-view"; 2 | export * from "./calendar-view"; 3 | export * from "./filters"; 4 | export * from "./gantt-chart-view"; 5 | export * from "./list-view"; 6 | export * from "./modals"; 7 | export * from "./spreadsheet-view"; 8 | export * from "./theme"; 9 | export * from "./sidebar"; 10 | export * from "./issues-view"; 11 | export * from "./image-picker-popover"; 12 | export * from "./feeds"; 13 | -------------------------------------------------------------------------------- /apps/app/constants/module.ts: -------------------------------------------------------------------------------- 1 | export const MODULE_STATUS = [ 2 | { label: "Backlog", value: "backlog", color: "#5e6ad2" }, 3 | { label: "Planned", value: "planned", color: "#26b5ce" }, 4 | { label: "In Progress", value: "in-progress", color: "#f2c94c" }, 5 | { label: "Paused", value: "paused", color: "#ff6900" }, 6 | { label: "Completed", value: "completed", color: "#4cb782" }, 7 | { label: "Cancelled", value: "cancelled", color: "#cc1d10" }, 8 | ]; 9 | -------------------------------------------------------------------------------- /apps/app/components/gantt-chart/hooks/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | // types 3 | import { ChartContextReducer } from "../types"; 4 | // context 5 | import { ChartContext } from "../contexts"; 6 | 7 | export const useChart = (): ChartContextReducer => { 8 | const context = useContext(ChartContext); 9 | 10 | if (!context) { 11 | throw new Error("useChart must be used within a GanttChart"); 12 | } 13 | 14 | return context; 15 | }; 16 | -------------------------------------------------------------------------------- /apiserver/plane/api/serializers/integration/slack.py: -------------------------------------------------------------------------------- 1 | # Module imports 2 | from plane.api.serializers import BaseSerializer 3 | from plane.db.models import SlackProjectSync 4 | 5 | 6 | class SlackProjectSyncSerializer(BaseSerializer): 7 | class Meta: 8 | model = SlackProjectSync 9 | fields = "__all__" 10 | read_only_fields = [ 11 | "project", 12 | "workspace", 13 | "workspace_integration", 14 | ] 15 | -------------------------------------------------------------------------------- /apps/app/components/workspace/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./activity-graph"; 2 | export * from "./completed-issues-graph"; 3 | export * from "./create-workspace-form"; 4 | export * from "./delete-workspace-modal"; 5 | export * from "./help-section"; 6 | export * from "./issues-list"; 7 | export * from "./issues-pie-chart"; 8 | export * from "./issues-stats"; 9 | export * from "./settings-header"; 10 | export * from "./sidebar-dropdown"; 11 | export * from "./sidebar-menu"; 12 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0017_alter_workspace_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2023-01-07 17:49 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0016_auto_20230107_1735'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterUniqueTogether( 14 | name='workspace', 15 | unique_together=set(), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0030_alter_estimatepoint_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-05 14:17 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0029_auto_20230502_0126'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterUniqueTogether( 14 | name='estimatepoint', 15 | unique_together=set(), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /apps/app/components/project/settings-header.tsx: -------------------------------------------------------------------------------- 1 | import SettingsNavbar from "layouts/settings-navbar"; 2 | 3 | export const SettingsHeader = () => ( 4 |
5 |
6 |

Project Settings

7 |

8 | This information will be displayed to every member of the project. 9 |

10 |
11 | 12 |
13 | ); 14 | -------------------------------------------------------------------------------- /apps/app/components/workspace/settings-header.tsx: -------------------------------------------------------------------------------- 1 | import SettingsNavbar from "layouts/settings-navbar"; 2 | 3 | export const SettingsHeader = () => ( 4 |
5 |
6 |

Workspace Settings

7 |

8 | This information will be displayed to every member of the workspace. 9 |

10 |
11 | 12 |
13 | ); 14 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cp ./.env.example ./.env 3 | 4 | # Export for tr error in mac 5 | export LC_ALL=C 6 | export LC_CTYPE=C 7 | 8 | 9 | # Generate the NEXT_PUBLIC_API_BASE_URL with given IP 10 | echo -e "\nNEXT_PUBLIC_API_BASE_URL=$1" >> ./.env 11 | 12 | # Generate the SECRET_KEY that will be used by django 13 | echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./.env 14 | 15 | # WEB_URL for email redirection and image saving 16 | echo -e "WEB_URL=$1" >> ./.env 17 | -------------------------------------------------------------------------------- /apps/app/components/ui/dropdowns/types.d.ts: -------------------------------------------------------------------------------- 1 | export type DropdownProps = { 2 | buttonClassName?: string; 3 | className?: string; 4 | customButton?: JSX.Element; 5 | disabled?: boolean; 6 | input?: boolean; 7 | label?: string | JSX.Element; 8 | maxHeight?: "sm" | "rg" | "md" | "lg"; 9 | noChevron?: boolean; 10 | optionsClassName?: string; 11 | position?: "right" | "left"; 12 | selfPositioned?: boolean; 13 | verticalPosition?: "top" | "bottom"; 14 | width?: "auto" | string; 15 | }; 16 | -------------------------------------------------------------------------------- /apps/app/hooks/use-timer.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const TIMER = 30; 4 | 5 | const useTimer = (initialValue: number = TIMER) => { 6 | const [timer, setTimer] = useState(initialValue); 7 | 8 | useEffect(() => { 9 | const interval = setInterval(() => { 10 | setTimer((prev) => prev - 1); 11 | }, 1000); 12 | 13 | return () => clearInterval(interval); 14 | }, []); 15 | 16 | return { timer, setTimer }; 17 | }; 18 | 19 | export default useTimer; 20 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0008_label_colour.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.14 on 2022-11-29 19:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0007_label_parent'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='label', 15 | name='colour', 16 | field=models.CharField(blank=True, max_length=255), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /apps/app/helpers/common.helper.ts: -------------------------------------------------------------------------------- 1 | export const debounce = (func: any, wait: number, immediate: boolean = false) => { 2 | let timeout: any; 3 | 4 | return function executedFunction(...args: any) { 5 | const later = () => { 6 | timeout = null; 7 | if (!immediate) func(...args); 8 | }; 9 | 10 | const callNow = immediate && !timeout; 11 | 12 | clearTimeout(timeout); 13 | 14 | timeout = setTimeout(later, wait); 15 | 16 | if (callNow) func(...args); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /apps/app/lib/redirect.ts: -------------------------------------------------------------------------------- 1 | import Router from "next/router"; 2 | import type { NextPageContext } from "next"; 3 | 4 | const redirect = (context: NextPageContext, target: any) => { 5 | if (context.res) { 6 | // server 7 | // 303: "See other" 8 | context.res.writeHead(301, { Location: target }); 9 | context.res.end(); 10 | } else { 11 | // In the browser, we just pretend like this never even happened ;) 12 | Router.push(target); 13 | } 14 | }; 15 | 16 | export default redirect; 17 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0004_alter_state_sequence.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.14 on 2022-11-10 19:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0003_auto_20221109_2320'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='state', 15 | name='sequence', 16 | field=models.FloatField(default=65535), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0014_alter_workspacememberinvite_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2023-01-07 05:41 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0013_auto_20230107_0041'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterUniqueTogether( 14 | name='workspacememberinvite', 15 | unique_together={('email', 'workspace')}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /apps/app/components/search-listbox/types.d.ts: -------------------------------------------------------------------------------- 1 | type Value = any; 2 | 3 | export type Props = { 4 | title: string; 5 | multiple?: boolean; 6 | options?: Array<{ display: string; element?: JSX.Element; value: Value }>; 7 | onChange: (value: Value) => void; 8 | value: Value; 9 | icon?: JSX.Element; 10 | buttonClassName?: string; 11 | optionsClassName?: string; 12 | width?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl"; 13 | optionsFontsize?: "sm" | "md" | "lg" | "xl" | "2xl"; 14 | assignee?: boolean; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "eslint": "^7.23.0", 8 | "eslint-config-next": "13.0.0", 9 | "eslint-config-prettier": "^8.3.0", 10 | "eslint-plugin-react": "7.31.8", 11 | "eslint-config-turbo": "latest" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^4.7.4" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/app/public/site.webmanifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Plane", 3 | "short_name": "Plane", 4 | "description": "Plane helps you plan your issues, cycles, and product modules.", 5 | "start_url": ".", 6 | "display": "standalone", 7 | "background_color": "#f9fafb", 8 | "theme_color": "#3f76ff", 9 | "icons": [ 10 | { "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, 11 | { "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/app/hooks/use-debounce.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const useDebounce = (value: any, milliSeconds: number) => { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, milliSeconds); 10 | 11 | return () => { 12 | clearTimeout(handler); 13 | }; 14 | }, [value, milliSeconds]); 15 | 16 | return debouncedValue; 17 | }; 18 | 19 | export default useDebounce; 20 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0036_alter_workspace_organization_size.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.19 on 2023-07-05 07:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0035_auto_20230704_2225'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='workspace', 15 | name='organization_size', 16 | field=models.CharField(max_length=20), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /apiserver/plane/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from channels.routing import ProtocolTypeRouter 4 | from django.core.asgi import get_asgi_application 5 | 6 | django_asgi_app = get_asgi_application() 7 | 8 | 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production") 10 | # Initialize Django ASGI application early to ensure the AppRegistry 11 | # is populated before importing code that may import ORM models. 12 | 13 | 14 | application = ProtocolTypeRouter( 15 | { 16 | "http": get_asgi_application(), 17 | } 18 | ) 19 | -------------------------------------------------------------------------------- /apps/app/lib/cookie.ts: -------------------------------------------------------------------------------- 1 | export const convertCookieStringToObject = (cookieHeader: string | undefined) => { 2 | const list: any = {}; 3 | if (!cookieHeader) return list; 4 | 5 | cookieHeader.split(`;`).forEach(function (cookie) { 6 | // eslint-disable-next-line prefer-const 7 | let [name, ...rest] = cookie.split(`=`); 8 | name = name?.trim(); 9 | if (!name) return; 10 | const value = rest.join(`=`).trim(); 11 | if (!value) return; 12 | list[name] = decodeURIComponent(value); 13 | }); 14 | 15 | return list; 16 | }; 17 | -------------------------------------------------------------------------------- /nginx/nginx.conf.template: -------------------------------------------------------------------------------- 1 | events { } 2 | 3 | 4 | http { 5 | sendfile on; 6 | 7 | server { 8 | listen 80; 9 | root /www/data/; 10 | access_log /var/log/nginx/access.log; 11 | 12 | client_max_body_size ${FILE_SIZE_LIMIT}; 13 | 14 | location / { 15 | proxy_pass http://planefrontend:3000/; 16 | } 17 | 18 | location /api/ { 19 | proxy_pass http://planebackend:8000/api/; 20 | } 21 | 22 | location /${BUCKET_NAME}/ { 23 | proxy_pass http://plane-minio:9000/uploads/; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": "https://github.com/makeplane/plane.git", 3 | "license": "MIT", 4 | "private": true, 5 | "workspaces": [ 6 | "apps/*", 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "build": "turbo run build", 11 | "dev": "turbo run dev", 12 | "start": "turbo run start", 13 | "lint": "turbo run lint", 14 | "clean": "turbo run clean" 15 | }, 16 | "devDependencies": { 17 | "eslint-config-custom": "*", 18 | "prettier": "latest", 19 | "turbo": "latest" 20 | }, 21 | "packageManager": "yarn@1.22.19" 22 | } 23 | -------------------------------------------------------------------------------- /apps/app/components/ui/text-area/types.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { UseFormRegister, RegisterOptions, FieldError } from "react-hook-form"; 3 | 4 | export interface Props extends React.ComponentPropsWithoutRef<"textarea"> { 5 | label?: string; 6 | value?: string | number | readonly string[]; 7 | name: string; 8 | register?: UseFormRegister; 9 | mode?: "primary" | "transparent" | "secondary" | "disabled"; 10 | validations?: RegisterOptions; 11 | error?: FieldError | Merge>; 12 | noPadding?: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /apps/app/components/ui/input/types.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { UseFormRegister, RegisterOptions } from "react-hook-form"; 3 | 4 | export interface Props extends React.ComponentPropsWithoutRef<"input"> { 5 | label?: string; 6 | name: string; 7 | value?: string | number | readonly string[]; 8 | mode?: "primary" | "transparent" | "trueTransparent" | "secondary" | "disabled"; 9 | register?: UseFormRegister; 10 | validations?: RegisterOptions; 11 | error?: any; 12 | className?: string; 13 | size?: "rg" | "lg"; 14 | fullWidth?: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0026_alter_projectmember_view_props.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-04 21:50 2 | 3 | from django.db import migrations, models 4 | import plane.db.models.project 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('db', '0025_auto_20230331_0203'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='projectmember', 16 | name='view_props', 17 | field=models.JSONField(default=plane.db.models.project.get_default_props), 18 | ), 19 | ] -------------------------------------------------------------------------------- /apps/app/components/cycles/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cycles-list"; 2 | export * from "./active-cycle-details"; 3 | export * from "./active-cycle-stats"; 4 | export * from "./cycles-list-gantt-chart"; 5 | export * from "./cycles-view"; 6 | export * from "./delete-cycle-modal"; 7 | export * from "./form"; 8 | export * from "./gantt-chart"; 9 | export * from "./modal"; 10 | export * from "./select"; 11 | export * from "./sidebar"; 12 | export * from "./single-cycle-card"; 13 | export * from "./single-cycle-list"; 14 | export * from "./transfer-issues-modal"; 15 | export * from "./transfer-issues"; 16 | -------------------------------------------------------------------------------- /apps/app/helpers/emoji.helper.ts: -------------------------------------------------------------------------------- 1 | export const getRandomEmoji = () => { 2 | const emojis = [ 3 | "8986", 4 | "9200", 5 | "128204", 6 | "127773", 7 | "127891", 8 | "127947", 9 | "128076", 10 | "128077", 11 | "128187", 12 | "128188", 13 | "128512", 14 | "128522", 15 | "128578", 16 | ]; 17 | 18 | return emojis[Math.floor(Math.random() * emojis.length)]; 19 | }; 20 | 21 | export const renderEmoji = (emoji: string) => { 22 | if (!emoji) return; 23 | 24 | return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji)); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "scripts": { 8 | "lint": "eslint *.ts*" 9 | }, 10 | "devDependencies": { 11 | "@types/react": "^18.0.17", 12 | "@types/react-dom": "^18.0.6", 13 | "@typescript-eslint/eslint-plugin": "^5.51.0", 14 | "classnames": "^2.3.2", 15 | "eslint": "^7.32.0", 16 | "eslint-config-custom": "*", 17 | "next": "12.3.2", 18 | "react": "^18.2.0", 19 | "tsconfig": "*", 20 | "typescript": "4.7.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apiserver/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %}{% load i18n %} 2 | 3 | {% block title %}{{ title }} | {% trans 'plane Admin' %} {% endblock %} 4 | 5 | {% block branding %} 6 | 7 | 20 |

{% trans 'Plane Django Admin' %}

21 | 22 | 23 | {% endblock %}{% block nav-global %}{% endblock %} 24 | -------------------------------------------------------------------------------- /apiserver/templates/emails/exports/analytics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hey there,
4 | Your requested data export from Plane Analytics is now ready. The information has been compiled into a CSV format for your convenience.
5 | Please find the attachment and download the CSV file. This file can easily be imported into any spreadsheet program for further analysis.
6 | If you require any assistance or have any questions, please do not hesitate to contact us.
7 | Thank you 8 | 9 | -------------------------------------------------------------------------------- /apps/app/components/icons/backlog-state-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const BacklogStateIcon: React.FC = ({ 6 | width = "20", 7 | height = "20", 8 | className, 9 | color = "rgb(var(--color-text-200))", 10 | }) => ( 11 | 19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /apps/app/components/icons/completed-cycle-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const CompletedCycleIcon: React.FC = ({ 6 | width = "24", 7 | height = "24", 8 | className, 9 | color = "black", 10 | }) => ( 11 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /apps/app/hooks/use-outside-click-detector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | const useOutsideClickDetector = (ref: React.RefObject, callback: () => void) => { 4 | const handleClick = (event: MouseEvent) => { 5 | if (ref.current && !ref.current.contains(event.target as Node)) { 6 | callback(); 7 | } 8 | }; 9 | 10 | useEffect(() => { 11 | document.addEventListener("click", handleClick); 12 | 13 | return () => { 14 | document.removeEventListener("click", handleClick); 15 | }; 16 | }); 17 | }; 18 | 19 | export default useOutsideClickDetector; 20 | -------------------------------------------------------------------------------- /packages/ui/index.tsx: -------------------------------------------------------------------------------- 1 | // import * as React from "react"; 2 | // components 3 | // export * from "./breadcrumbs"; 4 | // export * from "./button"; 5 | // export * from "./custom-listbox"; 6 | // export * from "./custom-menu"; 7 | // export * from "./custom-select"; 8 | // export * from "./empty-space"; 9 | // export * from "./header-button"; 10 | // export * from "./input"; 11 | // export * from "./loader"; 12 | // export * from "./outline-button"; 13 | // export * from "./select"; 14 | // export * from "./spinner"; 15 | // export * from "./text-area"; 16 | // export * from "./tooltip"; 17 | export * from "./button"; 18 | -------------------------------------------------------------------------------- /apps/app/components/issues/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./comment"; 2 | export * from "./sidebar-select"; 3 | export * from "./view-select"; 4 | export * from "./activity"; 5 | export * from "./delete-issue-modal"; 6 | export * from "./description-form"; 7 | export * from "./form"; 8 | export * from "./main-content"; 9 | export * from "./modal"; 10 | export * from "./my-issues-list-item"; 11 | export * from "./parent-issues-list-modal"; 12 | export * from "./sidebar"; 13 | export * from "./sub-issues-list"; 14 | export * from "./attachment-upload"; 15 | export * from "./attachments"; 16 | export * from "./delete-attachment-modal"; 17 | -------------------------------------------------------------------------------- /apps/app/helpers/color.helper.ts: -------------------------------------------------------------------------------- 1 | export type TRgb = { r: number; g: number; b: number }; 2 | 3 | export const hexToRgb = (hex: string): TRgb => { 4 | const r = parseInt(hex.slice(1, 3), 16); 5 | const g = parseInt(hex.slice(3, 5), 16); 6 | const b = parseInt(hex.slice(5, 7), 16); 7 | 8 | return { r, g, b }; 9 | }; 10 | 11 | export const rgbToHex = (rgb: TRgb): string => { 12 | const { r, g, b } = rgb; 13 | 14 | const hexR = r.toString(16).padStart(2, "0"); 15 | const hexG = g.toString(16).padStart(2, "0"); 16 | const hexB = b.toString(16).padStart(2, "0"); 17 | 18 | return `#${hexR}${hexG}${hexB}`; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0006_alter_cycle_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.14 on 2022-11-16 14:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0005_auto_20221114_2127'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='cycle', 15 | name='status', 16 | field=models.CharField(choices=[('draft', 'Draft'), ('started', 'Started'), ('completed', 'Completed')], default='draft', max_length=255, verbose_name='Cycle Status'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0007_label_parent.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.14 on 2022-11-28 20:00 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('db', '0006_alter_cycle_status'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='label', 16 | name='parent', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_label', to='db.label'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /apiserver/plane/utils/html_processor.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | from html.parser import HTMLParser 3 | 4 | class MLStripper(HTMLParser): 5 | """ 6 | Markup Language Stripper 7 | """ 8 | def __init__(self): 9 | super().__init__() 10 | self.reset() 11 | self.strict = False 12 | self.convert_charrefs= True 13 | self.text = StringIO() 14 | 15 | def handle_data(self, d): 16 | self.text.write(d) 17 | 18 | def get_data(self): 19 | return self.text.getvalue() 20 | 21 | def strip_tags(html): 22 | s = MLStripper() 23 | s.feed(html) 24 | return s.get_data() 25 | -------------------------------------------------------------------------------- /apiserver/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault( 7 | 'DJANGO_SETTINGS_MODULE', 8 | 'plane.settings.production') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | -------------------------------------------------------------------------------- /apiserver/plane/api/serializers/integration/base.py: -------------------------------------------------------------------------------- 1 | # Module imports 2 | from plane.api.serializers import BaseSerializer 3 | from plane.db.models import Integration, WorkspaceIntegration 4 | 5 | 6 | class IntegrationSerializer(BaseSerializer): 7 | class Meta: 8 | model = Integration 9 | fields = "__all__" 10 | read_only_fields = [ 11 | "verified", 12 | ] 13 | 14 | 15 | class WorkspaceIntegrationSerializer(BaseSerializer): 16 | integration_detail = IntegrationSerializer(read_only=True, source="integration") 17 | 18 | class Meta: 19 | model = WorkspaceIntegration 20 | fields = "__all__" 21 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0032_auto_20230520_2015.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.19 on 2023-05-20 14:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0031_analyticview'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='project', 15 | old_name='icon', 16 | new_name='emoji', 17 | ), 18 | migrations.AddField( 19 | model_name='project', 20 | name='icon_prop', 21 | field=models.JSONField(null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /apps/app/components/ui/loader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | className?: string; 6 | }; 7 | 8 | const Loader = ({ children, className = "" }: Props) => ( 9 |
10 | {children} 11 |
12 | ); 13 | 14 | type ItemProps = { 15 | height?: string; 16 | width?: string; 17 | }; 18 | 19 | const Item: React.FC = ({ height = "auto", width = "auto" }) => ( 20 |
21 | ); 22 | 23 | Loader.Item = Item; 24 | 25 | export { Loader }; 26 | -------------------------------------------------------------------------------- /apiserver/plane/api/serializers/importer.py: -------------------------------------------------------------------------------- 1 | # Module imports 2 | from .base import BaseSerializer 3 | from .user import UserLiteSerializer 4 | from .project import ProjectLiteSerializer 5 | from .workspace import WorkspaceLiteSerializer 6 | from plane.db.models import Importer 7 | 8 | 9 | class ImporterSerializer(BaseSerializer): 10 | initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True) 11 | project_detail = ProjectLiteSerializer(source="project", read_only=True) 12 | workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) 13 | 14 | class Meta: 15 | model = Importer 16 | fields = "__all__" 17 | -------------------------------------------------------------------------------- /apps/app/helpers/attachment.helper.ts: -------------------------------------------------------------------------------- 1 | export const getFileExtension = (filename: string) => 2 | filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2); 3 | 4 | export const getFileName = (fileName: string) => { 5 | const dotIndex = fileName.lastIndexOf("."); 6 | 7 | const nameWithoutExtension = fileName.substring(0, dotIndex); 8 | 9 | return nameWithoutExtension; 10 | }; 11 | 12 | export const convertBytesToSize = (bytes: number) => { 13 | let size; 14 | 15 | if (bytes < 1024 * 1024) { 16 | size = Math.round(bytes / 1024) + " KB"; 17 | } else { 18 | size = Math.round(bytes / (1024 * 1024)) + " MB"; 19 | } 20 | 21 | return size; 22 | }; 23 | -------------------------------------------------------------------------------- /apiserver/plane/utils/issue_search.py: -------------------------------------------------------------------------------- 1 | # Python imports 2 | import re 3 | 4 | # Django imports 5 | from django.db.models import Q 6 | 7 | # Module imports 8 | from plane.db.models import Issue 9 | 10 | 11 | def search_issues(query, queryset): 12 | fields = ["name", "sequence_id"] 13 | q = Q() 14 | for field in fields: 15 | if field == "sequence_id": 16 | sequences = re.findall(r"\d+\.\d+|\d+", query) 17 | for sequence_id in sequences: 18 | q |= Q(**{"sequence_id": sequence_id}) 19 | else: 20 | q |= Q(**{f"{field}__icontains": query}) 21 | return queryset.filter( 22 | q, 23 | ).distinct() 24 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "include": ["src", "next-env.d.ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/app/components/icons/upcoming-cycle-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const UpcomingCycleIcon: React.FC = ({ 6 | width = "24", 7 | height = "24", 8 | className, 9 | color = "black", 10 | }) => ( 11 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /apps/app/components/icons/heartbeat-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const HeartbeatIcon: React.FC = ({ 6 | width = "24", 7 | height = "24", 8 | color = "rgb(var(--color-text-200))", 9 | className, 10 | }) => ( 11 | 19 | 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /apps/app/components/ui/graphs/pie-graph.tsx: -------------------------------------------------------------------------------- 1 | // nivo 2 | import { PieSvgProps, ResponsivePie } from "@nivo/pie"; 3 | // types 4 | import { TGraph } from "./types"; 5 | // constants 6 | import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; 7 | 8 | export const PieGraph: React.FC, "height" | "width">> = ({ 9 | height = "400px", 10 | width = "100%", 11 | margin, 12 | theme, 13 | ...rest 14 | }) => ( 15 |
16 | 22 |
23 | ); 24 | -------------------------------------------------------------------------------- /apps/app/hooks/use-issues.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | // services 3 | import userService from "services/user.service"; 4 | // types 5 | import type { IIssue } from "types"; 6 | // fetch-keys 7 | import { USER_ISSUE } from "constants/fetch-keys"; 8 | 9 | const useIssues = (workspaceSlug: string | undefined) => { 10 | // API Fetching 11 | const { data: myIssues, mutate: mutateMyIssues } = useSWR( 12 | workspaceSlug ? USER_ISSUE(workspaceSlug as string) : null, 13 | workspaceSlug ? () => userService.userIssues(workspaceSlug as string) : null 14 | ); 15 | 16 | return { 17 | myIssues: myIssues, 18 | mutateMyIssues, 19 | }; 20 | }; 21 | 22 | export default useIssues; 23 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next", "turbo", "prettier"], 3 | parser: "@typescript-eslint/parser", 4 | plugins: ["react", "@typescript-eslint"], 5 | settings: { 6 | next: { 7 | rootDir: ["app/", "docs/", "packages/*/"], 8 | }, 9 | }, 10 | rules: { 11 | "@next/next/no-html-link-for-pages": "off", 12 | "react/jsx-key": "off", 13 | "prefer-const": "error", 14 | "no-irregular-whitespace": "error", 15 | "no-trailing-spaces": "error", 16 | "no-duplicate-imports": "error", 17 | "arrow-body-style": ["error", "as-needed"], 18 | "react/self-closing-comp": ["error", { component: true, html: true }], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /apps/app/helpers/state.helper.ts: -------------------------------------------------------------------------------- 1 | // types 2 | import { IState, IStateResponse } from "types"; 3 | 4 | export const orderStateGroups = (unorderedStateGroups: IStateResponse) => 5 | Object.assign( 6 | { backlog: [], unstarted: [], started: [], completed: [], cancelled: [] }, 7 | unorderedStateGroups 8 | ); 9 | 10 | export const getStatesList = (stateGroups: any): IState[] => { 11 | // order the unordered state groups first 12 | const orderedStateGroups = orderStateGroups(stateGroups); 13 | 14 | // extract states from the groups and return them 15 | return Object.keys(orderedStateGroups) 16 | .map((group) => [...orderedStateGroups[group].map((state: IState) => state)]) 17 | .flat(); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/app/services/web-waitlist.service.ts: -------------------------------------------------------------------------------- 1 | // services 2 | import APIService from "services/api.service"; 3 | 4 | // types 5 | import { IWebWaitListResponse } from "types"; 6 | 7 | class WebWailtListServices extends APIService { 8 | constructor() { 9 | const origin = typeof window !== "undefined" ? window.location.origin || "" : ""; 10 | super(origin); 11 | } 12 | 13 | async create({ email }: { email: string }): Promise { 14 | return this.post(`/api/web-waitlist`, { email: email }) 15 | .then((response) => response?.data) 16 | .catch((error) => { 17 | throw error?.response; 18 | }); 19 | } 20 | } 21 | 22 | export default new WebWailtListServices(); 23 | -------------------------------------------------------------------------------- /apps/app/components/dnd/StrictModeDroppable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | // react beautiful dnd 4 | import { Droppable, DroppableProps } from "react-beautiful-dnd"; 5 | 6 | const StrictModeDroppable = ({ children, ...props }: DroppableProps) => { 7 | const [enabled, setEnabled] = useState(false); 8 | 9 | useEffect(() => { 10 | const animation = requestAnimationFrame(() => setEnabled(true)); 11 | 12 | return () => { 13 | cancelAnimationFrame(animation); 14 | setEnabled(false); 15 | }; 16 | }, []); 17 | 18 | if (!enabled) return null; 19 | 20 | return {children}; 21 | }; 22 | 23 | export default StrictModeDroppable; 24 | -------------------------------------------------------------------------------- /apps/app/constants/seo-variables.ts: -------------------------------------------------------------------------------- 1 | export const SITE_NAME = "Plane | Simple, extensible, open-source project management tool."; 2 | export const SITE_TITLE = "Plane | Simple, extensible, open-source project management tool."; 3 | export const SITE_DESCRIPTION = 4 | "Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind."; 5 | export const SITE_KEYWORDS = 6 | "software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration"; 7 | export const SITE_URL = "https://app.plane.so/"; 8 | export const TWITTER_USER_NAME = "Plane | Simple, extensible, open-source project management tool."; 9 | -------------------------------------------------------------------------------- /apiserver/plane/urls.py: -------------------------------------------------------------------------------- 1 | """plane URL Configuration 2 | 3 | """ 4 | 5 | # from django.contrib import admin 6 | from django.urls import path, include, re_path 7 | from django.views.generic import TemplateView 8 | 9 | from django.conf import settings 10 | 11 | # from django.conf.urls.static import static 12 | 13 | urlpatterns = [ 14 | # path("admin/", admin.site.urls), 15 | path("", TemplateView.as_view(template_name="index.html")), 16 | path("api/", include("plane.api.urls")), 17 | path("", include("plane.web.urls")), 18 | ] 19 | 20 | 21 | if settings.DEBUG: 22 | import debug_toolbar 23 | 24 | urlpatterns = [ 25 | re_path(r"^__debug__/", include(debug_toolbar.urls)), 26 | ] + urlpatterns 27 | -------------------------------------------------------------------------------- /apps/app/components/ui/graphs/scatter-plot-graph.tsx: -------------------------------------------------------------------------------- 1 | // nivo 2 | import { ResponsiveScatterPlot, ScatterPlotSvgProps } from "@nivo/scatterplot"; 3 | // types 4 | import { TGraph } from "./types"; 5 | // constants 6 | import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; 7 | 8 | export const ScatterPlotGraph: React.FC< 9 | TGraph & Omit, "height" | "width"> 10 | > = ({ height = "400px", width = "100%", margin, theme, ...rest }) => ( 11 |
12 | 18 |
19 | ); 20 | -------------------------------------------------------------------------------- /apiserver/plane/settings/redis.py: -------------------------------------------------------------------------------- 1 | import os 2 | import redis 3 | from django.conf import settings 4 | from urllib.parse import urlparse 5 | 6 | 7 | def redis_instance(): 8 | # connect to redis 9 | if ( 10 | settings.DOCKERIZED 11 | or os.environ.get("DJANGO_SETTINGS_MODULE", "plane.settings.production") 12 | == "plane.settings.local" 13 | ): 14 | ri = redis.Redis.from_url(settings.REDIS_URL, db=0) 15 | else: 16 | url = urlparse(settings.REDIS_URL) 17 | ri = redis.Redis( 18 | host=url.hostname, 19 | port=url.port, 20 | password=url.password, 21 | ssl=True, 22 | ssl_cert_reqs=None, 23 | ) 24 | 25 | return ri 26 | -------------------------------------------------------------------------------- /nginx/nginx-single-docker-image.conf: -------------------------------------------------------------------------------- 1 | upstream plane { 2 | server localhost:80; 3 | } 4 | 5 | error_log /var/log/nginx/error.log; 6 | 7 | server { 8 | listen 80; 9 | root /www/data/; 10 | access_log /var/log/nginx/access.log; 11 | location / { 12 | proxy_pass http://localhost:3000/; 13 | proxy_set_header Host $host; 14 | proxy_set_header X-Real-IP $remote_addr; 15 | } 16 | location /api/ { 17 | proxy_pass http://localhost:8000/api/; 18 | proxy_set_header Host $host; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | } 21 | error_page 500 502 503 504 /50x.html; 22 | location = /50x.html { 23 | root /usr/share/nginx/html; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apiserver/templates/emails/auth/forgot_password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

6 | Dear {{first_name}},

7 | We received a request to reset your password for your Plane account. 8 |

9 | To proceed with resetting your password, please click on the link below: 10 |
11 | {{forgot_password_url}} 12 |

13 | If you didn't request to reset your password, please ignore this email. Your account will remain secure. 14 |

15 | If you have any questions or need further assistance, please contact our support team. 16 |

17 | Thank you for using Plane. 18 |

19 | 20 | 21 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0005_auto_20221114_2127.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.14 on 2022-11-14 15:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0004_alter_state_sequence'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='cycle', 15 | name='end_date', 16 | field=models.DateField(blank=True, null=True, verbose_name='End Date'), 17 | ), 18 | migrations.AlterField( 19 | model_name='cycle', 20 | name='start_date', 21 | field=models.DateField(blank=True, null=True, verbose_name='Start Date'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /apps/app/constants/calendar.ts: -------------------------------------------------------------------------------- 1 | export const MONTHS_LIST = [ 2 | { value: 1, label: "January" }, 3 | { value: 2, label: "February" }, 4 | { value: 3, label: "March" }, 5 | { value: 4, label: "April" }, 6 | { value: 5, label: "May" }, 7 | { value: 6, label: "June" }, 8 | { value: 7, label: "July" }, 9 | { value: 8, label: "August" }, 10 | { value: 9, label: "September" }, 11 | { value: 10, label: "October" }, 12 | { value: 11, label: "November" }, 13 | { value: 12, label: "December" }, 14 | ]; 15 | 16 | export const YEARS_LIST = [ 17 | { value: "2021", label: "2021" }, 18 | { value: "2022", label: "2022" }, 19 | { value: "2023", label: "2023" }, 20 | { value: "2024", label: "2024" }, 21 | { value: "2025", label: "2025" }, 22 | ]; 23 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0019_auto_20230131_0049.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2023-01-30 19:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0018_auto_20230130_0119'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='issueactivity', 15 | name='new_value', 16 | field=models.TextField(blank=True, null=True, verbose_name='New Value'), 17 | ), 18 | migrations.AlterField( 19 | model_name='issueactivity', 20 | name='old_value', 21 | field=models.TextField(blank=True, null=True, verbose_name='Old Value'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /apps/app/helpers/graph.helper.ts: -------------------------------------------------------------------------------- 1 | export const generateYAxisTickValues = (data: number[]) => { 2 | if (!data || !Array.isArray(data) || data.length === 0) return []; 3 | 4 | const minValue = 0; 5 | const maxValue = Math.max(...data); 6 | 7 | const valueRange = maxValue - minValue; 8 | 9 | let tickInterval = 1; 10 | 11 | if (valueRange < 10) tickInterval = 1; 12 | else if (valueRange < 20) tickInterval = 2; 13 | else if (valueRange < 50) tickInterval = 5; 14 | else tickInterval = (Math.ceil(valueRange / 100) * 100) / 10; 15 | 16 | const tickValues: number[] = []; 17 | let tickValue = minValue; 18 | while (tickValue <= maxValue) { 19 | tickValues.push(tickValue); 20 | tickValue += tickInterval; 21 | } 22 | 23 | return tickValues; 24 | }; 25 | -------------------------------------------------------------------------------- /apps/app/types/importer/github-importer.d.ts: -------------------------------------------------------------------------------- 1 | export interface IGithubServiceImportFormData { 2 | metadata: { 3 | owner: string; 4 | name: string; 5 | repository_id: number; 6 | url: string; 7 | }; 8 | data: { 9 | users: { 10 | username: string; 11 | import: boolean | "invite" | "map"; 12 | email: string; 13 | }[]; 14 | }; 15 | config: { 16 | sync: boolean; 17 | }; 18 | project_id: string; 19 | } 20 | 21 | export interface IGithubRepoCollaborator { 22 | avatar_url: string; 23 | html_url: string; 24 | id: number; 25 | login: string; 26 | url: string; 27 | } 28 | 29 | export interface IGithubRepoInfo { 30 | issue_count: number; 31 | labels: number; 32 | collaborators: IGithubRepoCollaborator[]; 33 | } 34 | -------------------------------------------------------------------------------- /apiserver/plane/db/management/commands/wait_for_db.py: -------------------------------------------------------------------------------- 1 | import time 2 | from django.db import connections 3 | from django.db.utils import OperationalError 4 | from django.core.management import BaseCommand 5 | 6 | class Command(BaseCommand): 7 | """Django command to pause execution until db is available""" 8 | 9 | def handle(self, *args, **options): 10 | self.stdout.write('Waiting for database...') 11 | db_conn = None 12 | while not db_conn: 13 | try: 14 | db_conn = connections['default'] 15 | except OperationalError: 16 | self.stdout.write('Database unavailable, waititng 1 second...') 17 | time.sleep(1) 18 | 19 | self.stdout.write(self.style.SUCCESS('Database available!')) 20 | -------------------------------------------------------------------------------- /apiserver/bin/user_script.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import uuid 3 | 4 | sys.path.append("/code") 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production") 7 | import django 8 | 9 | django.setup() 10 | 11 | from plane.db.models import User 12 | 13 | 14 | def populate(): 15 | default_email = os.environ.get("DEFAULT_EMAIL", "captain@plane.so") 16 | default_password = os.environ.get("DEFAULT_PASSWORD", "password123") 17 | 18 | if not User.objects.filter(email=default_email).exists(): 19 | user = User.objects.create(email=default_email, username=uuid.uuid4().hex) 20 | user.set_password(default_password) 21 | user.save() 22 | print("User created") 23 | 24 | print("Success") 25 | 26 | 27 | if __name__ == "__main__": 28 | populate() 29 | -------------------------------------------------------------------------------- /apiserver/plane/api/views/release.py: -------------------------------------------------------------------------------- 1 | # Third party imports 2 | from rest_framework.response import Response 3 | from rest_framework import status 4 | from sentry_sdk import capture_exception 5 | 6 | # Module imports 7 | from .base import BaseAPIView 8 | from plane.utils.integrations.github import get_release_notes 9 | 10 | 11 | class ReleaseNotesEndpoint(BaseAPIView): 12 | def get(self, request): 13 | try: 14 | release_notes = get_release_notes() 15 | return Response(release_notes, status=status.HTTP_200_OK) 16 | except Exception as e: 17 | capture_exception(e) 18 | return Response( 19 | {"error": "Something went wrong please try again later"}, 20 | status=status.HTTP_400_BAD_REQUEST, 21 | ) 22 | -------------------------------------------------------------------------------- /apiserver/plane/utils/imports.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | import six 3 | 4 | 5 | def import_submodules(context, root_module, path): 6 | """ 7 | Import all submodules and register them in the ``context`` namespace. 8 | >>> import_submodules(locals(), __name__, __path__) 9 | """ 10 | for loader, module_name, is_pkg in pkgutil.walk_packages( 11 | path, 12 | root_module + 13 | '.'): 14 | # this causes a Runtime error with model conflicts 15 | # module = loader.find_module(module_name).load_module(module_name) 16 | module = __import__(module_name, globals(), locals(), ['__name__']) 17 | for k, v in six.iteritems(vars(module)): 18 | if not k.startswith('_'): 19 | context[k] = v 20 | context[module_name] = module -------------------------------------------------------------------------------- /apps/app/hooks/use-workspaces.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import useSWR from "swr"; 4 | 5 | // services 6 | import workspaceService from "services/workspace.service"; 7 | // fetch-keys 8 | import { USER_WORKSPACES } from "constants/fetch-keys"; 9 | 10 | const useWorkspaces = () => { 11 | // router 12 | const router = useRouter(); 13 | const { workspaceSlug } = router.query; 14 | // API to fetch user information 15 | const { data, error, mutate } = useSWR(USER_WORKSPACES, () => workspaceService.userWorkspaces()); 16 | // active workspace 17 | const activeWorkspace = data?.find((w) => w.slug === workspaceSlug); 18 | 19 | return { 20 | workspaces: data, 21 | error, 22 | activeWorkspace, 23 | mutateWorkspaces: mutate, 24 | }; 25 | }; 26 | 27 | export default useWorkspaces; 28 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0003_auto_20221109_2320.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.14 on 2022-11-09 17:50 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('db', '0002_auto_20221104_2239'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='issueproperty', 17 | name='user', 18 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_property_user', to=settings.AUTH_USER_MODEL), 19 | ), 20 | migrations.AlterUniqueTogether( 21 | name='issueproperty', 22 | unique_together={('user', 'project')}, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0009_auto_20221208_0310.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.14 on 2022-12-13 17:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0008_label_colour'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='projectmember', 15 | name='view_props', 16 | field=models.JSONField(null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='state', 20 | name='group', 21 | field=models.CharField(choices=[('backlog', 'Backlog'), ('unstarted', 'Unstarted'), ('started', 'Started'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='backlog', max_length=20), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /apps/app/components/analytics/select/y-axis.tsx: -------------------------------------------------------------------------------- 1 | // ui 2 | import { CustomSelect } from "components/ui"; 3 | // types 4 | import { TYAxisValues } from "types"; 5 | // constants 6 | import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; 7 | 8 | type Props = { 9 | value: TYAxisValues; 10 | onChange: () => void; 11 | }; 12 | 13 | export const SelectYAxis: React.FC = ({ value, onChange }) => ( 14 | {ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "None"}} 17 | onChange={onChange} 18 | width="w-full" 19 | > 20 | {ANALYTICS_Y_AXIS_VALUES.map((item) => ( 21 | 22 | {item.label} 23 | 24 | ))} 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /apps/app/sentry.client.config.js: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the browser. 2 | // The config you add here will be used whenever a page is visited. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN; 8 | 9 | Sentry.init({ 10 | dsn: SENTRY_DSN, 11 | environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development", 12 | // Adjust this value in production, or use tracesSampler for greater control 13 | tracesSampleRate: 1.0, 14 | // ... 15 | // Note: if you want to override the automatic release value, do not set a 16 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 17 | // that it will also get attached to your source maps 18 | }); 19 | -------------------------------------------------------------------------------- /apps/app/pages/api/slack-redirect.ts: -------------------------------------------------------------------------------- 1 | // pages/api/slack/authorize.js 2 | import axios from "axios"; 3 | import { NextApiRequest, NextApiResponse } from "next"; 4 | 5 | export default async function handleSlackAuthorize(req: NextApiRequest, res: NextApiResponse) { 6 | const { code } = req.body; 7 | 8 | if (!code || code === "") return res.status(400).json({ message: "Code is required" }); 9 | 10 | const response = await axios({ 11 | method: "post", 12 | url: "https://slack.com/api/oauth.v2.access", 13 | params: { 14 | client_id: process.env.NEXT_PUBLIC_SLACK_CLIENT_ID, 15 | client_secret: process.env.NEXT_PUBLIC_SLACK_CLIENT_SECRET, 16 | code, 17 | }, 18 | }); 19 | 20 | // if (response?.data?.ok) 21 | res.status(200).json(response.data); 22 | // else res.status(404).json(response.data); 23 | } 24 | -------------------------------------------------------------------------------- /apps/app/sentry.server.config.js: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN; 8 | 9 | Sentry.init({ 10 | dsn: SENTRY_DSN, 11 | environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development", 12 | // Adjust this value in production, or use tracesSampler for greater control 13 | tracesSampleRate: 1.0, 14 | // ... 15 | // Note: if you want to override the automatic release value, do not set a 16 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 17 | // that it will also get attached to your source maps 18 | }); 19 | -------------------------------------------------------------------------------- /apps/app/hooks/gantt-chart/issue-view.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | 3 | // services 4 | import issuesService from "services/issues.service"; 5 | // fetch-keys 6 | import { PROJECT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys"; 7 | 8 | const useGanttChartIssues = (workspaceSlug: string | undefined, projectId: string | undefined) => { 9 | // all issues under the workspace and project 10 | const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( 11 | workspaceSlug && projectId ? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId) : null, 12 | workspaceSlug && projectId 13 | ? () => issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString()) 14 | : null 15 | ); 16 | 17 | return { 18 | ganttIssues, 19 | mutateGanttIssues, 20 | }; 21 | }; 22 | 23 | export default useGanttChartIssues; 24 | -------------------------------------------------------------------------------- /apiserver/plane/db/models/analytic.py: -------------------------------------------------------------------------------- 1 | # Django models 2 | from django.db import models 3 | from django.conf import settings 4 | 5 | from .base import BaseModel 6 | 7 | 8 | class AnalyticView(BaseModel): 9 | workspace = models.ForeignKey( 10 | "db.Workspace", related_name="analytics", on_delete=models.CASCADE 11 | ) 12 | name = models.CharField(max_length=255) 13 | description = models.TextField(blank=True) 14 | query = models.JSONField() 15 | query_dict = models.JSONField(default=dict) 16 | 17 | class Meta: 18 | verbose_name = "Analytic" 19 | verbose_name_plural = "Analytics" 20 | db_table = "analytic_views" 21 | ordering = ("-created_at",) 22 | 23 | def __str__(self): 24 | """Return name of the analytic view""" 25 | return f"{self.name} <{self.workspace.name}>" 26 | -------------------------------------------------------------------------------- /apps/app/constants/graph.ts: -------------------------------------------------------------------------------- 1 | // nivo 2 | import { Theme } from "@nivo/core"; 3 | 4 | export const CHARTS_THEME: Theme = { 5 | background: "transparent", 6 | textColor: "rgb(var(--color-text-200))", 7 | axis: { 8 | domain: { 9 | line: { 10 | stroke: "rgb(var(--color-background-80))", 11 | strokeWidth: 0.5, 12 | }, 13 | }, 14 | }, 15 | tooltip: { 16 | container: { 17 | background: "rgb(var(--color-background-80))", 18 | color: "rgb(var(--color-text-200))", 19 | fontSize: "0.8rem", 20 | border: "1px solid rgb(var(--color-border-300))", 21 | }, 22 | }, 23 | grid: { 24 | line: { 25 | stroke: "rgb(var(--color-border-100))", 26 | }, 27 | }, 28 | }; 29 | 30 | export const DEFAULT_MARGIN = { 31 | top: 50, 32 | right: 50, 33 | bottom: 50, 34 | left: 50, 35 | }; 36 | -------------------------------------------------------------------------------- /apps/app/components/core/gantt-chart-view/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | // components 4 | import { CycleIssuesGanttChartView } from "components/cycles"; 5 | import { IssueGanttChartView } from "components/issues/gantt-chart"; 6 | import { ModuleIssuesGanttChartView } from "components/modules"; 7 | import { ViewIssuesGanttChartView } from "components/views"; 8 | 9 | export const GanttChartView = () => { 10 | const router = useRouter(); 11 | const { cycleId, moduleId, viewId } = router.query; 12 | 13 | return ( 14 | <> 15 | {cycleId ? ( 16 | 17 | ) : moduleId ? ( 18 | 19 | ) : viewId ? ( 20 | 21 | ) : ( 22 | 23 | )} 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /apps/app/sentry.edge.config.js: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever middleware or an Edge route handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN; 8 | 9 | Sentry.init({ 10 | dsn: SENTRY_DSN, 11 | environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development", 12 | // Adjust this value in production, or use tracesSampler for greater control 13 | tracesSampleRate: 1.0, 14 | // ... 15 | // Note: if you want to override the automatic release value, do not set a 16 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 17 | // that it will also get attached to your source maps 18 | }); 19 | -------------------------------------------------------------------------------- /apps/app/styles/nprogress.css: -------------------------------------------------------------------------------- 1 | #nprogress { 2 | pointer-events: none; 3 | } 4 | 5 | #nprogress .bar { 6 | background: #3f76ff; 7 | 8 | position: fixed; 9 | z-index: 1031; 10 | top: 0; 11 | left: 0; 12 | 13 | width: 100%; 14 | height: 0.2rem; 15 | } 16 | 17 | /* for blur effect */ 18 | #nprogress .peg { 19 | display: block; 20 | position: absolute; 21 | right: 0px; 22 | width: 100px; 23 | height: 100%; 24 | box-shadow: 0 0 10px #3f76ff, 0 0 5px #3f76ff; 25 | opacity: 1; 26 | 27 | -webkit-transform: rotate(3deg) translate(0px, -4px); 28 | -ms-transform: rotate(3deg) translate(0px, -4px); 29 | transform: rotate(3deg) translate(0px, -4px); 30 | } 31 | 32 | .nprogress-custom-parent { 33 | overflow: hidden; 34 | position: relative; 35 | } 36 | 37 | .nprogress-custom-parent #nprogress .bar { 38 | position: absolute; 39 | } 40 | -------------------------------------------------------------------------------- /apps/app/types/importer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./github-importer"; 2 | export * from "./jira-importer"; 3 | 4 | import { IProjectLite } from "types/projects"; 5 | // types 6 | import { IUserLite } from "types/users"; 7 | 8 | export interface IImporterService { 9 | created_at: string; 10 | config: { 11 | sync: boolean; 12 | }; 13 | created_by: string | null; 14 | data: { 15 | users: []; 16 | }; 17 | id: string; 18 | initiated_by: string; 19 | initiated_by_detail: IUserLite; 20 | metadata: { 21 | name: string; 22 | owner: string; 23 | repository_id: number; 24 | url: string; 25 | }; 26 | project: string; 27 | project_detail: IProjectLite; 28 | service: string; 29 | status: "processing" | "completed" | "failed"; 30 | updated_at: string; 31 | updated_by: string; 32 | token: string; 33 | workspace: string; 34 | } 35 | -------------------------------------------------------------------------------- /apps/app/pages/api/unsplash.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | // TODO: remove NEXT_PUBLIC_ prefix from env variable 4 | const unsplashKey = process.env.NEXT_PUBLIC_UNSPLASH_ACCESS; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | const { query, page, per_page = 20 } = req.query; 8 | 9 | const url = query 10 | ? `https://api.unsplash.com/search/photos/?client_id=${unsplashKey}&query=${query}&page=${page}&per_page=${per_page}` 11 | : `https://api.unsplash.com/photos/?client_id=${unsplashKey}&page=${page}&per_page=${per_page}`; 12 | 13 | const response = await fetch(url, { 14 | method: "GET", 15 | headers: { 16 | "Content-Type": "application/json", 17 | }, 18 | }); 19 | 20 | const data = await response.json(); 21 | 22 | res.status(200).json(data); 23 | } 24 | -------------------------------------------------------------------------------- /nginx/supervisor.conf: -------------------------------------------------------------------------------- 1 | [supervisord] ## This is the main process for the Supervisor 2 | nodaemon=true 3 | 4 | [program:node] 5 | command=sh /usr/local/bin/start.sh 6 | autostart=true 7 | autorestart=true 8 | stderr_logfile=/var/log/node.err.log 9 | stdout_logfile=/var/log/node.out.log 10 | 11 | [program:python] 12 | directory=/code 13 | command=sh bin/takeoff 14 | autostart=true 15 | autorestart=true 16 | stderr_logfile=/var/log/python.err.log 17 | stdout_logfile=/var/log/python.out.log 18 | 19 | [program:nginx] 20 | command=nginx -g "daemon off;" 21 | autostart=true 22 | autorestart=true 23 | stderr_logfile=/var/log/nginx.err.log 24 | stdout_logfile=/var/log/nginx.out.log 25 | 26 | [program:worker] 27 | directory=/code 28 | command=sh bin/worker 29 | autostart=true 30 | autorestart=true 31 | stderr_logfile=/var/log/worker.err.log 32 | stdout_logfile=/var/log/worker.out.log -------------------------------------------------------------------------------- /apiserver/requirements/base.txt: -------------------------------------------------------------------------------- 1 | # base requirements 2 | 3 | Django==4.2.3 4 | django-braces==1.15.0 5 | django-taggit==4.0.0 6 | psycopg==3.1.9 7 | django-oauth-toolkit==2.3.0 8 | mistune==3.0.1 9 | djangorestframework==3.14.0 10 | redis==4.6.0 11 | django-nested-admin==4.0.2 12 | django-cors-headers==4.1.0 13 | whitenoise==6.5.0 14 | django-allauth==0.54.0 15 | faker==18.11.2 16 | django-filter==23.2 17 | jsonmodels==2.6.0 18 | djangorestframework-simplejwt==5.2.2 19 | sentry-sdk==1.27.0 20 | django-s3-storage==0.14.0 21 | django-crum==0.7.9 22 | django-guardian==2.4.0 23 | dj_rest_auth==2.2.5 24 | google-auth==2.21.0 25 | google-api-python-client==2.92.0 26 | django-redis==5.3.0 27 | uvicorn==0.22.0 28 | channels==4.0.0 29 | openai==0.27.8 30 | slack-sdk==3.21.3 31 | celery==5.3.1 32 | django_celery_beat==2.5.0 33 | psycopg-binary==3.1.9 34 | psycopg-c==3.1.9 35 | scout-apm==2.26.1 -------------------------------------------------------------------------------- /apps/app/public/mac-command.svg: -------------------------------------------------------------------------------- 1 | 2 | mac-command -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0015_auto_20230107_1636.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2023-01-07 11:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0014_alter_workspacememberinvite_unique_together'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='issuecomment', 15 | old_name='comment', 16 | new_name='comment_stripped', 17 | ), 18 | migrations.AddField( 19 | model_name='issuecomment', 20 | name='comment_html', 21 | field=models.TextField(blank=True), 22 | ), 23 | migrations.AddField( 24 | model_name='issuecomment', 25 | name='comment_json', 26 | field=models.JSONField(blank=True, null=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /apps/app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme_color": "#3579f6", 3 | "background_color": "#ffffff", 4 | "display": "standalone", 5 | "scope": "/", 6 | "start_url": "/", 7 | "name": "Plane | Accelerate software development with peace.", 8 | "short_name": "Plane", 9 | "description": "Plane accelerated the software development by order of magnitude for agencies and product companies.", 10 | "icons": [ 11 | { 12 | "src": "/icon-192x192.png", 13 | "sizes": "192x192", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "/icon-256x256.png", 18 | "sizes": "256x256", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "/icon-384x384.png", 23 | "sizes": "384x384", 24 | "type": "image/png" 25 | }, 26 | { 27 | "src": "/icon-512x512.png", 28 | "sizes": "512x512", 29 | "type": "image/png" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /apps/app/types/state.d.ts: -------------------------------------------------------------------------------- 1 | import { IProject, IProjectLite, IWorkspaceLite } from "types"; 2 | 3 | export type TStateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; 4 | 5 | export interface IState { 6 | readonly id: string; 7 | color: string; 8 | readonly created_at: Date; 9 | readonly created_by: string; 10 | default: boolean; 11 | description: string; 12 | group: TStateGroup; 13 | name: string; 14 | project: string; 15 | readonly project_detail: IProjectLite; 16 | sequence: number; 17 | readonly slug: string; 18 | readonly updated_at: Date; 19 | readonly updated_by: string; 20 | workspace: string; 21 | workspace_detail: IWorkspaceLite; 22 | } 23 | 24 | export interface IStateLite { 25 | color: string; 26 | group: TStateGroup; 27 | id: string; 28 | name: string; 29 | } 30 | 31 | export interface IStateResponse { 32 | [key: string]: IState[]; 33 | } 34 | -------------------------------------------------------------------------------- /apiserver/plane/api/serializers/state.py: -------------------------------------------------------------------------------- 1 | # Module imports 2 | from .base import BaseSerializer 3 | from .workspace import WorkspaceLiteSerializer 4 | from .project import ProjectLiteSerializer 5 | 6 | from plane.db.models import State 7 | 8 | 9 | class StateSerializer(BaseSerializer): 10 | workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") 11 | project_detail = ProjectLiteSerializer(read_only=True, source="project") 12 | 13 | class Meta: 14 | model = State 15 | fields = "__all__" 16 | read_only_fields = [ 17 | "workspace", 18 | "project", 19 | ] 20 | 21 | 22 | class StateLiteSerializer(BaseSerializer): 23 | class Meta: 24 | model = State 25 | fields = [ 26 | "id", 27 | "name", 28 | "color", 29 | "group", 30 | ] 31 | read_only_fields = fields 32 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0016_auto_20230107_1735.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2023-01-07 12:05 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import plane.db.models.asset 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('db', '0015_auto_20230107_1636'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='fileasset', 17 | name='workspace', 18 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='db.workspace'), 19 | ), 20 | migrations.AlterField( 21 | model_name='fileasset', 22 | name='asset', 23 | field=models.FileField(upload_to=plane.db.models.asset.get_upload_path, validators=[plane.db.models.asset.file_size]), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /apps/app/types/estimate.d.ts: -------------------------------------------------------------------------------- 1 | export interface IEstimate { 2 | id: string; 3 | created_at: Date; 4 | updated_at: Date; 5 | name: string; 6 | description: string; 7 | created_by: string; 8 | updated_by: string; 9 | points: IEstimatePoint[]; 10 | project: string; 11 | project_detail: IProject; 12 | workspace: string; 13 | workspace_detail: IWorkspace; 14 | } 15 | 16 | export interface IEstimatePoint { 17 | id: string; 18 | created_at: string; 19 | created_by: string; 20 | description: string; 21 | estimate: string; 22 | key: number; 23 | project: string; 24 | updated_at: string; 25 | updated_by: string; 26 | value: string; 27 | workspace: string; 28 | } 29 | 30 | export interface IEstimateFormData { 31 | estimate: { 32 | name: string; 33 | description: string; 34 | }; 35 | estimate_points: { 36 | id?: string; 37 | key: number; 38 | value: string; 39 | }[]; 40 | } 41 | -------------------------------------------------------------------------------- /apps/app/hooks/use-reload-confirmation.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | const useReloadConfirmations = (message?: string) => { 4 | const [showAlert, setShowAlert] = useState(false); 5 | 6 | const handleBeforeUnload = useCallback( 7 | (event: BeforeUnloadEvent) => { 8 | event.preventDefault(); 9 | event.returnValue = ""; 10 | return message ?? "Are you sure you want to leave?"; 11 | }, 12 | [message] 13 | ); 14 | 15 | useEffect(() => { 16 | if (!showAlert) { 17 | window.removeEventListener("beforeunload", handleBeforeUnload); 18 | return; 19 | } 20 | 21 | window.addEventListener("beforeunload", handleBeforeUnload); 22 | return () => window.removeEventListener("beforeunload", handleBeforeUnload); 23 | }, [handleBeforeUnload, showAlert]); 24 | 25 | return { setShowAlert }; 26 | }; 27 | 28 | export default useReloadConfirmations; 29 | -------------------------------------------------------------------------------- /apps/app/components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./buttons"; 2 | export * from "./dropdowns"; 3 | export * from "./graphs"; 4 | export * from "./input"; 5 | export * from "./text-area"; 6 | export * from "./avatar"; 7 | export * from "./date"; 8 | export * from "./datepicker"; 9 | export * from "./empty-space"; 10 | export * from "./empty-state"; 11 | export * from "./icon"; 12 | export * from "./labels-list"; 13 | export * from "./linear-progress-indicator"; 14 | export * from "./loader"; 15 | export * from "./multi-level-dropdown"; 16 | export * from "./multi-level-select"; 17 | export * from "./progress-bar"; 18 | export * from "./spinner"; 19 | export * from "./tooltip"; 20 | export * from "./toggle-switch"; 21 | export * from "./markdown-to-component"; 22 | export * from "./product-updates-modal"; 23 | export * from "./integration-and-import-export-banner"; 24 | export * from "./range-datepicker"; 25 | export * from "./circular-progress"; 26 | -------------------------------------------------------------------------------- /apps/app/components/integration/github/auth.tsx: -------------------------------------------------------------------------------- 1 | // hooks 2 | import useIntegrationPopup from "hooks/use-integration-popup"; 3 | // ui 4 | import { PrimaryButton } from "components/ui"; 5 | // types 6 | import { IWorkspaceIntegration } from "types"; 7 | 8 | type Props = { 9 | workspaceIntegration: false | IWorkspaceIntegration | undefined; 10 | provider: string | undefined; 11 | }; 12 | 13 | export const GithubAuth: React.FC = ({ workspaceIntegration, provider }) => { 14 | const { startAuth, isConnecting } = useIntegrationPopup(provider); 15 | 16 | return ( 17 |
18 | {workspaceIntegration && workspaceIntegration?.id ? ( 19 | Successfully Connected 20 | ) : ( 21 | 22 | {isConnecting ? "Connecting..." : "Connect"} 23 | 24 | )} 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /apps/app/components/ui/integration-and-import-export-banner.tsx: -------------------------------------------------------------------------------- 1 | import { ExclamationIcon } from "components/icons"; 2 | 3 | type Props = { 4 | bannerName: string; 5 | }; 6 | 7 | export const IntegrationAndImportExportBanner: React.FC = ({ bannerName }) => ( 8 |
9 |

{bannerName}

10 |
11 | 12 |

13 | Integrations and importers are only available on the cloud version. We plan to open-source 14 | our SDKs in the near future so that the community can request or contribute integrations as 15 | needed. 16 |

17 |
18 |
19 | ); 20 | -------------------------------------------------------------------------------- /apps/app/components/pages/pages-list/all-pages-list.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import useSWR from "swr"; 4 | 5 | // services 6 | import pagesService from "services/pages.service"; 7 | // components 8 | import { PagesView } from "components/pages"; 9 | // types 10 | import { TPagesListProps } from "./types"; 11 | // fetch-keys 12 | import { ALL_PAGES_LIST } from "constants/fetch-keys"; 13 | 14 | export const AllPagesList: React.FC = ({ viewType }) => { 15 | const router = useRouter(); 16 | const { workspaceSlug, projectId } = router.query; 17 | 18 | const { data: pages } = useSWR( 19 | workspaceSlug && projectId ? ALL_PAGES_LIST(projectId as string) : null, 20 | workspaceSlug && projectId 21 | ? () => pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, "all") 22 | : null 23 | ); 24 | 25 | return ; 26 | }; 27 | -------------------------------------------------------------------------------- /apps/app/hooks/use-project-details.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import useSWR from "swr"; 4 | 5 | // services 6 | import projectService from "services/project.service"; 7 | // fetch-keys 8 | import { PROJECT_DETAILS } from "constants/fetch-keys"; 9 | 10 | const useProjectDetails = () => { 11 | const router = useRouter(); 12 | const { workspaceSlug, projectId } = router.query; 13 | 14 | const { 15 | data: projectDetails, 16 | error: projectDetailsError, 17 | mutate: mutateProjectDetails, 18 | } = useSWR( 19 | workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, 20 | workspaceSlug && projectId 21 | ? () => projectService.getProject(workspaceSlug as string, projectId as string) 22 | : null 23 | ); 24 | 25 | return { 26 | projectDetails, 27 | projectDetailsError, 28 | mutateProjectDetails, 29 | }; 30 | }; 31 | 32 | export default useProjectDetails; 33 | -------------------------------------------------------------------------------- /apps/app/components/icons/priority-icon.tsx: -------------------------------------------------------------------------------- 1 | export const getPriorityIcon = (priority: string | null, className?: string) => { 2 | if (!className || className === "") className = "text-xs flex items-center"; 3 | 4 | priority = priority?.toLowerCase() ?? null; 5 | 6 | switch (priority) { 7 | case "urgent": 8 | return error; 9 | case "high": 10 | return signal_cellular_alt; 11 | case "medium": 12 | return ( 13 | signal_cellular_alt_2_bar 14 | ); 15 | case "low": 16 | return ( 17 | signal_cellular_alt_1_bar 18 | ); 19 | default: 20 | return block; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /apps/app/components/emoji-icon-picker/helpers.ts: -------------------------------------------------------------------------------- 1 | export const saveRecentEmoji = (emoji: string) => { 2 | const recentEmojis = localStorage.getItem("recentEmojis"); 3 | if (recentEmojis) { 4 | const recentEmojisArray = recentEmojis.split(","); 5 | if (recentEmojisArray.includes(emoji)) { 6 | const index = recentEmojisArray.indexOf(emoji); 7 | recentEmojisArray.splice(index, 1); 8 | } 9 | recentEmojisArray.unshift(emoji); 10 | if (recentEmojisArray.length > 18) { 11 | recentEmojisArray.pop(); 12 | } 13 | localStorage.setItem("recentEmojis", recentEmojisArray.join(",")); 14 | } else { 15 | localStorage.setItem("recentEmojis", emoji); 16 | } 17 | }; 18 | 19 | export const getRecentEmojis = () => { 20 | const recentEmojis = localStorage.getItem("recentEmojis"); 21 | if (recentEmojis) { 22 | const recentEmojisArray = recentEmojis.split(","); 23 | return recentEmojisArray; 24 | } 25 | return []; 26 | }; 27 | -------------------------------------------------------------------------------- /apps/app/constants/project.ts: -------------------------------------------------------------------------------- 1 | export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" }; 2 | 3 | export const GROUP_CHOICES = { 4 | backlog: "Backlog", 5 | unstarted: "Unstarted", 6 | started: "Started", 7 | completed: "Completed", 8 | cancelled: "Cancelled", 9 | }; 10 | 11 | export const PRIORITIES = ["urgent", "high", "medium", "low", null]; 12 | 13 | export const MONTHS = [ 14 | "January", 15 | "February", 16 | "March", 17 | "April", 18 | "May", 19 | "June", 20 | "July", 21 | "August", 22 | "September", 23 | "October", 24 | "November", 25 | "December", 26 | ]; 27 | 28 | export const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 29 | 30 | export const PROJECT_AUTOMATION_MONTHS = [ 31 | { label: "1 Month", value: 1 }, 32 | { label: "3 Months", value: 3 }, 33 | { label: "6 Months", value: 6 }, 34 | { label: "9 Months", value: 9 }, 35 | { label: "12 Months", value: 12 }, 36 | ]; 37 | -------------------------------------------------------------------------------- /apps/app/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./users"; 2 | export * from "./workspace"; 3 | export * from "./cycles"; 4 | export * from "./projects"; 5 | export * from "./state"; 6 | export * from "./invitation"; 7 | export * from "./issues"; 8 | export * from "./modules"; 9 | export * from "./views"; 10 | export * from "./integration"; 11 | export * from "./pages"; 12 | export * from "./ai"; 13 | export * from "./estimate"; 14 | export * from "./importer"; 15 | export * from "./inbox"; 16 | export * from "./analytics"; 17 | export * from "./calendar"; 18 | export * from "./notifications"; 19 | export * from "./waitlist"; 20 | 21 | export type NestedKeyOf = { 22 | [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object 23 | ? ObjectType[Key] extends { pop: any; push: any } 24 | ? `${Key}` 25 | : `${Key}` | `${Key}.${NestedKeyOf}` 26 | : `${Key}`; 27 | }[keyof ObjectType & (string | number)]; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a feature to improve Plane 3 | title: "[feature]: " 4 | labels: [feature] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for taking the time to request a feature for Plane 10 | - type: checkboxes 11 | attributes: 12 | label: Is there an existing issue for this? 13 | description: Please search to see if an issue related to this feature request already exists 14 | options: 15 | - label: I have searched the existing issues 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Summary 20 | description: One paragraph description of the feature 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: Why should this be worked on? 26 | description: A concise description of the problems or use cases for this feature request 27 | validations: 28 | required: true -------------------------------------------------------------------------------- /apps/app/components/icons/single-comment-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const SingleCommentCard: React.FC = ({ 6 | width = "24", 7 | height = "24", 8 | className, 9 | color, 10 | }) => ( 11 | 19 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /apps/app/public/empty-state/issue-archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/app/styles/command-pallette.css: -------------------------------------------------------------------------------- 1 | [cmdk-group]:not(:first-child) { 2 | margin-top: 0.5rem; 3 | } 4 | 5 | [cmdk-group-heading] { 6 | color: rgb(var(--color-text-secondary)); 7 | font-size: 0.75rem; 8 | margin: 0 0 0.25rem 0.25rem; 9 | } 10 | 11 | [cmdk-item] { 12 | display: flex; 13 | align-items: center; 14 | justify-content: space-between; 15 | border-radius: 0.375rem; 16 | padding: 0.5rem; 17 | font-size: 0.825rem; 18 | line-height: 1.25rem; 19 | cursor: pointer; 20 | } 21 | 22 | [cmdk-item] kbd { 23 | height: 1.25rem; 24 | width: 1.25rem; 25 | display: grid; 26 | place-items: center; 27 | font-size: 0.75rem; 28 | line-height: 1rem; 29 | border-radius: 0.25rem; 30 | background-color: rgba(var(--color-background-100)); 31 | } 32 | 33 | [cmdk-item]:hover { 34 | background-color: rgba(var(--color-background-80)); 35 | } 36 | 37 | [cmdk-item][aria-selected="true"] { 38 | background-color: rgba(var(--color-background-80)); 39 | } 40 | -------------------------------------------------------------------------------- /apps/app/components/cycles/cycles-list/all-cycles-list.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import useSWR from "swr"; 4 | 5 | // services 6 | import cyclesService from "services/cycles.service"; 7 | // components 8 | import { CyclesView } from "components/cycles"; 9 | // fetch-keys 10 | import { CYCLES_LIST } from "constants/fetch-keys"; 11 | 12 | type Props = { 13 | viewType: string | null; 14 | }; 15 | 16 | export const AllCyclesList: React.FC = ({ viewType }) => { 17 | const router = useRouter(); 18 | const { workspaceSlug, projectId } = router.query; 19 | 20 | const { data: allCyclesList } = useSWR( 21 | workspaceSlug && projectId ? CYCLES_LIST(projectId.toString()) : null, 22 | workspaceSlug && projectId 23 | ? () => 24 | cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "all") 25 | : null 26 | ); 27 | 28 | return ; 29 | }; 30 | -------------------------------------------------------------------------------- /apps/app/components/pages/pages-list/favorite-pages-list.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import useSWR from "swr"; 4 | 5 | // services 6 | import pagesService from "services/pages.service"; 7 | // components 8 | import { PagesView } from "components/pages"; 9 | // types 10 | import { TPagesListProps } from "./types"; 11 | // fetch-keys 12 | import { FAVORITE_PAGES_LIST } from "constants/fetch-keys"; 13 | 14 | export const FavoritePagesList: React.FC = ({ viewType }) => { 15 | const router = useRouter(); 16 | const { workspaceSlug, projectId } = router.query; 17 | 18 | const { data: pages } = useSWR( 19 | workspaceSlug && projectId ? FAVORITE_PAGES_LIST(projectId as string) : null, 20 | workspaceSlug && projectId 21 | ? () => 22 | pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, "favorite") 23 | : null 24 | ); 25 | 26 | return ; 27 | }; 28 | -------------------------------------------------------------------------------- /apps/app/components/ui/labels-list.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type IssueLabelsListProps = { 4 | labels?: (string | undefined)[]; 5 | length?: number; 6 | showLength?: boolean; 7 | }; 8 | 9 | export const IssueLabelsList: React.FC = ({ 10 | labels, 11 | length = 5, 12 | showLength = true, 13 | }) => ( 14 | <> 15 | {labels && ( 16 | <> 17 | {labels.slice(0, length).map((color, index) => ( 18 |
19 | 26 |
27 | ))} 28 | {labels.length > length ? +{labels.length - length} : null} 29 | 30 | )} 31 | 32 | ); 33 | -------------------------------------------------------------------------------- /apps/app/types/views.d.ts: -------------------------------------------------------------------------------- 1 | export interface IView { 2 | id: string; 3 | access: string; 4 | created_at: Date; 5 | updated_at: Date; 6 | is_favorite: boolean; 7 | created_by: string; 8 | updated_by: string; 9 | name: string; 10 | description: string; 11 | query: IQuery; 12 | query_data: IQuery; 13 | project: string; 14 | workspace: string; 15 | } 16 | 17 | export interface IQuery { 18 | state: string[] | null; 19 | parent: string[] | null; 20 | priority: string[] | null; 21 | labels: string[] | null; 22 | assignees: string[] | null; 23 | created_by: string[] | null; 24 | name: string | null; 25 | created_at: [ 26 | { 27 | datetime: string; 28 | timeline: "before"; 29 | }, 30 | { 31 | datetime: string; 32 | timeline: "after"; 33 | } 34 | ]; 35 | updated_at: string[] | null; 36 | start_date: string[] | null; 37 | target_date: string[] | null; 38 | completed_at: string[] | null; 39 | type: string; 40 | } 41 | -------------------------------------------------------------------------------- /apps/app/components/cycles/cycles-list/draft-cycles-list.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import useSWR from "swr"; 4 | 5 | // services 6 | import cyclesService from "services/cycles.service"; 7 | // components 8 | import { CyclesView } from "components/cycles"; 9 | // fetch-keys 10 | import { DRAFT_CYCLES_LIST } from "constants/fetch-keys"; 11 | 12 | type Props = { 13 | viewType: string | null; 14 | }; 15 | 16 | export const DraftCyclesList: React.FC = ({ viewType }) => { 17 | const router = useRouter(); 18 | const { workspaceSlug, projectId } = router.query; 19 | 20 | const { data: draftCyclesList } = useSWR( 21 | workspaceSlug && projectId ? DRAFT_CYCLES_LIST(projectId.toString()) : null, 22 | workspaceSlug && projectId 23 | ? () => 24 | cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "draft") 25 | : null 26 | ); 27 | 28 | return ; 29 | }; 30 | -------------------------------------------------------------------------------- /apps/app/components/icons/current-cycle-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const CurrentCycleIcon: React.FC = ({ 6 | width = "24", 7 | height = "24", 8 | className, 9 | color = "black", 10 | }) => ( 11 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /apps/app/components/integration/jira/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./root"; 2 | export * from "./give-details"; 3 | export * from "./jira-project-detail"; 4 | export * from "./import-users"; 5 | export * from "./confirm-import"; 6 | 7 | import { IJiraImporterForm } from "types"; 8 | 9 | export type TJiraIntegrationSteps = 10 | | "import-configure" 11 | | "display-import-data" 12 | | "select-import-data" 13 | | "import-users" 14 | | "import-confirmation"; 15 | 16 | export interface IJiraIntegrationData { 17 | state: TJiraIntegrationSteps; 18 | } 19 | 20 | export const jiraFormDefaultValues: IJiraImporterForm = { 21 | metadata: { 22 | cloud_hostname: "", 23 | api_token: "", 24 | project_key: "", 25 | email: "", 26 | }, 27 | config: { 28 | epics_to_modules: false, 29 | }, 30 | data: { 31 | users: [], 32 | invite_users: true, 33 | total_issues: 0, 34 | total_labels: 0, 35 | total_modules: 0, 36 | total_states: 0, 37 | }, 38 | project_id: "", 39 | }; 40 | -------------------------------------------------------------------------------- /apiserver/plane/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | from celery import Celery 3 | from plane.settings.redis import redis_instance 4 | from celery.schedules import crontab 5 | 6 | # Set the default Django settings module for the 'celery' program. 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production") 8 | 9 | ri = redis_instance() 10 | 11 | app = Celery("plane") 12 | 13 | # Using a string here means the worker will not have to 14 | # pickle the object when using Windows. 15 | app.config_from_object("django.conf:settings", namespace="CELERY") 16 | 17 | app.conf.beat_schedule = { 18 | # Executes every day at 12 AM 19 | "check-every-day-to-archive-and-close": { 20 | "task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues", 21 | "schedule": crontab(hour=0, minute=0), 22 | }, 23 | } 24 | 25 | # Load task modules from all registered Django app configs. 26 | app.autodiscover_tasks() 27 | 28 | app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | 4 | ### NextJS ### 5 | # Dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # Testing 11 | /coverage 12 | 13 | # Next.js 14 | /.next/ 15 | /out/ 16 | 17 | # Production 18 | /build 19 | 20 | # Misc 21 | .DS_Store 22 | *.pem 23 | .history 24 | 25 | # Debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .pnpm-debug.log* 30 | 31 | # Local env files 32 | .env 33 | .env.local 34 | .env.development.local 35 | .env.test.local 36 | .env.production.local 37 | 38 | # Vercel 39 | .vercel 40 | 41 | # Turborepo 42 | .turbo 43 | 44 | ## Django ## 45 | venv 46 | .venv 47 | *.pyc 48 | staticfiles 49 | mediafiles 50 | .env 51 | .DS_Store 52 | 53 | node_modules/ 54 | assets/dist/ 55 | npm-debug.log 56 | yarn-error.log 57 | 58 | # Editor directories and files 59 | .idea 60 | *.suo 61 | *.ntvs* 62 | *.njsproj 63 | *.sln 64 | package-lock.json 65 | .vscode 66 | 67 | # Sentry 68 | .sentryclirc 69 | 70 | # lock files 71 | package-lock.json 72 | pnpm-lock.yaml 73 | pnpm-workspace.yaml -------------------------------------------------------------------------------- /apps/app/components/pages/pages-list/my-pages-list.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import useSWR from "swr"; 4 | 5 | // services 6 | import pagesService from "services/pages.service"; 7 | // components 8 | import { PagesView } from "components/pages"; 9 | // types 10 | import { TPagesListProps } from "./types"; 11 | // fetch-keys 12 | import { MY_PAGES_LIST } from "constants/fetch-keys"; 13 | 14 | export const MyPagesList: React.FC = ({ viewType }) => { 15 | const router = useRouter(); 16 | const { workspaceSlug, projectId } = router.query; 17 | 18 | const { data: pages } = useSWR( 19 | workspaceSlug && projectId ? MY_PAGES_LIST(projectId as string) : null, 20 | workspaceSlug && projectId 21 | ? () => 22 | pagesService.getPagesWithParams( 23 | workspaceSlug as string, 24 | projectId as string, 25 | "created_by_me" 26 | ) 27 | : null 28 | ); 29 | 30 | return ; 31 | }; 32 | -------------------------------------------------------------------------------- /apps/app/components/icons/state-group-icon.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BacklogStateIcon, 3 | CancelledStateIcon, 4 | CompletedStateIcon, 5 | StartedStateIcon, 6 | UnstartedStateIcon, 7 | } from "components/icons"; 8 | 9 | export const getStateGroupIcon = ( 10 | stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled", 11 | width = "20", 12 | height = "20", 13 | color?: string 14 | ) => { 15 | switch (stateGroup) { 16 | case "backlog": 17 | return ; 18 | case "unstarted": 19 | return ; 20 | case "started": 21 | return ; 22 | case "completed": 23 | return ; 24 | case "cancelled": 25 | return ; 26 | default: 27 | return <>; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /apps/app/hooks/gantt-chart/cycle-issues-view.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | 3 | // services 4 | import cyclesService from "services/cycles.service"; 5 | // fetch-keys 6 | import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; 7 | 8 | const useGanttChartCycleIssues = ( 9 | workspaceSlug: string | undefined, 10 | projectId: string | undefined, 11 | cycleId: string | undefined 12 | ) => { 13 | // all issues under the workspace and project 14 | const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( 15 | workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) : null, 16 | workspaceSlug && projectId && cycleId 17 | ? () => 18 | cyclesService.getCycleIssuesWithParams( 19 | workspaceSlug.toString(), 20 | projectId.toString(), 21 | cycleId.toString() 22 | ) 23 | : null 24 | ); 25 | 26 | return { 27 | ganttIssues, 28 | mutateGanttIssues, 29 | }; 30 | }; 31 | 32 | export default useGanttChartCycleIssues; 33 | -------------------------------------------------------------------------------- /apps/app/components/gantt-chart/root.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | // components 3 | import { ChartViewRoot } from "./chart"; 4 | // context 5 | import { ChartContextProvider } from "./contexts"; 6 | 7 | type GanttChartRootProps = { 8 | border?: boolean; 9 | title: null | string; 10 | loaderTitle: string; 11 | blocks: any; 12 | blockUpdateHandler: (data: any) => void; 13 | sidebarBlockRender: FC; 14 | blockRender: FC; 15 | }; 16 | 17 | export const GanttChartRoot: FC = ({ 18 | border = true, 19 | title = null, 20 | blocks, 21 | loaderTitle = "blocks", 22 | blockUpdateHandler, 23 | sidebarBlockRender, 24 | blockRender, 25 | }) => ( 26 | 27 | 36 | 37 | ); 38 | -------------------------------------------------------------------------------- /apps/app/components/pages/pages-list/other-pages-list.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import useSWR from "swr"; 4 | 5 | // services 6 | import pagesService from "services/pages.service"; 7 | // components 8 | import { PagesView } from "components/pages"; 9 | // types 10 | import { TPagesListProps } from "./types"; 11 | // fetch-keys 12 | import { OTHER_PAGES_LIST } from "constants/fetch-keys"; 13 | 14 | export const OtherPagesList: React.FC = ({ viewType }) => { 15 | const router = useRouter(); 16 | const { workspaceSlug, projectId } = router.query; 17 | 18 | const { data: pages } = useSWR( 19 | workspaceSlug && projectId ? OTHER_PAGES_LIST(projectId as string) : null, 20 | workspaceSlug && projectId 21 | ? () => 22 | pagesService.getPagesWithParams( 23 | workspaceSlug as string, 24 | projectId as string, 25 | "created_by_other" 26 | ) 27 | : null 28 | ); 29 | 30 | return ; 31 | }; 32 | -------------------------------------------------------------------------------- /apps/app/components/icons/bolt-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const BoltIcon: React.FC = ({ width = "24", height = "24", className }) => ( 6 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /apps/app/hooks/gantt-chart/module-issues-view.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | 3 | // services 4 | import modulesService from "services/modules.service"; 5 | // fetch-keys 6 | import { MODULE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; 7 | 8 | const useGanttChartModuleIssues = ( 9 | workspaceSlug: string | undefined, 10 | projectId: string | undefined, 11 | moduleId: string | undefined 12 | ) => { 13 | // all issues under the workspace and project 14 | const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( 15 | workspaceSlug && projectId && moduleId ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString()) : null, 16 | workspaceSlug && projectId && moduleId 17 | ? () => 18 | modulesService.getModuleIssuesWithParams( 19 | workspaceSlug.toString(), 20 | projectId.toString(), 21 | moduleId.toString() 22 | ) 23 | : null 24 | ); 25 | 26 | return { 27 | ganttIssues, 28 | mutateGanttIssues, 29 | }; 30 | }; 31 | 32 | export default useGanttChartModuleIssues; 33 | -------------------------------------------------------------------------------- /apps/app/components/rich-text-editor/toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | // buttons 2 | import { 3 | ToggleBoldButton, 4 | ToggleItalicButton, 5 | ToggleUnderlineButton, 6 | ToggleStrikeButton, 7 | ToggleOrderedListButton, 8 | ToggleBulletListButton, 9 | RedoButton, 10 | UndoButton, 11 | } from "@remirror/react"; 12 | // headings 13 | import HeadingControls from "./heading-controls"; 14 | 15 | export const RichTextToolbar: React.FC = () => ( 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | ); 36 | -------------------------------------------------------------------------------- /apps/app/pages/api/track-event.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | // jitsu 4 | import { createClient } from "@jitsu/nextjs"; 5 | import { convertCookieStringToObject } from "lib/cookie"; 6 | 7 | const jitsu = createClient({ 8 | key: process.env.TRACKER_ACCESS_KEY || "", 9 | tracking_host: "https://t.jitsu.com", 10 | }); 11 | 12 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 13 | const { eventName, user, extra } = req.body; 14 | 15 | if (!eventName) { 16 | return res.status(400).json({ message: "Bad request" }); 17 | } 18 | 19 | if (!user) return res.status(401).json({ message: "Unauthorized" }); 20 | 21 | // TODO: cache user info 22 | 23 | jitsu 24 | .id({ 25 | id: user.id, 26 | email: user.email, 27 | first_name: user.first_name, 28 | last_name: user.last_name, 29 | }) 30 | .then(() => { 31 | jitsu.track(eventName, { 32 | ...extra, 33 | }); 34 | }); 35 | 36 | res.status(200).json({ message: "success" }); 37 | } 38 | -------------------------------------------------------------------------------- /apiserver/plane/tests/api/base.py: -------------------------------------------------------------------------------- 1 | # Third party imports 2 | from rest_framework.test import APITestCase, APIClient 3 | 4 | # Module imports 5 | from plane.db.models import User 6 | from plane.api.views.authentication import get_tokens_for_user 7 | 8 | 9 | class BaseAPITest(APITestCase): 10 | def setUp(self): 11 | self.client = APIClient(HTTP_USER_AGENT="plane/test", REMOTE_ADDR="10.10.10.10") 12 | 13 | 14 | class AuthenticatedAPITest(BaseAPITest): 15 | def setUp(self): 16 | super().setUp() 17 | 18 | ## Create Dummy User 19 | self.email = "user@plane.so" 20 | user = User.objects.create(email=self.email) 21 | user.set_password("user@123") 22 | user.save() 23 | 24 | # Set user 25 | self.user = user 26 | 27 | # Set Up User ID 28 | self.user_id = user.id 29 | 30 | access_token, _ = get_tokens_for_user(user) 31 | self.access_token = access_token 32 | 33 | # Set Up Authentication Token 34 | self.client.credentials(HTTP_AUTHORIZATION="Bearer " + access_token) 35 | -------------------------------------------------------------------------------- /apps/app/components/integration/github/import-confirm.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | // react-hook-form 4 | import { UseFormWatch } from "react-hook-form"; 5 | // ui 6 | import { PrimaryButton, SecondaryButton } from "components/ui"; 7 | // types 8 | import { TFormValues, TIntegrationSteps } from "components/integration"; 9 | 10 | type Props = { 11 | handleStepChange: (value: TIntegrationSteps) => void; 12 | watch: UseFormWatch; 13 | }; 14 | 15 | export const GithubImportConfirm: FC = ({ handleStepChange, watch }) => ( 16 |
17 |

18 | You are about to import issues from {watch("github").full_name}. Click on {'"'}Confirm & 19 | Import{'" '} 20 | to complete the process. 21 |

22 |
23 | handleStepChange("import-users")}>Back 24 | Confirm & Import 25 |
26 |
27 | ); 28 | -------------------------------------------------------------------------------- /apps/app/components/ui/buttons/secondary-button.tsx: -------------------------------------------------------------------------------- 1 | // types 2 | import { ButtonProps } from "./type"; 3 | 4 | export const SecondaryButton: React.FC = ({ 5 | children, 6 | className = "", 7 | onClick, 8 | type = "button", 9 | disabled = false, 10 | loading = false, 11 | size = "sm", 12 | outline = false, 13 | }) => ( 14 | 32 | ); 33 | -------------------------------------------------------------------------------- /apps/app/hooks/use-sub-issue.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { useRouter } from "next/router"; 4 | 5 | import useSWR from "swr"; 6 | 7 | // services 8 | import issuesService from "services/issues.service"; 9 | // types 10 | import { ISubIssueResponse } from "types"; 11 | // fetch-keys 12 | import { SUB_ISSUES } from "constants/fetch-keys"; 13 | 14 | const useSubIssue = (issueId: string, isExpanded: boolean) => { 15 | const router = useRouter(); 16 | const { workspaceSlug, projectId } = router.query; 17 | 18 | const shouldFetch = workspaceSlug && projectId && issueId && isExpanded; 19 | 20 | const { data: subIssuesResponse, isLoading } = useSWR( 21 | shouldFetch ? SUB_ISSUES(issueId as string) : null, 22 | shouldFetch 23 | ? () => 24 | issuesService.subIssues(workspaceSlug as string, projectId as string, issueId as string) 25 | : null 26 | ); 27 | 28 | return { 29 | subIssues: subIssuesResponse?.sub_issues ?? [], 30 | isLoading, 31 | }; 32 | }; 33 | 34 | export default useSubIssue; 35 | -------------------------------------------------------------------------------- /apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import useSWR from "swr"; 4 | 5 | // services 6 | import cyclesService from "services/cycles.service"; 7 | // components 8 | import { CyclesView } from "components/cycles"; 9 | // fetch-keys 10 | import { UPCOMING_CYCLES_LIST } from "constants/fetch-keys"; 11 | 12 | type Props = { 13 | viewType: string | null; 14 | }; 15 | 16 | export const UpcomingCyclesList: React.FC = ({ viewType }) => { 17 | const router = useRouter(); 18 | const { workspaceSlug, projectId } = router.query; 19 | 20 | const { data: upcomingCyclesList } = useSWR( 21 | workspaceSlug && projectId ? UPCOMING_CYCLES_LIST(projectId.toString()) : null, 22 | workspaceSlug && projectId 23 | ? () => 24 | cyclesService.getCyclesWithParams( 25 | workspaceSlug.toString(), 26 | projectId.toString(), 27 | "upcoming" 28 | ) 29 | : null 30 | ); 31 | 32 | return ; 33 | }; 34 | -------------------------------------------------------------------------------- /apps/app/components/cycles/cycles-list/completed-cycles-list.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import useSWR from "swr"; 4 | 5 | // services 6 | import cyclesService from "services/cycles.service"; 7 | // components 8 | import { CyclesView } from "components/cycles"; 9 | // fetch-keys 10 | import { COMPLETED_CYCLES_LIST } from "constants/fetch-keys"; 11 | 12 | type Props = { 13 | viewType: string | null; 14 | }; 15 | 16 | export const CompletedCyclesList: React.FC = ({ viewType }) => { 17 | const router = useRouter(); 18 | const { workspaceSlug, projectId } = router.query; 19 | 20 | const { data: completedCyclesList } = useSWR( 21 | workspaceSlug && projectId ? COMPLETED_CYCLES_LIST(projectId.toString()) : null, 22 | workspaceSlug && projectId 23 | ? () => 24 | cyclesService.getCyclesWithParams( 25 | workspaceSlug.toString(), 26 | projectId.toString(), 27 | "completed" 28 | ) 29 | : null 30 | ); 31 | 32 | return ; 33 | }; 34 | -------------------------------------------------------------------------------- /apps/app/components/ui/buttons/primary-button.tsx: -------------------------------------------------------------------------------- 1 | // types 2 | import { ButtonProps } from "./type"; 3 | 4 | export const PrimaryButton: React.FC = ({ 5 | children, 6 | className = "", 7 | onClick, 8 | type = "button", 9 | disabled = false, 10 | loading = false, 11 | size = "sm", 12 | outline = false, 13 | }) => ( 14 | 32 | ); 33 | -------------------------------------------------------------------------------- /apps/app/components/ui/graphs/line-graph.tsx: -------------------------------------------------------------------------------- 1 | // nivo 2 | import { ResponsiveLine, LineSvgProps } from "@nivo/line"; 3 | // helpers 4 | import { generateYAxisTickValues } from "helpers/graph.helper"; 5 | // types 6 | import { TGraph } from "./types"; 7 | // constants 8 | import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; 9 | 10 | type Props = { 11 | customYAxisTickValues?: number[]; 12 | }; 13 | 14 | export const LineGraph: React.FC = ({ 15 | customYAxisTickValues, 16 | height = "400px", 17 | width = "100%", 18 | margin, 19 | theme, 20 | ...rest 21 | }) => ( 22 |
23 | 36 |
37 | ); 38 | -------------------------------------------------------------------------------- /apps/app/components/icons/pencil-scribble-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const PencilScribbleIcon: React.FC = ({ 6 | width = "20", 7 | height = "20", 8 | className, 9 | color = "#000000", 10 | }) => ( 11 | 18 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /apps/app/components/icons/contrast-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const ContrastIcon: React.FC = ({ 6 | width = "24", 7 | height = "24", 8 | color = "rgb(var(--color-text-200))", 9 | className, 10 | }) => ( 11 | 19 | 20 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /apps/app/components/icons/comment-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const CommentIcon: React.FC = ({ width = "24", height = "24", className }) => ( 6 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /apps/app/constants/inbox.ts: -------------------------------------------------------------------------------- 1 | export const INBOX_STATUS = [ 2 | { 3 | key: "pending", 4 | label: "Pending", 5 | value: -2, 6 | textColor: "text-yellow-500", 7 | bgColor: "bg-yellow-500/10", 8 | borderColor: "border-yellow-500", 9 | }, 10 | { 11 | key: "declined", 12 | label: "Declined", 13 | value: -1, 14 | textColor: "text-red-500", 15 | bgColor: "bg-red-500/10", 16 | borderColor: "border-red-500", 17 | }, 18 | { 19 | key: "snoozed", 20 | label: "Snoozed", 21 | value: 0, 22 | textColor: "text-custom-text-200", 23 | bgColor: "bg-gray-500/10", 24 | borderColor: "border-gray-500", 25 | }, 26 | { 27 | key: "accepted", 28 | label: "Accepted", 29 | value: 1, 30 | textColor: "text-green-500", 31 | bgColor: "bg-green-500/10", 32 | borderColor: "border-green-500", 33 | }, 34 | { 35 | key: "duplicate", 36 | label: "Duplicate", 37 | value: 2, 38 | textColor: "text-custom-text-200", 39 | bgColor: "bg-gray-500/10", 40 | borderColor: "border-gray-500", 41 | }, 42 | ]; 43 | 44 | export const INBOX_ISSUE_SOURCE = "in-app"; 45 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0034_auto_20230628_1046.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.19 on 2023-06-28 05:16 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0033_auto_20230618_2125'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='timelineissue', 15 | name='created_by', 16 | ), 17 | migrations.RemoveField( 18 | model_name='timelineissue', 19 | name='issue', 20 | ), 21 | migrations.RemoveField( 22 | model_name='timelineissue', 23 | name='project', 24 | ), 25 | migrations.RemoveField( 26 | model_name='timelineissue', 27 | name='updated_by', 28 | ), 29 | migrations.RemoveField( 30 | model_name='timelineissue', 31 | name='workspace', 32 | ), 33 | migrations.DeleteModel( 34 | name='Shortcut', 35 | ), 36 | migrations.DeleteModel( 37 | name='TimelineIssue', 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /apps/app/components/gantt-chart/types/index.ts: -------------------------------------------------------------------------------- 1 | // context types 2 | export type allViewsType = { 3 | key: string; 4 | title: string; 5 | data: Object | null; 6 | }; 7 | 8 | export interface ChartContextData { 9 | allViews: allViewsType[]; 10 | currentView: "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year"; 11 | currentViewData: any; 12 | renderView: any; 13 | } 14 | 15 | export type ChartContextActionPayload = { 16 | type: "CURRENT_VIEW" | "CURRENT_VIEW_DATA" | "PARTIAL_UPDATE" | "RENDER_VIEW"; 17 | payload: any; 18 | }; 19 | 20 | export interface ChartContextReducer extends ChartContextData { 21 | dispatch: (action: ChartContextActionPayload) => void; 22 | } 23 | 24 | // chart render types 25 | export interface WeekMonthDataType { 26 | key: number; 27 | shortTitle: string; 28 | title: string; 29 | } 30 | 31 | export interface ChartDataType { 32 | key: string; 33 | title: string; 34 | data: ChartDataTypeData; 35 | } 36 | 37 | export interface ChartDataTypeData { 38 | startDate: Date; 39 | currentDate: Date; 40 | endDate: Date; 41 | approxFilterRange: number; 42 | width: number; 43 | } 44 | -------------------------------------------------------------------------------- /apps/app/next.config.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config({ path: ".env" }); 2 | 3 | const { withSentryConfig } = require("@sentry/nextjs"); 4 | const path = require("path"); 5 | const extraImageDomains = (process.env.NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS ?? "") 6 | .split(",") 7 | .filter((domain) => domain.length > 0); 8 | 9 | const nextConfig = { 10 | reactStrictMode: false, 11 | swcMinify: true, 12 | images: { 13 | domains: [ 14 | "vinci-web.s3.amazonaws.com", 15 | "planefs-staging.s3.ap-south-1.amazonaws.com", 16 | "planefs.s3.amazonaws.com", 17 | "images.unsplash.com", 18 | "avatars.githubusercontent.com", 19 | "localhost", 20 | ...extraImageDomains, 21 | ], 22 | }, 23 | output: "standalone", 24 | experimental: { 25 | // this includes files from the monorepo base two directories up 26 | outputFileTracingRoot: path.join(__dirname, "../../"), 27 | }, 28 | }; 29 | 30 | if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0")) { 31 | module.exports = withSentryConfig(nextConfig, { silent: true }, { hideSourceMaps: true }); 32 | } else { 33 | module.exports = nextConfig; 34 | } 35 | -------------------------------------------------------------------------------- /apiserver/plane/db/models/integration/slack.py: -------------------------------------------------------------------------------- 1 | # Python imports 2 | import uuid 3 | 4 | # Django imports 5 | from django.db import models 6 | 7 | # Module imports 8 | from plane.db.models import ProjectBaseModel 9 | 10 | 11 | class SlackProjectSync(ProjectBaseModel): 12 | access_token = models.CharField(max_length=300) 13 | scopes = models.TextField() 14 | bot_user_id = models.CharField(max_length=50) 15 | webhook_url = models.URLField(max_length=1000) 16 | data = models.JSONField(default=dict) 17 | team_id = models.CharField(max_length=30) 18 | team_name = models.CharField(max_length=300) 19 | workspace_integration = models.ForeignKey( 20 | "db.WorkspaceIntegration", related_name="slack_syncs", on_delete=models.CASCADE 21 | ) 22 | 23 | def __str__(self): 24 | """Return the repo name""" 25 | return f"{self.project.name}" 26 | 27 | class Meta: 28 | unique_together = ["team_id", "project"] 29 | verbose_name = "Slack Project Sync" 30 | verbose_name_plural = "Slack Project Syncs" 31 | db_table = "slack_project_syncs" 32 | ordering = ("-created_at",) 33 | -------------------------------------------------------------------------------- /apps/app/components/ui/buttons/danger-button.tsx: -------------------------------------------------------------------------------- 1 | // types 2 | import { ButtonProps } from "./type"; 3 | 4 | export const DangerButton: React.FC = ({ 5 | children, 6 | className = "", 7 | onClick, 8 | type = "button", 9 | disabled = false, 10 | loading = false, 11 | size = "sm", 12 | outline = false, 13 | }) => ( 14 | 36 | ); 37 | -------------------------------------------------------------------------------- /apiserver/plane/settings/test.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .common import * # noqa 4 | 5 | DEBUG = True 6 | 7 | INSTALLED_APPS.append("plane.tests") 8 | 9 | if os.environ.get('GITHUB_WORKFLOW'): 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.postgresql', 13 | 'NAME': 'github_actions', 14 | 'USER': 'postgres', 15 | 'PASSWORD': 'postgres', 16 | 'HOST': '127.0.0.1', 17 | 'PORT': '5432', 18 | } 19 | } 20 | else: 21 | DATABASES = { 22 | 'default': { 23 | 'ENGINE': 'django.db.backends.postgresql', 24 | 'NAME': 'plane_test', 25 | 'USER': 'postgres', 26 | 'PASSWORD': 'password123', 27 | 'HOST': '127.0.0.1', 28 | 'PORT': '5432', 29 | } 30 | } 31 | 32 | REDIS_HOST = "localhost" 33 | REDIS_PORT = 6379 34 | REDIS_URL = False 35 | 36 | RQ_QUEUES = { 37 | "default": { 38 | "HOST": "localhost", 39 | "PORT": 6379, 40 | "DB": 0, 41 | "DEFAULT_TIMEOUT": 360, 42 | }, 43 | } 44 | 45 | WEB_URL = "http://localhost:3000" 46 | -------------------------------------------------------------------------------- /apps/app/constants/due-dates.ts: -------------------------------------------------------------------------------- 1 | // helper 2 | import { renderDateFormat } from "helpers/date-time.helper"; 3 | 4 | export const DUE_DATES = [ 5 | { 6 | name: "Last week", 7 | value: [ 8 | `${renderDateFormat(new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000))};after`, 9 | `${renderDateFormat(new Date())};before`, 10 | ], 11 | }, 12 | { 13 | name: "2 weeks from now", 14 | value: [ 15 | `${renderDateFormat(new Date())};after`, 16 | `${renderDateFormat(new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000))};before`, 17 | ], 18 | }, 19 | { 20 | name: "1 month from now", 21 | value: [ 22 | `${renderDateFormat(new Date())};after`, 23 | `${renderDateFormat( 24 | new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()) 25 | )};before`, 26 | ], 27 | }, 28 | { 29 | name: "2 months from now", 30 | value: [ 31 | `${renderDateFormat(new Date())};after`, 32 | `${renderDateFormat( 33 | new Date(new Date().getFullYear(), new Date().getMonth() + 2, new Date().getDate()) 34 | )};before`, 35 | ], 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /apps/app/layouts/auth-layout/user-authorization-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import useSWR from "swr"; 4 | 5 | // services 6 | import userService from "services/user.service"; 7 | // ui 8 | import { Spinner } from "components/ui"; 9 | // fetch-keys 10 | import { CURRENT_USER } from "constants/fetch-keys"; 11 | 12 | type Props = { 13 | children: React.ReactNode; 14 | }; 15 | 16 | export const UserAuthorizationLayout: React.FC = ({ children }) => { 17 | const router = useRouter(); 18 | 19 | const { data: currentUser, error } = useSWR(CURRENT_USER, () => userService.currentUser()); 20 | 21 | if (!currentUser && !error) { 22 | return ( 23 |
24 |
25 |

Loading your profile...

26 | 27 |
28 |
29 | ); 30 | } 31 | 32 | if (error) { 33 | const redirectTo = router.asPath; 34 | 35 | router.push(`/?next=${redirectTo}`); 36 | return null; 37 | } 38 | 39 | return <>{children}; 40 | }; 41 | -------------------------------------------------------------------------------- /apps/app/hooks/gantt-chart/view-issues-view.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | 3 | // services 4 | import issuesService from "services/issues.service"; 5 | // hooks 6 | import useIssuesView from "hooks/use-issues-view"; 7 | // fetch-keys 8 | import { VIEW_ISSUES } from "constants/fetch-keys"; 9 | 10 | const useGanttChartViewIssues = ( 11 | workspaceSlug: string | undefined, 12 | projectId: string | undefined, 13 | viewId: string | undefined 14 | ) => { 15 | const { params } = useIssuesView(); 16 | const { order_by, group_by, ...viewGanttParams } = params; 17 | 18 | // all issues under the view 19 | const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( 20 | workspaceSlug && projectId && viewId ? VIEW_ISSUES(viewId.toString(), viewGanttParams) : null, 21 | workspaceSlug && projectId && viewId 22 | ? () => 23 | issuesService.getIssuesWithParams( 24 | workspaceSlug.toString(), 25 | projectId.toString(), 26 | viewGanttParams 27 | ) 28 | : null 29 | ); 30 | 31 | return { 32 | ganttIssues, 33 | mutateGanttIssues, 34 | }; 35 | }; 36 | 37 | export default useGanttChartViewIssues; 38 | -------------------------------------------------------------------------------- /apps/app/components/icons/cycle-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const CyclesIcon: React.FC = ({ 6 | width = "24", 7 | height = "24", 8 | className, 9 | color = "rgb(var(--color-text-200))", 10 | }) => ( 11 | 19 | 26 | 33 | 34 | ); 35 | -------------------------------------------------------------------------------- /apiserver/plane/middleware/user_middleware.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import pytz 3 | from django.conf import settings 4 | from django.utils import timezone 5 | from plane.db.models import User 6 | 7 | 8 | class UserMiddleware(object): 9 | 10 | def __init__(self, get_response): 11 | self.get_response = get_response 12 | 13 | def __call__(self, request): 14 | 15 | try: 16 | if request.headers.get("Authorization"): 17 | authorization_header = request.headers.get("Authorization") 18 | access_token = authorization_header.split(" ")[1] 19 | decoded = jwt.decode( 20 | access_token, settings.SECRET_KEY, algorithms=["HS256"] 21 | ) 22 | id = decoded['user_id'] 23 | user = User.objects.get(id=id) 24 | user.last_active = timezone.now() 25 | user.token_updated_at = None 26 | user.save() 27 | timezone.activate(pytz.timezone(user.user_timezone)) 28 | except Exception as e: 29 | print(e) 30 | 31 | response = self.get_response(request) 32 | 33 | return response 34 | -------------------------------------------------------------------------------- /apps/app/components/icons/plus-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const PlusIcon: React.FC = ({ width = "24", height = "24", className }) => ( 6 | 14 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /apps/app/components/icons/view-list-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const ViewListIcon: React.FC = ({ 6 | width = "24", 7 | height = "24", 8 | className, 9 | color = "rgb(var(--color-text-200))", 10 | }) => ( 11 | 19 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0010_auto_20221213_0037.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.14 on 2022-12-13 18:18 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('db', '0009_auto_20221208_0310'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='projectidentifier', 16 | name='workspace', 17 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_identifiers', to='db.workspace'), 18 | ), 19 | migrations.AlterField( 20 | model_name='project', 21 | name='identifier', 22 | field=models.CharField(max_length=5, verbose_name='Project Identifier'), 23 | ), 24 | migrations.AlterUniqueTogether( 25 | name='project', 26 | unique_together={('name', 'workspace'), ('identifier', 'workspace')}, 27 | ), 28 | migrations.AlterUniqueTogether( 29 | name='projectidentifier', 30 | unique_together={('name', 'workspace')}, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /apps/app/components/icons/check.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const CheckIcon: React.FC = ({ 6 | width = "24", 7 | height = "24", 8 | color = "rgb(var(--color-text-200))", 9 | className, 10 | }) => ( 11 | 19 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /apps/app/components/ui/linear-progress-indicator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Tooltip } from "./tooltip"; 3 | 4 | type Props = { 5 | data: any; 6 | noTooltip?: boolean 7 | }; 8 | 9 | export const LinearProgressIndicator: React.FC = ({ data, noTooltip=false }) => { 10 | const total = data.reduce((acc: any, cur: any) => acc + cur.value, 0); 11 | let progress = 0; 12 | 13 | const bars = data.map((item: any) => { 14 | const width = `${(item.value / total) * 100}%`; 15 | const style = { 16 | width, 17 | backgroundColor: item.color, 18 | }; 19 | progress += item.value; 20 | if (noTooltip) return
21 | else return ( 22 | 23 |
24 | 25 | ); 26 | }); 27 | 28 | return ( 29 |
30 | {total === 0 ? ( 31 |
{bars}
32 | ) : ( 33 |
{bars}
34 | )} 35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /apps/app/hooks/use-workspace-details.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useRouter } from "next/router"; 3 | import useSWR from "swr"; 4 | // services 5 | import workspaceService from "services/workspace.service"; 6 | // fetch-keys 7 | import { WORKSPACE_DETAILS } from "constants/fetch-keys"; 8 | 9 | const useWorkspaceDetails = () => { 10 | const router = useRouter(); 11 | const { workspaceSlug } = router.query; 12 | // Fetching Workspace Details 13 | const { 14 | data: workspaceDetails, 15 | error: workspaceDetailsError, 16 | mutate: mutateWorkspaceDetails, 17 | } = useSWR( 18 | workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, 19 | workspaceSlug ? () => workspaceService.getWorkspace(workspaceSlug as string) : null 20 | ); 21 | 22 | useEffect(() => { 23 | if (workspaceDetailsError?.status === 404) { 24 | router.push("/404"); 25 | } else if (workspaceDetailsError) { 26 | router.push("/error"); 27 | } 28 | }, [workspaceDetailsError, router]); 29 | 30 | return { 31 | workspaceDetails, 32 | workspaceDetailsError, 33 | mutateWorkspaceDetails, 34 | }; 35 | }; 36 | 37 | export default useWorkspaceDetails; 38 | -------------------------------------------------------------------------------- /apiserver/plane/api/serializers/analytic.py: -------------------------------------------------------------------------------- 1 | from .base import BaseSerializer 2 | from plane.db.models import AnalyticView 3 | from plane.utils.issue_filters import issue_filters 4 | 5 | 6 | class AnalyticViewSerializer(BaseSerializer): 7 | class Meta: 8 | model = AnalyticView 9 | fields = "__all__" 10 | read_only_fields = [ 11 | "workspace", 12 | "query", 13 | ] 14 | 15 | def create(self, validated_data): 16 | query_params = validated_data.get("query_dict", {}) 17 | if bool(query_params): 18 | validated_data["query"] = issue_filters(query_params, "POST") 19 | else: 20 | validated_data["query"] = dict() 21 | return AnalyticView.objects.create(**validated_data) 22 | 23 | def update(self, instance, validated_data): 24 | query_params = validated_data.get("query_data", {}) 25 | if bool(query_params): 26 | validated_data["query"] = issue_filters(query_params, "POST") 27 | else: 28 | validated_data["query"] = dict() 29 | validated_data["query"] = issue_filters(query_params, "PATCH") 30 | return super().update(instance, validated_data) 31 | -------------------------------------------------------------------------------- /apps/app/services/ai.service.ts: -------------------------------------------------------------------------------- 1 | // services 2 | import APIService from "services/api.service"; 3 | import trackEventServices from "services/track-event.service"; 4 | 5 | // types 6 | import { ICurrentUserResponse, IGptResponse } from "types"; 7 | 8 | const { NEXT_PUBLIC_API_BASE_URL } = process.env; 9 | 10 | const trackEvent = 11 | process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; 12 | 13 | class AiServices extends APIService { 14 | constructor() { 15 | super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); 16 | } 17 | 18 | async createGptTask( 19 | workspaceSlug: string, 20 | projectId: string, 21 | data: { prompt: string; task: string }, 22 | user: ICurrentUserResponse | undefined 23 | ): Promise { 24 | return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data) 25 | .then((response) => { 26 | if (trackEvent) trackEventServices.trackAskGptEvent(response?.data, "ASK_GPT", user); 27 | return response?.data; 28 | }) 29 | .catch((error) => { 30 | throw error?.response; 31 | }); 32 | } 33 | } 34 | 35 | export default new AiServices(); 36 | -------------------------------------------------------------------------------- /apps/app/layouts/app-layout/app-header.tsx: -------------------------------------------------------------------------------- 1 | // icons 2 | import { Bars3Icon } from "@heroicons/react/24/outline"; 3 | 4 | type Props = { 5 | breadcrumbs?: JSX.Element; 6 | left?: JSX.Element; 7 | right?: JSX.Element; 8 | setToggleSidebar: React.Dispatch>; 9 | noHeader: boolean; 10 | }; 11 | 12 | const Header: React.FC = ({ breadcrumbs, left, right, setToggleSidebar, noHeader }) => ( 13 |
18 |
19 |
20 | 27 |
28 | {breadcrumbs} 29 | {left} 30 |
31 | {right} 32 |
33 | ); 34 | 35 | export default Header; 36 | -------------------------------------------------------------------------------- /apiserver/plane/bgtasks/user_welcome_task.py: -------------------------------------------------------------------------------- 1 | # Django imports 2 | from django.conf import settings 3 | 4 | # Third party imports 5 | from celery import shared_task 6 | from sentry_sdk import capture_exception 7 | from slack_sdk import WebClient 8 | from slack_sdk.errors import SlackApiError 9 | 10 | # Module imports 11 | from plane.db.models import User 12 | 13 | 14 | @shared_task 15 | def send_welcome_slack(user_id, created, message): 16 | try: 17 | instance = User.objects.get(pk=user_id) 18 | 19 | if created and not instance.is_bot: 20 | # Send message on slack as well 21 | if settings.SLACK_BOT_TOKEN: 22 | client = WebClient(token=settings.SLACK_BOT_TOKEN) 23 | try: 24 | _ = client.chat_postMessage( 25 | channel="#trackers", 26 | text=message, 27 | ) 28 | except SlackApiError as e: 29 | print(f"Got an error: {e.response['error']}") 30 | return 31 | except Exception as e: 32 | # Print logs if in DEBUG mode 33 | if settings.DEBUG: 34 | print(e) 35 | capture_exception(e) 36 | return 37 | -------------------------------------------------------------------------------- /apps/app/constants/themes.ts: -------------------------------------------------------------------------------- 1 | export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"]; 2 | 3 | export const THEMES_OBJ = [ 4 | { 5 | value: "light", 6 | label: "Light", 7 | type: "light", 8 | icon: { 9 | border: "#DEE2E6", 10 | color1: "#FAFAFA", 11 | color2: "#3F76FF", 12 | }, 13 | }, 14 | { 15 | value: "dark", 16 | label: "Dark", 17 | type: "dark", 18 | icon: { 19 | border: "#2E3234", 20 | color1: "#191B1B", 21 | color2: "#3C85D9", 22 | }, 23 | }, 24 | { 25 | value: "light-contrast", 26 | label: "Light High Contrast", 27 | type: "light", 28 | icon: { 29 | border: "#000000", 30 | color1: "#FFFFFF", 31 | color2: "#3F76FF", 32 | }, 33 | }, 34 | { 35 | value: "dark-contrast", 36 | label: "Dark High Contrast", 37 | type: "dark", 38 | icon: { 39 | border: "#FFFFFF", 40 | color1: "#030303", 41 | color2: "#3A8BE9", 42 | }, 43 | }, 44 | { 45 | value: "custom", 46 | label: "Custom Theming", 47 | type: "light", 48 | icon: { 49 | border: "#FFC9C9", 50 | color1: "#FFF7F7", 51 | color2: "#FF5151", 52 | }, 53 | }, 54 | ]; 55 | -------------------------------------------------------------------------------- /apiserver/plane/db/models/social_connection.py: -------------------------------------------------------------------------------- 1 | # Django imports 2 | from django.db import models 3 | from django.conf import settings 4 | from django.utils import timezone 5 | 6 | # Module import 7 | from . import BaseModel 8 | 9 | 10 | class SocialLoginConnection(BaseModel): 11 | medium = models.CharField( 12 | max_length=20, 13 | choices=(("Google", "google"), ("Github", "github")), 14 | default=None, 15 | ) 16 | last_login_at = models.DateTimeField(default=timezone.now, null=True) 17 | last_received_at = models.DateTimeField(default=timezone.now, null=True) 18 | user = models.ForeignKey( 19 | settings.AUTH_USER_MODEL, 20 | on_delete=models.CASCADE, 21 | related_name="user_login_connections", 22 | ) 23 | token_data = models.JSONField(null=True) 24 | extra_data = models.JSONField(null=True) 25 | 26 | class Meta: 27 | verbose_name = "Social Login Connection" 28 | verbose_name_plural = "Social Login Connections" 29 | db_table = "social_login_connections" 30 | ordering = ("-created_at",) 31 | 32 | def __str__(self): 33 | """Return name of the user and medium""" 34 | return f"{self.medium} <{self.user.email}>" 35 | -------------------------------------------------------------------------------- /apiserver/plane/db/migrations/0013_auto_20230107_0041.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2023-01-06 19:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('db', '0012_auto_20230104_0117'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='issue', 15 | name='description_html', 16 | field=models.TextField(blank=True), 17 | ), 18 | migrations.AddField( 19 | model_name='issue', 20 | name='description_stripped', 21 | field=models.TextField(blank=True), 22 | ), 23 | migrations.AddField( 24 | model_name='user', 25 | name='role', 26 | field=models.CharField(blank=True, max_length=300, null=True), 27 | ), 28 | migrations.AddField( 29 | model_name='workspacemember', 30 | name='view_props', 31 | field=models.JSONField(blank=True, null=True), 32 | ), 33 | migrations.AlterField( 34 | model_name='issue', 35 | name='description', 36 | field=models.JSONField(blank=True), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /apiserver/plane/db/models/api_token.py: -------------------------------------------------------------------------------- 1 | # Python imports 2 | from uuid import uuid4 3 | 4 | # Django imports 5 | from django.db import models 6 | from django.conf import settings 7 | 8 | from .base import BaseModel 9 | 10 | 11 | def generate_label_token(): 12 | return uuid4().hex 13 | 14 | 15 | def generate_token(): 16 | return uuid4().hex + uuid4().hex 17 | 18 | 19 | class APIToken(BaseModel): 20 | token = models.CharField(max_length=255, unique=True, default=generate_token) 21 | label = models.CharField(max_length=255, default=generate_label_token) 22 | user = models.ForeignKey( 23 | settings.AUTH_USER_MODEL, 24 | on_delete=models.CASCADE, 25 | related_name="bot_tokens", 26 | ) 27 | user_type = models.PositiveSmallIntegerField( 28 | choices=((0, "Human"), (1, "Bot")), default=0 29 | ) 30 | workspace = models.ForeignKey( 31 | "db.Workspace", related_name="api_tokens", on_delete=models.CASCADE, null=True 32 | ) 33 | 34 | class Meta: 35 | verbose_name = "API Token" 36 | verbose_name_plural = "API Tokems" 37 | db_table = "api_tokens" 38 | ordering = ("-created_at",) 39 | 40 | def __str__(self): 41 | return str(self.user.name) 42 | -------------------------------------------------------------------------------- /apps/app/components/icons/x-mark-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Props } from "./types"; 4 | 5 | export const XMarkIcon: React.FC = ({ width = "24", height = "24", className, color }) => ( 6 | 14 | 18 | 19 | ); 20 | --------------------------------------------------------------------------------